Bidirectional Delay in SystemVerilog


I recently wrote about detecting driver strength in SystemVerilog. That work actually came out of solving a larger problem: How do you build a bidirectional digital wire model that includes propagation delay?

Motivation

The problem I had was how to model, in a digital simulation, the behavior of a “channel” (wire) for a high speed SerDes link. At the symbol rates used for modern high speed SerDes links, we start running into the physical limitations of copper as a conductor: the attenuation, frequency response, and speed of signals propagating through the channel are all factors that cannot be ignored as they might usually be. An additional requirement that my channel model had, was that it needed to be bidirectional. For my gate models to function, I not only needed to model the propagation delay of signals through the channel, but I needed drive strength resolution to resolve the value of the channel with drivers on both ends.

The protocol for the PHY I was modeling specifies that the transmitter should only be enabled when it detects that there is a receiver connected to it. The way this detection is done is by having the transmitter sense the presence of the termination resistor in the receiver. This is convenient because it lets the transmitter do endpoint detection without any extra wires, but it does complicate the channel modeling, because a lot of SystemVerilog constructs do not resolve drive strengths in both directions.

Basic schematic diagram of system.

Basic schematic diagram of system.

A First Attempt

While constructing a model that could inject bit flip errors and remain bidirectional was a little tricky, its not too difficult with the right set up of tranif gates. What turned out to be the tricky part was modeling propagation delay, because as far as I could find, all SystemVerilog constructs which introduced a delay, also behave as unidirectional drivers. So, how can I make a module where a driver change on either port will appear, after a delay, on the other?

Breaking Down The Problem

An important thing to remember is that in SystemVerilog a net value is really a combination of two components, a value (0, 1, z, or x), and a strength (highz, weak, pull, strong, or supply). It’s easy to to delay the value part, but in SystemVerilog this always loses the strength part. Considering this, I hypothesized that if I could decompose the net value into two separate signals (value and strength), then recompose them on the other side of the delay, I could make a strength preserving delay module. If that works, then I could put two of those circuits back to back to make a truly bidirectional delay module.

The basic idea was to first build a strength maintaining delay element. To do this I needed three pieces: To decompose the net value I needed a strength detector which would take in a net, a output a value proportional to the strength of the net’s driver. To recompose the net value I needed a variable strength driver which would take in two “values” a net value and a net strength, and would drive an output net with the specified strength and value. Finally I would need a controllable delay element so that the delay could be controlled by my testbench. Once I had the unidirectional delay, I could put two of these back to back, as shown in the system schematic below, to create the bidirectional delay I needed.

Schematic showing structure of first bidirectional delay module concept.

Schematic showing structure of first bidirectional delay module concept.

