Problem: a bounded response wait

Specify and build a task that waits a fixed number of clock edges for a response, accepts one on the last allowed edge, and leaves no process behind.

Lesson
3.7
Time
25 min
Runs on
Verilator
Check
5 questions

A read checker reports TIMEOUT for a read that the DUT answered on time. The spec allows the response up to 3 clock edges after the request, and the DUT answered on edge 3. The checker counted that edge, saw that its budget was used up, and gave up before it looked at rsp_valid.

A response on the deadline edge, reported as a timeout
01234reqrsp_validedges counted0123checkerstartwaitTIMEOUT-edge 3: response present

One column per rising edge; the request is on edge 0. The checker tested its budget before the response.

The rule in one sentence

Write the rule down before any code: after the request edge, the response is on time if rsp_valid is 1 on any of the next N rising edges, edge N included. N is the budget.

Each part of that sentence settles a question that the buggy checker got wrong or left open. Counting starts after the request edge, so a stale rsp_valid on edge 0 does not count. Edge N itself is allowed, so the check on edge N must happen before the task gives up.

Response on edgeBudgetExpected result
13ok on edge 1
33ok on edge 3, the deadline
43timeout after 3 edges
never3timeout after 3 edges
10timeout at once: a budget of 0 allows no edges

The cases the rule decides. Each one catches a different broken wait.

Look first, then give up

  1. Wait for the next rising edge

    @(posedge clk). The first pass waits for edge 1, so the request edge is never sampled.

  2. Look at the response

    If rsp_valid is 1, report success with the number of this edge and return.

    edge 3, rsp_valid = 1ok on edge 3
  3. Count the edge

    Only after looking, decide whether the budget allows another edge. If it does, go back to step 1.

  4. Give up

    After N edges without a response, report a timeout after N edges.

    N = 3, no responsetimeout after 3 edges

The order of steps 2 and 3 is the whole bug from the opening story. Swap them and the response on edge N is never examined, so the task times out one edge early.

A for loop states the rule compactly: for (edges = 1; edges <= budget; edges++). The loop body waits for an edge and looks, and the <= makes edge N part of the budget. A budget of 0 runs the body zero times, which is the answer the table asks for.

Where the response is sampled

The task reads rsp_valid right after the rising edge. Suppose the DUT drives rsp_valid with a nonblocking assignment on that same edge. The new value lands later in the time step, so the task reads the value from before the edge. That is the value a flip-flop would capture, and it does not depend on process order.

The response model in the example changes rsp_valid only on falling edges, so the value is stable at every rising edge. In a full testbench, a clocking block (Module 6) gives the same guarantee for any DUT signal.

The fork version and its traps

A common first draft races the response against a timer with fork ... join_any. It can be made to work, but it needs every lesson from 3.3.

Timer race

fork begin
  fork
    do @(posedge clk); while (!rsp_valid);
    repeat (budget) @(posedge clk);
  join_any
  disable fork;
end join
ok = rsp_valid;   // re-check: edge N is a tie

Works, but only with the wrapper around disable fork and the re-check after it, because a response on edge N ties with the timer.

Counting loop

ok = 0;
for (edges = 1; edges <= budget; edges++) begin
  @(posedge clk);
  if (rsp_valid) begin ok = 1; return; end
end
edges = budget;

One process: nothing to stop and no tie. The deadline is visible in the <=.

The tie is the deeper problem. On edge N, the timer branch finishes in the same time step in which the response is sampled, and the standard does not say which branch runs first. The loop has a single process, so it never has to decide.

Test every edge of the rule

Run each row of the table as its own case, and write the expected result from the rule before running the task. Expected values copied from the task's own output only prove that the task agrees with itself.

Each case starts on a fresh request edge, and a late response must be finished before the next case starts, or it leaks into it. The example does this with wait fork at the end of each case, since the response model is the only child of the test process.

A watchdog for a stopped clock

Counting edges makes the wait independent of the clock period, but it depends on the clock running. If a bug stops the clock, @(posedge clk) never returns, and the bounded wait waits forever.

Keep one time-based watchdog at the top of the test, for example a process that waits for 1 ms of simulation time and then calls $fatal. The bounded wait handles a slow DUT, and the watchdog handles a dead clock.

Try it

Every row of the table comes out as the rule says, including the response on the deadline edge and the zero budget.

Now break the task on purpose: change <= to < and predict which rows change before you run it. Then restore it, and move the if (rsp_valid) test above the @(posedge clk) to see what sampling the request edge does.

RUNVerilator · simulator service
Edit the code, then run it
Expected: budget 3, response on edge 1: ok on edge 1 budget 3, response on edge 3: ok on edge 3 budget 3, response on edge 4: timeout after 3 edges budget 3, response never: timeout after 3 edges budget 0, response on edge 1: timeout after 0 edges

Check yourself

Predict the outputThis checker tests its budget before it looks at the response. rsp_valid rises at t=22, before the third rising edge at t=25. Predict the exact output.
module tb;
  bit clk, rsp_valid, ok;
  int edges;
  always #5 clk = ~clk;          // rising edges at t=5, 15, 25, ...
  initial #22 rsp_valid = 1;
  initial begin
    forever begin
      @(posedge clk);
      edges++;
      if (edges >= 3) break;     // budget of 3 edges
      if (rsp_valid) begin ok = 1; break; end
    end
    $display("ok=%0d edges=%0d", ok, edges);
    $finish;
  end
endmodule
Spot the bugEach option is the body of task automatic bounded_wait(input int budget, output bit ok, output int edges). It must follow the rule from this lesson for every case in the table. Which option breaks the rule?
ConceptA timer-race version of the wait uses fork ... join_any. One branch waits for a rising edge on which rsp_valid is 1, and the other counts budget rising edges. The response arrives on edge N. Which branch finishes first?
ConceptWhy does a test that uses this edge-counting wait still need a time-based watchdog?
ConceptSuppose bounded_wait were rewritten as a timer race ending in a bare disable fork, with no wrapper. In the example test, what else would it stop?

Remember

  • Write the rule first: the response is on time on any of the N edges after the request, edge N included.
  • On each edge, look at the response before deciding whether the budget is used up.
  • A single counting loop has no losing branch to stop and no same-time-step tie to resolve.
  • Test the first edge, the deadline edge, one edge late, no response and a zero budget, with expected values written in advance.
  • An edge-counting wait still needs a time-based watchdog in case the clock stops.

Practice