From the spec to a check
Read a spec the way its authors drew it, turn one sentence into one property, know when a pass means nothing, and put the checker where every instance of the bus will meet it.
- Lesson
- P1.10
- Time
- 30 min
- Runs on
- Verilator
- Check
- 5 questions
Two teams argued for a week about a memory controller that raised ready, saw no offer, and dropped ready again two cycles later. The DMA team's checker flagged every such pulse as a withdrawn offer, and the controller team called the checker wrong. Both teams had read the spec, and only one of them had read the sentence that settled it.
A lit ready with a dark valid is permission with nothing to permit.
- Cycle 1
The controller raises
ready: it has room. Nothing is offered. - Cycle 2
readydrops again. The DMA checker called this a withdrawn offer. But onlyvalidmakes an offer, andvalidwas 0. - Cycle 4
The real offer from cycle 3 is taken here. The spec: a sink may lower
readybeforevalidrises. Argument over.
The sentence "a destination may deassert ready before valid is asserted" ended the week. A checker that flags a permitted event is a bug in the checker.
Reading a timing diagram the way its author drew it
A spec draws rules as pictures before it states them as sentences, and the pictures use a small shared vocabulary. A single line at two levels is a one-bit signal. A hexagon with a value inside is a bus holding that value. A hatched region is a bus whose value does not matter, or is not yet known. Every vertical line is a clock edge, and the values are the ones sampled at that edge.
Hatching means the value does not matter. The bus is only required to hold a1 while valid is 1.
Arrows between signals carry meaning too. In the AMBA handshake pictures a single-headed arrow says the signal at its tip may come before or after the one at its tail. A double-headed arrow says it must come after. A single-headed arrow from valid to ready, with nothing pointing back, is the deadlock rule of P1.3 drawn in one stroke.
Must, should and may
Four words carry the force of every sentence. Must (or shall) is a rule, and a checker reports every violation. Must not is a rule too. Should (or recommended) is advice: a checker may warn, never fail. May (or permitted) is a freedom, and a checker that flags it is wrong.
| Word | What it means | What a checker does |
|---|---|---|
| must, shall, must not | A rule. Every implementation obeys it. | Reports each violation with the edge |
| should, recommended | Advice. A good implementation follows it. | Warns, or counts how often it is followed |
| may, permitted, can | A freedom. Any choice is correct. | Nothing. Flagging it is a checker bug |
| must, with a number | A rule with a bound: within 16 cycles, at most 4 outstanding | Reports the bound and the edge it was crossed |
The hook's sentence was a may. The DMA checker treated it as a must not.
One sentence, one property
A property is a sentence with its edges named. Find the condition the sentence talks about: that is the antecedent. Find what must then be true, and at which edge: that is the consequent. The same edge gives |->, the next edge gives |=>, and n edges later gives ##n.
- Draw
Draw the sentence: an offer, two stalled edges, the transfer. The picture shows which edges the sentence relates.
Follow the calls, values and waits
- data hold rule: The sentence → The picture
- Name the edges
The antecedent is
valid && !readyat one edge. The consequent isvalidat the next edge, so the operator is|=>.Follow the calls, values and waits
- data hold rule: The picture → The property
- Collect
The property joins the others in
link_props, a module whose ports are the link's wires and nothing else.Follow the calls, values and waits
- data hold rule: The property → link_props
- Bind
One
bindline puts a copy oflink_propsinside everylink_sink, in the bench and in the system, without touching the sink.Follow the calls, values and waits
- data hold rule: link_props → Every link_sink
Read the diagram as text
- The sentence: Once valid is 1 and ready is 0, valid stays 1 until the transfer
- The picture: A stalled offer, drawn edge by edge
- The property: valid && !ready |=> valid
- link_props: Every property of the link in one module
- Every link_sink: bind puts a copy of link_props inside each one
- The sentence → The picture: draw it
- The picture → The property: name the edges
- The property → link_props: collect
- link_props → Every link_sink: bind
Keep the sentence as a comment above its property. The next reader argues with the spec, never with you.
Sentence 2 of the link, "while an offer waits, data does not change", has the same antecedent and a consequent about the bus: valid && !ready |=> $stable(data). Measured on a link that holds one offer and spoils the next:
valid && !ready |=> $stable(data).- Edge 3 · t=25
a1 is offered and stalled at 15 and 25. Each stalled edge starts an attempt; both find a1 still there.
- Edge 7 · t=65
b2 is stalled at 55, and at 65 the bus shows b3. The attempt from 55 fails: the sentence said the data holds.
- Edge 8 · t=75
The attempt from 65 passes: b3 held until the transfer. A property decides each stalled edge on its own.
Read the lanes as text
- stalled @15, from edge 2 (t=15): passes at edge 3 (t=25).
- stalled @25, from edge 3 (t=25): passes at edge 4 (t=35).
- stalled @55, from edge 6 (t=55): fails at edge 7 (t=65).
- stalled @65, from edge 7 (t=65): passes at edge 8 (t=75).
Sampled values, edge 1 to 9: valid 0 1 1 1 0 1 1 1 0; ready 0 0 0 1 0 0 0 1 0; data 00 a1 a1 a1 00 b2 b3 b3 b3.
The program behind the lanes
The lanes come from running this program on Verilator 5.052. Each lane is a separate copy of stable_data that can start only at that lane's edge, so every verdict and its edge are read from that copy's own output.
module tb;
logic clk = 0;
always #5 clk = ~clk; // rising edges at 5, 15, 25, ...
logic valid = 0, ready = 0;
logic [7:0] data = 8'h00;
stable_data: assert property (@(posedge clk) valid && !ready |=> $stable(data));
initial begin
@(negedge clk); valid = 1; data = 8'ha1; // offer sampled at 15, stalled
@(negedge clk); // still stalled at 25, data held
@(negedge clk); ready = 1; // taken at 35
@(negedge clk); valid = 0; ready = 0; data = 8'h00;
@(negedge clk); valid = 1; data = 8'hb2; // second offer, sampled at 55, stalled
@(negedge clk); data = 8'hb3; // changed while waiting: seen at 65
@(negedge clk); ready = 1; // taken at 75
@(negedge clk); valid = 0; ready = 0;
@(negedge clk);
$finish;
end
endmodule
Four stalled edges, four attempts, one failure, at the exact edge where the bus changed.
A pass that checked nothing
A property only works when its antecedent happens. On a link whose sink is always ready, no offer ever stalls, so the hold property never looks at anything. Every attempt ends at once with a vacuous pass: no failure, no check, and a green line in the report.
- Edge 2 · t=15
Beat one is taken at 15 with
readyalready high.valid && !readyis false, so the attempt ends as it starts. - Edge 5 · t=45
Five edges, five open circles. The hold rule was never exercised, and the report says pass.
Read the lanes as text
- edge @5, from edge 1 (t=5): succeeds vacuously at edge 1 (t=5).
- edge @15, from edge 2 (t=15): succeeds vacuously at edge 2 (t=15).
- edge @25, from edge 3 (t=25): succeeds vacuously at edge 3 (t=25).
- edge @35, from edge 4 (t=35): succeeds vacuously at edge 4 (t=35).
- edge @45, from edge 5 (t=45): succeeds vacuously at edge 5 (t=45).
Sampled values, edge 1 to 6: valid 0 1 1 1 0 0; ready 1 1 1 1 1 1.
The program behind the lanes
The lanes come from running this program on Verilator 5.052. Each lane is a separate copy of hold_valid that can start only at that lane's edge, so every verdict and its edge are read from that copy's own output.
module tb;
logic clk = 0;
always #5 clk = ~clk; // rising edges at 5, 15, 25, ...
logic valid = 0, ready = 1; // the sink is ready at every edge
hold_valid: assert property (@(posedge clk) valid && !ready |=> valid);
initial begin
@(negedge clk); valid = 1; // three beats, taken at 15, 25 and 35
@(negedge clk);
@(negedge clk);
@(negedge clk); valid = 0;
@(negedge clk);
@(negedge clk);
$finish;
end
endmodule
A bus checker needs a slow sink in at least one test, or its most important rules are never tried.
So count the antecedents. A small counter beside the property, incremented at every edge where valid && !ready is true, tells you how many real checks happened. Zero is a hole in the tests, whatever the pass count says.
Where the checker lives
A protocol checker is a module whose ports are exactly the wires of the link and whose body is the properties, one per sentence. It changes nothing on the link; it only watches. The bind directive puts a copy of it inside every instance of a module. The same checker then runs on the bench, in the system, and in a colleague's testbench you have never seen.
Asked in interviews: specs and checkers
How do you verify a bus protocol?
Turn each must sentence of the spec into a property, bind the properties to every instance of the bus, and add a scoreboard for the data. Then make sure tests stall, so the properties are exercised.
What is a vacuous pass?
An attempt whose antecedent was false, so the property checked nothing and reported no failure. A rule that only passes vacuously has never been tested.
What does "may" in a spec mean for your checker?
It is a freedom, so the checker must stay silent about it. A checker that flags a permitted event is wrong, however reasonable the flag looks.
Why bind a checker instead of adding assertions to the design?
The design stays untouched and the checker follows every instance into every environment. One file holds every rule, and it can be removed for synthesis in one line.
How do you know a protocol checker was exercised?
Count antecedent matches, or cover them. Each rule needs at least one test where its antecedent happened and the rule held.
Try it
Two sentences are already properties in link_props, bound into the sink. The stimulus withdraws one offer, and both the property and the procedural checker name the edge. Run it.
Then write sentence 3 yourself: at the first edge after reset is released, valid is 0. The property is @(posedge clk) $rose(rst_n) |-> !valid, with no disable iff. Break it by moving the first send above the @(posedge clk); #1; line that follows the reset release.
edge 3: beat a1 taken edge 4: beat b2 taken property hold failed at edge 6: valid withdrawn RULE hold broken at edge 6: valid dropped before ready: the offer was withdrawn edge 10: beat c3 taken properties: 1 failure(s) checker: 3 transfers, 4 stalled edges, 1 rules brokenRead included file: link_sink.sv
// link_sink.sv: the target of the generic link (read-only in this lesson).
// A DEPTH-entry FIFO behind valid/ready. It takes a beat at every rising edge where valid and
// ready are both 1, and hands beats on to its consumer at every edge where drain and out_valid
// are both 1. ready is 1 while the FIFO has room: it never waits for valid.
module link_sink #(parameter int DEPTH = 4, parameter int WIDTH = 8) (
input logic clk,
input logic rst_n,
input logic valid,
input logic [WIDTH-1:0] data,
input logic last,
output logic ready,
input logic drain, // the consumer behind the FIFO takes a beat while 1
output logic out_valid,
output logic [WIDTH-1:0] out_data,
output logic out_last,
output int count // beats held right now
);
logic [WIDTH:0] mem [DEPTH]; // {last, data}
int wr = 0, rd = 0;
assign ready = (count < DEPTH);
assign out_valid = (count > 0);
assign {out_last, out_data} = mem[rd];
wire push = valid && ready;
wire pop = drain && out_valid;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
wr <= 0; rd <= 0; count <= 0;
end else begin
if (push) begin mem[wr] <= {last, data}; wr <= (wr + 1) % DEPTH; end
if (pop) rd <= (rd + 1) % DEPTH;
count <= count + (push ? 1 : 0) - (pop ? 1 : 0);
end
end
endmodule
Read included file: link_checker.sv
// link_checker.sv: the rules of the generic link, checked at every rising edge (read-only).
// Each broken rule is named with the edge where it was seen:
// reset valid is 0 while reset is active;
// hold once valid is 1 and ready is 0, valid is still 1 at the next edge (an offer is not withdrawn);
// stable while an offer waits, data and last do not change;
// known data is not x while valid is 1.
// The one rule no checker can see on the wires, "valid must not wait for ready", is the stimulus's
// own responsibility (lesson P1.3).
// It also counts transfers and stalled edges, and prints one summary line at the end of the run.
module link_checker #(parameter int WIDTH = 8) (
input logic clk,
input logic rst_n,
input logic valid,
input logic ready,
input logic [WIDTH-1:0] data,
input logic last
);
int cycle = -1, transfers = 0, stalls = 0, broken = 0;
logic pv = 0, pr = 0, pl = 0;
logic [WIDTH-1:0] pd = 'x;
always @(posedge clk) begin
cycle++;
if (!rst_n) begin
if (valid === 1) report("reset", "valid is 1 during reset");
end else begin
if (pv && !pr && valid !== 1) report("hold", "valid dropped before ready: the offer was withdrawn");
if (pv && !pr && valid === 1 && data !== pd) report("stable", "data changed while the offer waited");
if (pv && !pr && valid === 1 && last !== pl) report("stable", "last changed while the offer waited");
if (valid === 1 && $isunknown(data)) report("known", "data is x while valid is 1");
if (valid === 1 && ready === 1) transfers++;
if (valid === 1 && ready !== 1) stalls++;
end
pv = (valid === 1); pr = (ready === 1); pd = data; pl = last;
end
function void report(input string rule, input string what);
broken++;
$display("RULE %s broken at edge %0d: %s", rule, cycle, what);
endfunction
final $display("checker: %0d transfers, %0d stalled edges, %0d rules broken", transfers, stalls, broken);
endmodule
Read included file: link_tasks.svh
// link_tasks.svh: the source side of the generic link, as tasks for the stimulus (read-only).
// Every task returns one time unit after the rising edge where its last beat transferred, so the
// next call starts driving in the following cycle. `cycle` counts rising edges from 0.
int cycle = -1;
always @(posedge clk) cycle = cycle + 1;
// Offer one beat and hold it until the sink takes it.
task automatic send(input logic [7:0] d, input logic is_last = 0);
valid = 1; data = d; last = is_last;
do @(posedge clk); while (!ready);
#1 $display("edge %0d: beat %02h taken", cycle, d);
valid = 0; data = 'x; last = 0;
endtask
// A burst of n beats with consecutive values; last marks the final beat.
task automatic send_burst(input logic [7:0] first, input int n);
for (int i = 0; i < n; i++) begin
valid = 1; data = first + i[7:0]; last = (i == n - 1);
do @(posedge clk); while (!ready);
#1 $display("edge %0d: beat %02h taken%s", cycle, first + i[7:0], last ? " (last)" : "");
end
valid = 0; data = 'x; last = 0;
endtask
// Drive nothing for n cycles.
task automatic idle(input int n);
repeat (n) @(posedge clk);
#1;
endtask
Check yourself
Remember
- Read the picture first: a hexagon is a bus value, hatching means the value does not matter, and every vertical line is a sampled edge.
- Must is a rule, should is advice, may is a freedom. A checker reports the first, may warn on the second, and must stay silent on the third.
- One sentence becomes one property: the antecedent is the condition the sentence talks about, the consequent is what must follow, and the edge gap picks
|->,|=>or##n. - A vacuous pass checked nothing. Count the antecedent matches, and give every bus rule at least one test with a stall.
- A protocol checker is a module of properties over the link's wires, bound into every instance, so it changes nothing and misses no instance.
Practice
- Properties for the link Verify · Hard