Two Verilog Interview Questions Worked Through: Open-Drain Outputs and a Nonblocking Delay Puzzle

Interview questions for verification and design roles tend to probe two things: whether you understand the hardware you are modelling, and whether you understand the simulator that is modelling it. This post takes one question of each kind, both originally published here as short notes in 2015 and 2016, and works them through properly. The second one is worth reading even if you have seen it before, because the original answer on this blog was wrong, and the way it was wrong is the whole lesson.

Question 1: What is an open-drain output, and why would you use one?

An open-drain output (open-collector in bipolar logic) is a digital output stage that contains a single pull-down transistor and nothing else. It can do exactly two things: pull the line to ground, or let go of it. It can never drive the line high. A resistor to the supply, the pull-up, provides the high level whenever nobody is pulling low.

TransistorOutputWho sets the level
OffHigh impedanceThe pull-up resistor pulls the line to VDD
OnLowThe transistor sinks current to ground

That asymmetry is the feature, not a limitation. Because no device ever drives high, any number of open-drain outputs can share one wire without contention. If several push-pull outputs shared a wire, one driving high and another driving low would short the supply to ground through the two transistors. With open-drain outputs the worst case is several devices pulling low at once, which is harmless.

Wired-AND and wired-OR

A shared open-drain line is low if any device pulls it low, and high only when all devices release it. Read with active-high logic, that is an AND of the released states, so the arrangement is called wired-AND. Read with active-low logic, where "asserted" means low, the line is asserted if any device asserts, which is a wired-OR. Same wire, two names, depending on which polarity you call true.

Verilog models this directly with the wand and wor net types, or by resolving multiple assign statements that each drive either 0 or z. Here is a three-device line, run with Icarus Verilog:


// Three open-drain drivers sharing one line
module wired_and_demo;
  reg d1, d2, d3;                   // 0 = pull low, 1 = release
  wand line;                        // wired-AND net: low if any driver is low
  pullup (line);                    // the external pull-up resistor
  assign line = d1 ? 1'bz : 1'b0;   // open-drain: drive 0 or release
  assign line = d2 ? 1'bz : 1'b0;
  assign line = d3 ? 1'bz : 1'b0;

  initial begin
    $monitor("t=%0t d1=%b d2=%b d3=%b line=%b", $time, d1, d2, d3, line);
    {d1,d2,d3} = 3'b111; #10;       // everyone released: pull-up wins
    {d1,d2,d3} = 3'b110; #10;       // one device pulls low
    {d1,d2,d3} = 3'b000; #10;       // all pull low: no contention
    {d1,d2,d3} = 3'b111; #10;
    $finish;
  end
endmodule

t=0  d1=1 d2=1 d3=1 line=1
t=10 d1=1 d2=1 d3=0 line=0
t=20 d1=0 d2=0 d3=0 line=0
t=30 d1=1 d2=1 d3=1 line=1

The pullup primitive is the part most answers forget. Without it, a wand net that every driver has released resolves to z, not 1, which is exactly what happens on a real board when someone forgets to fit the resistor.

Where it shows up

