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 ready pulse with nothing to takeThe sink advertises room, takes it back, then serves the real offer two cycles later.
validdatareadyDMA enginesourceMemory ctrlsink

A lit ready with a dark valid is permission with nothing to permit.

012345clkvalidreadydataxa1x
  1. Cycle 1

    The controller raises ready: it has room. Nothing is offered.

  2. Cycle 2

    ready drops again. The DMA checker called this a withdrawn offer. But only valid makes an offer, and valid was 0.

  3. Cycle 4

    The real offer from cycle 3 is taken here. The spec: a sink may lower ready before valid rises. 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.

A stalled offer, drawn as a spec would draw it
01234validreadydataxa1xoffertransfer

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.

WordWhat it meansWhat a checker does
must, shall, must notA rule. Every implementation obeys it.Reports each violation with the edge
should, recommendedAdvice. A good implementation follows it.Warns, or counts how often it is followed
may, permitted, canA freedom. Any choice is correct.Nothing. Flagging it is a checker bug
must, with a numberA rule with a bound: within 16 cycles, at most 4 outstandingReports 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.

From one sentence to a checker on every linkArrows show the order of work. The last hop is a bind: a copy of the checker inside every sink.
From one sentence to a checker on every linkArrows show the order of work. The last hop is a bind: a copy of the checker inside every sink. spec The sentence Once valid is 1 andready is 0, validstays 1 until thetransfer drawing The picture A stalled offer, drawnedge by edge SVA The property valid && !ready |=>valid checker link_props Every property ofthe link in one module design Every link_sink bind puts a copy oflink_props inside eachone draw itname the edgescollectbind
  1. 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
    1. data hold rule: The sentence → The picture
  2. Name the edges

    The antecedent is valid && !ready at one edge. The consequent is valid at the next edge, so the operator is |=>.

    Follow the calls, values and waits
    1. data hold rule: The picture → The property
  3. 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
    1. data hold rule: The property → link_props
  4. Bind

    One bind line puts a copy of link_props inside every link_sink, in the bench and in the system, without touching the sink.

    Follow the calls, values and waits
    1. 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:

Two stalled offers, one spoiledOne lane per stalled edge of valid && !ready |=> $stable(data).valid && !ready |=> $stable(data)Measured on Verilator 5.052: one copy of the assertion per lane
attempt startspassfail
Two stalled offers, one spoiledstalled @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).edgetime15215325435545655765875985clkvalidreadydata00a100b2b3attemptsstalled @15stalled @15: passes at edge 3 (t=25)stalled @25stalled @25: passes at edge 4 (t=35)stalled @55stalled @55: fails at edge 7 (t=65)stalled @65stalled @65: passes at edge 8 (t=75)
  1. Edge 3 · t=25

    a1 is offered and stalled at 15 and 25. Each stalled edge starts an attempt; both find a1 still there.

  2. 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.

  3. 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.

Three beats, zero stalls, five vacuous passesThe hold property on a sink that is always ready.valid && !ready |=> validMeasured on Verilator 5.052: one copy of the assertion per lane
attempt startsvacuous success: the antecedent did not match
Three beats, zero stalls, five vacuous passesedge @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).edgetime15215325435545655clkvalidreadyattemptsedge @5edge @5: succeeds vacuously at edge 1 (t=5)edge @15edge @15: succeeds vacuously at edge 2 (t=15)edge @25edge @25: succeeds vacuously at edge 3 (t=25)edge @35edge @35: succeeds vacuously at edge 4 (t=35)edge @45edge @45: succeeds vacuously at edge 5 (t=45)
  1. Edge 2 · t=15

    Beat one is taken at 15 with ready already high. valid && !ready is false, so the attempt ends as it starts.

  2. 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
  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

RUNVerilator · simulator service
Edit the code, then run it
Expected: 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 broken
Read 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

Predict the outputThe counter counts edges where the hold property's antecedent is true; the property counts its failures. What line is printed?
module tb;
  logic clk = 0;
  always #5 clk = ~clk;                 // rising edges at 5, 15, 25, ...
  logic valid = 0, ready = 0;
  int checked = 0, failed = 0;
  always @(posedge clk) if (valid && !ready) checked++;
  hold: assert property (@(posedge clk) valid && !ready |=> valid) else failed++;
  initial begin
    @(negedge clk); valid = 1;            // offered: stalled at 15 and 25
    @(negedge clk);
    @(negedge clk); ready = 1;            // taken at 35
    @(negedge clk); valid = 0; ready = 0;
    @(negedge clk); valid = 1;            // offered: stalled at 55
    @(negedge clk); valid = 0;            // withdrawn before 65
    @(negedge clk); valid = 1; ready = 1; // taken at 75 with no stall
    @(negedge clk); valid = 0;
    @(negedge clk);
    $display("checked=%0d failed=%0d", checked, failed);
    $finish;
  end
endmodule
Spot the bugThe stimulus offers one beat, stalls it for two edges with the data held, then takes it. Each option is a property placed on the link. Which one reports a failure on this correct link?
ConceptA spec says "the target may deassert ready before valid is asserted". What should a protocol checker do with that sentence?
ConceptA regression report shows the hold property with 40,000 passes and 0 failures. What does that prove about the link?
Read the waveformFor the hold property valid && !ready |=> valid, at which cycles does an attempt start that checks something?
One offer, two stalled cycles
012345validready

One value per cycle, sampled at the edge that ends the cycle.

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