The previous article showed how a # delay pauses a procedural block. A delay can be placed in two different positions in an assignment: on the left of the statement, before the whole assignment, or on the right of the assignment operator, before the expression. The two positions look similar, but they change when the right-hand side is evaluated, which can give a completely different result.

10 min read | Beginner Level

What You'll Learn

  • The difference between inter-assignment and intra-assignment delays
  • Why an intra-assignment delay can miss values updated by nonblocking assignments
  • How blocking and nonblocking intra-assignment delays affect the rest of the block
  • How to use intra-assignment delays to model wire delays without losing short pulses
FormExampleRHS evaluatedLHS updated
Inter-assignment#5 q = a & b;After the delayAfter the delay
Intra-assignmentq = #5 a & b;ImmediatelyAfter the delay

Inter-assignment Delays

An inter-assignment delay is written on the left side of the statement. It delays the execution of the whole statement: the block waits first, and then evaluates the right-hand side and assigns the result, as if the delay were a separate statement. This is the most common way to use a delay.


// Delay is specified on the left side
#<delay> <LHS> = <RHS>

In the example below, a and c are set to 1 at T=5 with nonblocking assignments, and q is assigned 5 time units later:


module tb;
  reg  a, b, c, q;

  initial begin
    $monitor("[%0t] a=%0b b=%0b c=%0b q=%0b", $time, a, b, c, q);

    // Initialize all signals to 0 at time 0
    a <= 0;
    b <= 0;
    c <= 0;
    q <= 0;

    // Inter-assignment delay: Wait for #5 time units
    // and then assign a and c to 1. Note that 'a' and 'c'
    // gets updated at the end of current timestep
    #5  a <= 1;
        c <= 1;

    // Inter-assignment delay: Wait for #5 time units
    // and then assign 'q' with whatever value RHS gets
    // evaluated to
    #5 q <= a & b | c;

    #20;
  end

endmodule
Output
xcelium> run
[0] a=0 b=0 c=0 q=0
[5] a=1 b=0 c=1 q=0
[10] a=1 b=0 c=1 q=1
xmsim: *W,RNQUIE: Simulation is complete.

q becomes 1 at T=10. The right-hand side is evaluated at T=10, after the delay, and by then a and c have long been updated to 1, so a & b | c gives 1 & 0 | 1 = 1.

Synthesis result: Synthesis ignores the delay, so #5 q <= a & b | c; becomes the same AND-OR logic as q <= a & b | c;. Inter-assignment delays are used in testbenches to space out stimulus.

Intra-assignment Delays

An intra-assignment delay is written on the right side of the assignment operator. The right-hand side is evaluated immediately, the result is held, and it is assigned to the left-hand side only after the delay:


// Delay is specified on the right side
<LHS> = #<delay> <RHS>

A blocking intra-assignment delay behaves like this pair of statements, with a hidden temporary variable:


q = #5 a & b | c;

// is equivalent to:
temp = a & b | c;    // Evaluate now
#5 q = temp;         // Assign later

Here is the same testbench with the delay moved to the right side of the q assignment:


module tb;
  reg  a, b, c, q;

  initial begin
    $monitor("[%0t] a=%0b b=%0b c=%0b q=%0b", $time, a, b, c, q);

    // Initialize all signals to 0 at time 0
    a <= 0;
    b <= 0;
    c <= 0;
    q <= 0;

    // Inter-assignment delay: Wait for #5 time units
    // and then assign a and c to 1. Note that 'a' and 'c'
    // gets updated at the end of current timestep
    #5  a <= 1;
        c <= 1;

    // Intra-assignment delay: First execute the statement
    // then wait for 5 time units and then assign the evaluated
    // value to q
    q <= #5 a & b | c;

    #20;
  end
endmodule
Output
xcelium> run
[0] a=0 b=0 c=0 q=0
[5] a=1 b=0 c=1 q=0
xmsim: *W,RNQUIE: Simulation is complete.

There is no line for T=10, so q never became 1. The reason is in the order of events at T=5:

  1. a <= 1 and c <= 1 are nonblocking, so their updates are scheduled for the end of the T=5 time step.
  2. The block moves straight on to q <= #5 a & b | c and evaluates the right-hand side at once. At this point a and c still hold their old value 0, so the result is 0.
  3. At the end of the time step, a and c become 1, which is what the [5] line shows.
  4. At T=10, the saved value 0 is assigned to q. q was already 0, so nothing changes and $monitor prints nothing.

If a and c are assigned with blocking assignments instead, they are updated immediately, before the q statement evaluates its right-hand side:


  ...

  // Non-blocking changed to blocking and rest of the
  // code remains the same
  #5  a = 1;
      c = 1;

  q <= #5 a & b | c;

  ...