First, I needed a way to convert the strength of a driver on a net to a SystemVerilog value. This turned out to be a deep hole all on it’s own, and I’ve already written about it at length in my last blog post. There are a couple ways of doing this doing this with different trade-offs, but for the purposes of this model I used the Verilog only “sense net” strength detection method because it’s a nice fast standalone implementation. It detects supply strength drivers as strong but that was okay for my use case. The code for that is shown below, for details on how it works, that can be found in my previous post.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
module sense_net_strength_detect (
  inout sig,
  output reg [2:0] strength
);
  
  wire strong_sense;
  wire pull_sense;
  wire weak_sense;
  
  assign (strong1, strong0) strong_sense = (sig === 1'bx) ? 0 : ~sig;
  assign (pull1  , pull0  ) pull_sense   = (sig === 1'bx) ? 0 : ~sig;
  assign (weak1  , weak0  ) weak_sense   = (sig === 1'bx) ? 0 : ~sig;
  
  cmos strong_gate(strong_sense, sig, 1, 0);
  cmos pull_gate  (pull_sense  , sig, 1, 0);
  cmos weak_gate  (weak_sense  , sig, 1, 0);

  assign strength = (
    (sig === 1'bz) ? 0 :
    (sig === 1'bx) ? (
      (pull_sense   !== 1'bx) ? 1 :
      (strong_sense !== 1'bx) ? 2 : 3
    ) : (
      (weak_sense   === 1'bx) ? 1 :
      (pull_sense   === 1'bx) ? 2 : 3
    )
  );
endmodule
sense_net_strength_detect.sv

The next piece I needed was a module that has “value” and “strength” inputs, and will drive an output node high or low with the given strength level. There turns out to be a few ways to do this, but the cleanest approach I found was to use two assign statements for each strength level: One which can only drive a '1 value with the given strength, and one which can only drive a '0 value with the given strength. Then the value of the assign statement is the “value” input ORed or ANDed with a strength value comparison so that only the correct assign statement is active for the given value of the “strength” input. The code for this module looks like the following:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
module multi_strength_driver (
  input       value,
  input [2:0] strength, // highz = 0, weak = 1, pull = 2, strong = 3, supply = 4
  inout sig
);
  assign (supply0, highz1 ) sig = value || (strength != 4);
  assign (highz0,  supply1) sig = value && (strength == 4);
  assign (strong0, highz1 ) sig = value || (strength != 3);
  assign (highz0,  strong1) sig = value && (strength == 3);
  assign (pull0,   highz1 ) sig = value || (strength != 2);
  assign (highz0,  pull1  ) sig = value && (strength == 2);
  assign (weak0,   highz1 ) sig = value || (strength != 1);
  assign (highz0,  weak1  ) sig = value && (strength == 1);
endmodule
multi_strength_driver.sv

Finally, the programmable delays could be simple assign statements with a delay value. Since I’m breaking up the strength and value of the signal, and providing independent data paths in each direction, there is no need to try to preserve drive strength in these assignments or use bidirectional gates.

Putting It All Together (For The First Time)

Now that I had all the pieces, I just needed to stick them together and try it out. For each of the two inout, I created a “sense_strength” signal that was the value representation of that net’s drive strength, and two reg signals to hold the delayed value and strength values for the multi_strength_driver, from the other side of the link.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
module bidir_delay_initial #(
  parameter DELAY_WIDTH=32
) (
  inout sig_a,
  inout sig_b,
  input [DELAY_WIDTH-1:0] delay
);
  wire [1:0] sense_strength_a;
  reg        drive_value_a;
  reg  [1:0] drive_strength_a;
  wire [1:0] sense_strength_b;
  reg        drive_value_b;
  reg  [1:0] drive_strength_b;
  
  sense_net_strength_detect sense_a(.sig(sig_a), .strength(sense_strength_a));
  sense_net_strength_detect sense_b(.sig(sig_b), .strength(sense_strength_b));
  multi_strength_driver drive_a(.sig(sig_a), .strength(drive_strength_a), .value(drive_value_a));
  multi_strength_driver drive_b(.sig(sig_b), .strength(drive_strength_b), .value(drive_value_b));

  initial begin
    drive_value_a    = 1'd0;
    drive_strength_a = 2'd0;
    drive_value_b    = 1'b0;
    drive_strength_b = 2'd0;
  end
  
  always @(sig_b)            drive_value_a    <= #(delay) sig_b;
  always @(sense_strength_b) drive_strength_a <= #(delay) sense_strength_b;
  always @(sig_a)            drive_value_b    <= #(delay) sig_a;
  always @(sense_strength_a) drive_strength_b <= #(delay) sense_strength_a;
endmodule
bidir_delay_initial.sv

To test my solution I created a basic little testbench. On one side (side A) of my delay module I put another instance of my multi-strength driver module, so I could dynamically control the strength of the driver on that side, and to the other (side B), I placed weak strength driver to 0

Simple bidirectional delay test bench.

Simple bidirectional delay test bench.

For initial turn on I used a sequence that would start by driving a strong1, decreasing strength down to highz, and then increase the strength back to strong with a value of 0 on the net a. Between each step I used a 10ns delay, and the delay element was set with a fixed 2ns delay.

Unfortunately, my initial results were not what I was intending, I expected that the b net would be initially high, transition to 1'bx 2ns after the a driver changes strength to weak1, and then transition low 2ns after the a driver transitioned to highz. The a net should have been high until 4ns after the a driver strength goes to 1, when it should to go to a 1'bx. After the a driver goes to a strength of zero it should be pulled low by the pulldown on net b. What the simulation results, as seen below, actually showed was both nets a and b being driven high (with a strong driver) until the driver on side a drove a strong0, when both sides go to strongX.

Simulation waveforms from initial bidirectional delay module showing positive feedback latch behavior.

Simulation waveforms from initial bidirectional delay module showing positive feedback latch behavior.

While not the intended result, an astute observer may have predicted this behavior simply from looking at the initial simplified schematic. By putting two of the same strength preserving delays back to back, I created a positive feedback loop in the net strength. After 2x the delay duration from time 0, the variable strength driver inside the delay module wll be driving the same value and strength as the strongest external driver on either side of the delay. If the external driver changes value or strength the internal driver will continue to drive the old value for 2x the delay duration. Unless the net is externally driven with a larger strength than it was before, the net will continue to hold the old value. This is somewhat akin to the latching behavior you get from a unity gain amplifier with positive feedback.

Breaking the Feedback Loop (a more correct delay)

To actually make this delay element to behave correctly, I needed to break the positive feedback loop caused when one of the variable-strength drivers in the delay element is the strongest driver on it’s net. I found that this is possible by only enabling the driver on port b when the driver on port a is not driving the net, and vice versa. While there isn’t a (native) way to know which driver is driving a net, a close enough approximation is if the detected value and strength of the net matches the value and strength inputs to the variable-strength driver on that net. If the strength and value match, then the driver on the opposite port is disabled, which breaks the feedback loop.

Schematic showing structure of bidirectional delay module with broken feedback loop.

Schematic showing structure of bidirectional delay module with broken feedback loop.

While this is actually a conceptually simple way to break up the feedback loop, the trick turned out to be expressing it in SystemVerilog such that it wouldn’t cause zero-time loops in the simulator when resolving the bidirectional net values. After a decent amount of experimentation, the solution turned out to be correct partitioning of always blocks and some well placed #0 delays to force net resolution order. Note that there has to be some additional special handling for 1'bx values, because they can be the result of multiple drivers on one net. The code for my final bidirectional delay module is shown below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
module bidir_delay #(
  parameter DELAY_WIDTH=32
) (
  inout sig_a,
  inout sig_b,
  input [DELAY_WIDTH-1:0] delay
);
  wire [1:0] sense_strength_a, sense_strength_b;
  reg  [1:0] drive_strength_a, drive_strength_b;;
  reg        sense_value_a, sense_value_a;
  reg        drive_value_a, drive_value_a;
  reg        driving_a, driving_b;
  
  sense_net_strength_detect sense_a(.sig(sig_a), .strength(sense_strength_a));
  sense_net_strength_detect sense_b(.sig(sig_b), .strength(sense_strength_b));
  multi_strength_driver drive_a(.sig(sig_a), .strength(drive_strength_a), .value(drive_value_a));
  multi_strength_driver drive_b(.sig(sig_b), .strength(drive_strength_b), .value(drive_value_b));
  
  initial begin
    drive_value_a    = 1'd0;
    drive_strength_a = 2'd0;
    drive_value_b    = 1'b0;
    drive_strength_b = 2'd0;
    driving_a        = 1'd0;
    driving_b        = 1'd0;
  end
  
  always @(driving_a, sig_a) if (!driving_a) drive_value_a_delay <= #(delay) sig_a;
  always @(driving_b, sig_b) if (!driving_b) drive_value_a_delay <= #(delay) sig_b;
  always @(sense_value_a_delay) drive_value_b <= (sense_value_a_delay === 1'bx) ? ~sig_b : sense_value_a_delay;
  always @(sense_value_b_delay) drive_value_a <= (sense_value_b_delay === 1'bx) ? ~sig_a : sense_value_b_delay;
  always @(driving_a, sense_strength_a) if (!driving_a) drive_strength_b <= #(delay) sense_strength_a;
  always @(driving_b, sense_strength_b) if (!driving_b) drive_strength_a <= #(delay) sense_strength_b;
  always @(posedge driving_a) drive_strength_b <= #(delay) 2'b0;
  always @(posedge driving_b) drive_strength_a <= #(delay) 2'b0;
  always @(sig_a, drive_value_a, drive_strength_a, sense_strength_a) begin
    #0; // Evaluate this after all the inputs have settled to prevent extra delta cycles.
    driving_a <= (sig_a === drive_value_a) && (drive_value_a !== 1'bx) && (sense_strength_a == drive_strength_a);
  end

  always @(sig_b, drive_value_b, drive_strength_b, sense_strength_b) begin
    #0; // Evaluate this after all the inputs have settled to prevent extra delta cycles.
    driving_b <= (sig_b === drive_value_b) && (drive_value_b !== 1'bx) && (sense_strength_b == drive_strength_b);
  end
endmodule
bidir_delay.sv

Using the same testbench as before, simulating this modified delay module resulted in the waves shown below. With the positive feedback loop broken, we now get the expected behavior on the bidriectional nets. The a node gets pulled down by the pulldown on net bafter the 2x delay period, instead of latching the strong value, as the old implementation did.

Simulation results of bidirectional delay module showing expected behavior, after breaking feedback loop.

Simulation results of bidirectional delay module showing expected behavior, after breaking feedback loop.

Remaining Issues

The most significant remaining issue with this design is that if the delay port is set to 0, it can cause an infinite zero-time loop in the simulator. For my use case, the I was able to externally ensure that the delay input never was set to 0, but if this was not possible, one could also add tranif gates to disconnect and bypass the drivers when the delay input was set to zero. The other significant issue is that it cannot handle supply strength net values, mainly because of the approach used to sense the net drive strength. If this was a significant issue, the DPI drive strength detection implementation from my previous blog post could be used in place of the sense-net one I used.

Conclusion

While there are still some corner cases with this bidirectional delay implementation that don’t quite match with reality, it has proven to be fairly solid in production. The fact that it was possible to implement in pure SystemVerilog is both convenient, and means that it should be portable to any simulator. In the end, this project gave me the opportunity to really dig into the details of how net resolution, strength modeling, as well as gate primetives actually work in SystemVerilog.

If you would like to play around with my bidirectional delay module, you can find an implementation on EDA Playgound here: https://www.edaplayground.com/x/2PdE