Is it necessary to take into account the actual state of a combinational circuit's signals to calculate its maximum delay?

I'm doing an exercise in which I need to calculate the maximum delay of a 1-bit full adder. In a full adder, the slowest path is the carry_out. Here is how I have designed it:

Let's suppose all gates have a delay of 50ps. Then the slowest path would be the one that goes from c_in to c_out through those two AND gates. Thus, 4 * 50 = 200ps should be the maximum delay.

However, I have implemented this circuit in Verilog, and I find that it is not that slow. Here is the implementation and the test bench:

timescale 10ps/1ps

input wire a,
input wire b,
input wire cin,
output wire o,
output wire cout
);
//
// cout
//
wire ncin, net0, net1, net2, net3;

not #(5) n0(ncin, cin);
and #(5) a0(net0, ncin, b);
and #(5) a1(net1, a, net0);

or  #(5) o0(net2, a, b);
and #(5) a2(net3, net2, cin);

or  #(5) o1(cout, net3, net1);

//
// o
//
wire net4;
xor #(5) x0(net4, a, b);
xor #(5) x1(o, net4, cin);
endmodule


Test bench

timescale 10ps/1ps

define WAIT_DELAY 16

reg a = 0;
reg b = 0;
reg cin = 0;

wire o;
wire cout;

full_adder fa(a, b, cin, o, cout);

initial begin
$dumpfile("full_adder_tb.vcd");$dumpvars(1, full_adder_tb);
cin <= 0; a <= 0; b <= 0; #WAIT_DELAY;
$display("%d;%d", o, cout); cin <= 0; a <= 0; b <= 1; #WAIT_DELAY;$display("%d;%d", o, cout);
cin <= 0; a <= 1; b <= 0; #WAIT_DELAY;
$display("%d;%d", o, cout); cin <= 0; a <= 1; b <= 1; #WAIT_DELAY;$display("%d;%d", o, cout);
cin <= 1; a <= 0; b <= 0; #WAIT_DELAY;
$display("%d;%d", o, cout); cin <= 1; a <= 0; b <= 1; #WAIT_DELAY;$display("%d;%d", o, cout);
cin <= 1; a <= 1; b <= 0; #WAIT_DELAY;
$display("%d;%d", o, cout); cin <= 1; a <= 1; b <= 1; #WAIT_DELAY;$display("%d;%d", o, cout);
$finish; end endmodule  After the test runs, I double check that the output values match the expected with a Python script. I was expecting WAIT_DELAY to be required to be 20 (200ps) for the test to pass; however, 16 (160ps) is enough. My test hits the error when the inputs {cin, a, b} transit from {0, 0, 1} to {0, 1, 0}. My understanding is that this happens because the actual worst case is not the sum of the gates mentioned above, because depending on what was the previous state, and what is the current one, the gates might need less time to get stabilized because some signals will be 'don't care'. For instance, if {cin, a} = {1, 1}, cout will be always 1 even if {b} starts oscillating every 1ps, because the last or gate will be like 1 + X = 1 (where X is an invalid state). In other words, it might happen that the actual critical path is not the one made of 4 gates, because there is no combination of inputs that make all of those gate's outputs to change. Questions • Am I right in the above reasoning? • If so, is this valid out of the simulator, in real life, or can the X input of the above example's or gate 'pertubate' its output? • I imagine it will be better to use the worst case in terms of the sum of all the critical path's gate's delays because it is easier, safer and less error prone. Is this correct? • I'm curious if your adder was optimized. For example, a schematic using only 2-in logic might look like this. And that would more closely match your simulated results. – jonk Apr 2, 2022 at 18:15 • I'm using Icarus Verilog as simulator. I don't know if it optimizes the design, or how to disable optimizations Apr 2, 2022 at 18:54 1 Answer Yes, it is necessary to take into account the actual state of a combinational circuit's signals to calculate its maximum delay. The problem with your testbench is that it is too simple. While it accounts for all 8 combinations of the 3 inputs, it does not account for all possible transitions between input sets. Since it's hard to think up all possible combinations, we let the simulator do that for us. The customary way is to randomize the inputs and run it for a large number of combinations. The compact way to do that is to use a repeat loop. Since we've now made the input more complicated, it will make it more difficult to check the results with your Python script. But, this is simple enough to check in Verilog in the testbench. define WAIT_DELAY 22 module full_adder_tb; reg a = 0; reg b = 0; reg cin = 0; wire o; wire cout; event chk; full_adder fa(a, b, cin, o, cout); initial begin$dumpfile("full_adder_tb.vcd");
$dumpvars(1, full_adder_tb); repeat (100) begin {cin, a, b} =$random;
#WAIT_DELAY;
end

$finish; end initial begin : check reg c_exp, o_exp; #(WAIT_DELAY - 1); // Check outputs 10ps before next input change forever begin ->chk; {c_exp, o_exp} = a + b + cin; if ({cout, o} !== {c_exp, o_exp}) begin$display("%t error {cout, o}=%b {c_exp, o_exp}=%b", \$time, {cout, o}, {c_exp, o_exp});
end
#(WAIT_DELAY);
end
end
endmodule


This self-checking testbench passes when WAIT_DELAY is 22 or more, but it will fail if you set it to 20 or less.

The check occurs 10ps before the next input change. For example, when the delay is 220ps, the check occurs 210ps after the inputs changed. I added the chk event so you can easily see when the check occurs in waves.

When the delay is 170ps, for example, you should see errors something like (depending on randomization):

            5260 error {cout, o}=00 {c_exp, o_exp}=10
6450 error {cout, o}=00 {c_exp, o_exp}=10
6790 error {cout, o}=00 {c_exp, o_exp}=10
7980 error {cout, o}=00 {c_exp, o_exp}=10
`