Output
xcelium> run
[0] a=0 b=0 c=0 q=0
[5] a=1 b=0 c=1 q=0
[10] a=1 b=0 c=1 q=1
xmsim: *W,RNQUIE: Simulation is complete.

Now the right-hand side sees a = 1 and c = 1 at T=5, evaluates to 1, and q becomes 1 at T=10. The diagram compares the three cases:

Timeline of the three examples: a and c change to 1 at the end of the T=5 time step; with #5 q <= a & b | c the right-hand side is evaluated at T=10, sees a=1 and c=1, and q becomes 1 at T=10; with q <= #5 a & b | c the right-hand side is evaluated at T=5, sees the old values a=0 and c=0, and q is assigned 0 at T=10; with blocking assignments to a and c, the right-hand side is evaluated at T=5, sees a=1 and c=1, and q becomes 1 at T=10

Synthesis result: Intra-assignment delays are also ignored by synthesis. q <= #5 a & b | c; synthesizes to the same logic as q <= a & b | c;, which is why a design that depends on the timing of a delay can behave differently after synthesis.

Quick Check

In the intra-assignment example, if q <= #5 a & b | c; were changed to #0 q <= #5 a & b | c;, would q become 1 at T=10?

Show Answer

No. A #0 delay only moves the statement to the end of the active events in the same time step, but nonblocking updates happen even later, after all active and #0 events. The right-hand side would still see a = 0 and c = 0. To see the new values, wait at least one time unit, or use blocking assignments for a and c.

Blocking vs Nonblocking Intra-assignment Delays

An intra-assignment delay can be used with both kinds of assignment, and they affect the rest of the block very differently:


initial begin
  q1 = #5 d;     // Blocking: the block waits here for 5 units
  x  = 1;        // Runs at T=5
end

initial begin
  q2 <= #5 d;    // Nonblocking: the update is scheduled for T=5
  y  = 1;        // Runs at T=0, without waiting
end
StatementRHS evaluatedLHS updatedDoes the block wait?
#5 q = d;T+5T+5Yes, 5 units
q = #5 d;TT+5Yes, 5 units
#5 q <= d;T+5End of time step T+5Yes, 5 units
q <= #5 d;TEnd of time step T+5No

The last row is special: q <= #5 d evaluates d, schedules the update for later, and the block continues at once. Several such statements in a row can schedule several future updates, even of the same variable, and each one keeps its own value.

Quick Check

What values does q take, and when, for this block?


initial begin
  q <= #10 1;
  q <= #20 0;
  q <= #30 1;
end
Show Answer

q is x until T=10, then 1 at T=10, 0 at T=20 and 1 at T=30. All three statements execute at T=0 without waiting, and each schedules its own update. This is a compact way to write a waveform in a testbench.

Modeling Wire Delays

A common use of intra-assignment delays is to model the delay of a wire or a buffer in a testbench, so that a signal arrives at its destination some time after it leaves its source. The way the delay is written decides whether short pulses survive:


// Nonblocking intra-assignment delay: every change is
// scheduled 5 units later, so pulses are kept
always @(in) out1 <= #5 in;

// Delay before a blocking assignment: the block is busy
// for 5 units and misses any change that happens meanwhile
always @(in) #5 out2 = in;

Consider an input that pulses high from T=10 to T=13, and again from T=30 to T=40:

Waveforms for an input with a 3 unit pulse at T=10 and a 10 unit pulse at T=30: out1, using out1 <= #5 in, shows both pulses delayed by 5, at T=15 to 18 and T=35 to 45; out2, using #5 out2 = in, misses the short pulse because the block is busy in its #5 delay when in falls at T=13, and only shows the second pulse from T=35 to 45
  • out1 copies both pulses exactly, 5 units later. Each change of in schedules its own update, so even a pulse shorter than the delay passes through. This is called a transport delay.
  • out2 misses the first pulse. The block wakes up at T=10, then waits until T=15 and copies in, which by then is 0 again. The fall at T=13 happened while the block was waiting, so nothing triggered the block for it.

Board-Level Timing in Testbenches

When a chip talks to an external memory or another chip on a circuit board, the signals take time to travel along the board traces. Testbenches that check such interfaces often model each trace as always @(sig_out) sig_in <= #trace_delay sig_out;. Because this is a transport delay, every edge arrives at the other side, so the test checks the real timing margins of the interface instead of hiding fast toggles.

Unit Delays in RTL Flip-Flops