ApplicationWhy open-drain
I2C SDA and SCLMulti-controller bus. Clock stretching works because any target can hold SCL low.
Shared interrupt request linesSeveral peripherals assert one IRQ; the CPU polls to find which.
Fault and alert pins (SMBus ALERT#, SPI fault lines)Any device can flag an error without an arbiter.
Reset distributionSeveral reset sources, including a supervisor chip and a debug probe, can each hold the system in reset.
Level shiftingAn open-drain line can be pulled up to a different voltage than the driver's supply.

The follow-up questions

Interviewers rarely stop at the definition. Expect these:

  • Why is I2C slow? The pull-up resistor charges the bus capacitance on every rising edge, an RC ramp, while falling edges are driven by a transistor and are fast. Speed is limited by how small a resistor you can tolerate for power and drive strength. Fast-mode I2C reaches 400 kHz, and the higher speed modes need active pull-up assistance.
  • How do you pick the pull-up value? Small enough that the RC rise time meets the bus timing spec, large enough that a device pulling low does not exceed its sink current rating. Vendors publish the arithmetic. For a 3.3 V I2C bus with modest capacitance, values between 1 kΩ and 10 kΩ are typical.
  • How would you check this in a testbench? Model the pull-up with pullup or a weak drive, model each device as driving 0 or z, and assert that no device ever drives 1. A device that drives high on an open-drain bus is a real bug that simulation with plain wire nets will hide as an x.

Question 2: What do clk and a look like?

This one, from an Analog Devices interview, is about the simulator rather than the hardware. Read the code and predict the waveform before scrolling down.


module stratified_event;
  reg a, clk;

  always
    #10 clk = ~clk;

  always @(posedge clk) begin
    $display($time);
    a <= #15 clk;
  end

  initial begin
    clk = 1'b1;
    a = 1'b0;
    #200 $finish;
  end

endmodule

The wrong answer, and why it was attractive

The original post on this blog said: the clock has a period of 20, the first positive edge is at t=10, and a becomes 1 at t=25. It is a tidy story and every number in it is wrong.

Start with the clock. It is initialized to 1, so the first toggle at t=10 takes it to 0. That is a negative edge. The first guaranteed positive edge is at t=20, then 40, 60 and so on. Anyone who says "posedge at 10" has not looked at the initial value.

What actually happens at time zero

The subtle part is the very first time step. Both always blocks and the initial block start at t=0, and the LRM does not define the order in which they run within the active region. Two histories are possible:

  • The event control is reached first. The always @(posedge clk) block starts, reaches its event control, and waits. Then the initial block runs and sets clk from x to 1. In Verilog a positive edge is any transition from 0, x or z to 1, so the x to 1 change is a posedge. The block wakes at t=0, prints 0, samples clk, which is now 1, and schedules a <= 1 for t=15.
  • The initial block runs first. clk is already 1 by the time the always block reaches its event control, so the x to 1 edge is missed. The block first wakes at t=20, samples 1, and a becomes 1 at t=35.

Icarus Verilog takes the first path. The trace, with a $monitor added, is:


0
t=0  clk=1 a=0
t=10 clk=0 a=0
t=15 clk=0 a=1
                  20
t=20 clk=1 a=1
t=30 clk=0 a=1
                  40
t=40 clk=1 a=1

Either way, a rises once and stays high forever. The block only ever samples clk at a positive edge, when clk is by definition 1, so the value assigned is always 1. The #15 is an intra-assignment delay: the right-hand side is evaluated at the edge, and the update is scheduled 15 time units later. Nothing about clk changing in between matters, because the sampled value was already captured.

What the interviewer is actually testing

Three separate concepts hide in eleven lines:

  • Edge semantics on 4-state values. x to 1 counts as a positive edge. This is why a reset that starts at x can trigger always @(posedge rst) logic at time zero, and why good testbenches initialize everything in the same time step, or use $urandom seeds and reset sequences that do not depend on initial-value races.
  • Time-zero races. The LRM leaves the order of concurrent processes at the same time step undefined. Two simulators can legally give different answers, which is what the two histories above show. An interview answer that says "it depends on the simulator, here is why, and here is how I would remove the race" is stronger than any single waveform.
  • Intra-assignment delay with nonblocking assignment. a <= #15 clk samples now and updates later. Contrast with #15 a <= clk, which delays the whole statement and samples clk fifteen time units after the edge, at which point clk is 0. The two forms produce opposite waveforms, and being able to say so instantly is the point of the question.

Removing the race

If this were real testbench code, initialize the clock with a declaration assignment or drive it from a single initial block that owns it, and never sample a clock as data. The cleaned-up version has no ambiguity:


module stratified_event_fixed;
  reg clk = 1'b0;        // declaration assignment: no x at time zero
  reg a   = 1'b0;

  always #10 clk = ~clk; // first posedge at t=10, unambiguous

  always @(posedge clk)
    a <= #15 1'b1;       // sample a real signal, not the clock

  initial #200 $finish;
endmodule

Key takeaways

  • Open-drain outputs can only pull low or release. That is what lets many devices share one wire, and why the pull-up resistor is a required part of the design, in silicon and in the testbench model.
  • Wired-AND and wired-OR describe the same shared line under opposite polarity conventions. Verilog models them with wand and wor, or with multiple drivers of 0 and z.
  • A positive edge in Verilog includes transitions from x and z to 1. Initial values decide what the first time step looks like.
  • Processes that start at the same time step run in an undefined order. Any waveform that depends on that order is a race, and a race is a bug.
  • a <= #15 b samples now and updates later. #15 a <= b waits, then samples. Know both.

Verified with Icarus Verilog 13.0. The open-drain example was also linted with Verilator 5.052.

Author
Mayur Kubavat
DV engineer working on SoC verification. Writes here about UVM, PCIe, SystemVerilog, and the everyday craft of getting designs to tape-out.

Comments (0)

Leave a Comment