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.
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 edge | Budget | Expected result |
|---|---|---|
| 1 | 3 | ok on edge 1 |
| 3 | 3 | ok on edge 3, the deadline |
| 4 | 3 | timeout after 3 edges |
| never | 3 | timeout after 3 edges |
| 1 | 0 | timeout 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
Wait for the next rising edge
@(posedge clk). The first pass waits for edge 1, so the request edge is never sampled.Look at the response
If
rsp_validis 1, report success with the number of this edge and return.edge 3, rsp_valid = 1 ok on edge 3Count the edge
Only after looking, decide whether the budget allows another edge. If it does, go back to step 1.
Give up
After N edges without a response, report a timeout after N edges.
N = 3, no response timeout 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.
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 edgesCheck yourself
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
- Single-response deadline Implement · Easy
- Inclusive boundary checker Implement · Medium
- Catch the late responders Verify · Hard