Some design teams write sequential RTL as always @(posedge clk) q <= #1 d;. Synthesis ignores the #1, but in simulation every flip-flop output now changes slightly after the clock edge. Waveforms become easier to read, and it is obvious which value was sampled at each edge. Other teams avoid it, because it can hide real race conditions and slows down simulation, so check your project's coding guidelines.

Event-Based Intra-assignment Control

The control on the right side of an assignment does not have to be a delay. It can also be an event, which evaluates the right-hand side now and assigns it when the event happens:


// Sample d now, assign it to q at the next rising clock edge
q = @(posedge clk) d;

// Sample d now, assign it to q after three rising clock edges
q = repeat (3) @(posedge clk) d;

This is mostly used in testbenches and behavioral models, for example to capture a value at one moment and apply it a fixed number of clock cycles later. Like delays, these event controls are not synthesizable in this form.

Common Beginner Mistakes

Mistake #1: Expecting an intra-assignment delay to see nonblocking updates from the same time step

Wrong:


a <= 1;
q <= #5 a;     // Expecting q to become 1 at T+5

Right: Use #5 q <= a; if q should get the value of a at T+5, or use a blocking assignment a = 1; if a must change first

Why? The intra-assignment delay evaluates a immediately, before the nonblocking update to a has happened. q gets the old value of a, exactly as in the second example of this article.

Mistake #2: Not realizing that a blocking intra-assignment delay stops the block

Wrong: q1 = #10 d1; q2 = #10 d2;, expecting both outputs to change at T=10
Right: q1 <= #10 d1; q2 <= #10 d2;

Why? A blocking intra-assignment delay waits before the next statement. q1 changes at T=10, and only then is d2 evaluated, so q2 changes at T=20. The nonblocking version schedules both updates at T=0 and both happen at T=10.

Mistake #3: Modeling a wire delay with a delay before the assignment

Wrong: always @(in) #5 out = in;
Right: always @(in) out <= #5 in;

Why? While the block waits in #5, it is not watching in, so changes during the delay are missed and short pulses disappear. The nonblocking intra-assignment delay schedules every change independently, as shown in the diagram above.

Mistake #4: Relying on delays for correct RTL behavior

Wrong: Adding #1 or #2 to assignments until a race in the RTL simulation goes away
Right: Fix the race with correct coding: nonblocking assignments in clocked blocks, blocking assignments in combinational blocks

Why? Synthesis removes every delay, so the netlist still has the race the delay was hiding. The RTL simulation passes, but the gate-level simulation or the chip fails.

Practice Exercise

Challenge: Predict the Output

Predict every line printed by $monitor for the code below, then check your answer in a simulator.


module tb;
  reg [3:0] a, x, y, z;

  initial begin
    $monitor("[%0t] a=%0d x=%0d y=%0d z=%0d", $time, a, x, y, z);
    a = 1; x = 0; y = 0; z = 0;

    x <= #4 a;      // Statement 1
    #2 a = 2;
    y = #4 a;       // Statement 2
    #2 z <= a;      // Statement 3
    a = 3;
  end
endmodule
Hint

For each statement, write down the time at which the right-hand side is evaluated, the value of a at that moment, and the time at which the left-hand side is updated. Remember that a blocking intra-assignment delay makes the block wait.

Solution
StatementRHS evaluatedValue of aLHS updated
x <= #4 a;T=01x = 1 at T=4
y = #4 a;T=22y = 2 at T=6 (block waits until T=6)
#2 z <= a;T=82z = 2 at T=8
a = 3;Runs at T=8, right after statement 3

Expected output:

[0] a=1 x=0 y=0 z=0
[2] a=2 x=0 y=0 z=0
[4] a=2 x=1 y=0 z=0
[6] a=2 x=1 y=2 z=0
[8] a=3 x=1 y=2 z=2

Statement 3 evaluates a at T=8 before a = 3 runs, so z gets 2. Note that x gets 1, not 2: its value was sampled at T=0, even though a changed before x was updated.

Summary

In this tutorial, you learned:

  • An inter-assignment delay (#5 q = d;) waits first, then evaluates the right-hand side and assigns it
  • An intra-assignment delay (q = #5 d;) evaluates the right-hand side immediately and assigns the saved value after the delay
  • An intra-assignment delay right after nonblocking assignments sees the old values, because nonblocking updates happen at the end of the time step
  • A blocking intra-assignment delay stops the block, while a nonblocking one schedules the update and continues at once
  • out <= #d in models a transport delay that keeps short pulses, while #d out = in in an always @(in) block misses them
  • The right-hand control can also be an event, such as q = @(posedge clk) d;
  • Synthesis ignores all of these delays, so correct RTL must never depend on them
Did you find what you were looking for?

Quiz

Test Your Knowledge

1 of 5 Score: 0 / 5