A producer occasionally changes its data while the consumer is stalling it. Most of the time the consumer is ready, so nothing goes wrong; once in a few thousand transfers the stalled word is replaced, and one packet is corrupted. The scoreboard reports a data mismatch a hundred cycles later, three blocks downstream, and the debug takes a day. An assertion on the interface itself would have stopped the simulation at the exact cycle, with the rule's number in the message.
Which checks should every interface carry, and how are they packaged so that every testbench uses them?
The obvious answer is to rely on the end-to-end scoreboard: if data arrives intact, the interface works. The scoreboard sees the effect far from the cause, after the evidence has scrolled away. And it misses rule violations that happen not to corrupt data in this particular test: a producer that drops valid early works with a consumer that never stalls, then fails with the next consumer that does. Interface rules need checking at the interface, on every cycle, whatever the traffic.
Check | Rule | Kind |
|---|---|---|
Hold | Once valid, stay valid until the transfer | Safety |
Stable | While stalled, payload does not change | Safety |
Known | While valid, no X or Z on payload or control | Safety |
Conserved counts | Credits never negative or above the maximum; outstanding requests within the limit | Safety |
Bounded length | A packet or burst ends within its maximum length | Safety |
Bounded response | Every request answered within N cycles | Liveness, bounded (lesson 15.2) |
Put them in one checker module with the interface's signals as inputs, parameterised by its limits, and connect an instance to every copy of the interface in every testbench (and, through bind, inside the design). The checker is written once, from the specification's numbered rules, and reused everywhere.
The checker holds four of the rules and counts each one's violations. The testbench connects one checker to a correct producer and one to a faulty producer that changes its data while stalled (cycle 6) and drops valid before its transfer completes (cycle 13). A second reset arrives at cycle 25, after the first beat of a packet; both producers abandon that packet and start a new one.
This listing uses concurrent assertions with disable iff, which Icarus Verilog 13 does not parse. Run it with Verilator (verilator --binary --timing --assert file.sv); it is the one listing in the course that needs Verilator.
// DCLink step 12a: a reusable protocol checker for one valid/ready hop.
// It holds the interface's rules as assertions and counts every violation:
// hold: once valid, valid stays high until the transfer (valid && ready)
// stable: while stalled, data and last do not change
// known: while valid, data and last are never X or Z
// length: a packet ends (last) within MAX_BEATS beats
// The checker's own state (the beat count) follows the protocol's reset: a packet cut
// short by reset is abandoned, so the count restarts. The testbench connects one
// checker to a correct producer and one to a faulty producer that changes its data
// while stalled and later drops valid before the transfer; a second reset arrives in
// the middle of a packet. The checker must stay silent on the first producer, also
// across the mid-packet reset, and catch both bugs in the second.
module dclink_checker #(parameter int MAX_BEATS = 8) (
input logic clk, rst_n, vld, rdy, last,
input logic [31:0] data,
output int n_hold, n_stable, n_known, n_length
);
initial begin n_hold = 0; n_stable = 0; n_known = 0; n_length = 0; end
int beats = 0; // beats of the current packet so far
always @(posedge clk)
if (!rst_n) beats <= 0; // reset abandons a partial packet
else if (vld && rdy) beats <= last ? 0 : beats + 1;
a_hold: assert property (@(posedge clk) disable iff (!rst_n) vld && !rdy |=> vld) else n_hold++;
a_stable: assert property (@(posedge clk) disable iff (!rst_n) vld && !rdy |=> $stable(data) && $stable(last)) else n_stable++;
a_known: assert property (@(posedge clk) disable iff (!rst_n) vld |-> !$isunknown({data, last})) else n_known++;
a_length: assert property (@(posedge clk) disable iff (!rst_n) vld && rdy && !last |-> beats < MAX_BEATS - 1) else n_length++;
endmodule
module tb;
logic clk = 0;
always #5 clk = ~clk;
int cyc = 0;
logic rst_n = 0;
// Reset at the start, and again for two cycles from cycle 25, in the middle of a packet
always @(posedge clk) begin cyc <= cyc + 1; rst_n <= cyc >= 1 && !(cyc == 24 || cyc == 25); end
wire rdy = (cyc % 3 == 2); // the receiver accepts every third cycle
// A producer sending 3-beat packets: words 1, 2, 3 (last on 3), then 4, 5, 6, ...
// When FAULTY, it changes the word while stalled in cycle 6 and drops valid in cycle 13.
logic g_vld = 0, f_vld = 0, g_last = 0, f_last = 0;
logic [31:0] g_data = 0, f_data = 0;
int g_n = 0, f_n = 0;
always @(posedge clk) if (!rst_n) begin
g_vld <= 0; f_vld <= 0; g_n <= 0; f_n <= 0; // reset: both producers start a new packet
end else begin
if (!g_vld || rdy) begin g_vld <= 1; g_data <= g_n + 1; g_last <= (g_n % 3 == 2); g_n <= g_n + 1; end
if (!f_vld || rdy) begin f_vld <= 1; f_data <= f_n + 1; f_last <= (f_n % 3 == 2); f_n <= f_n + 1; end
else if (cyc == 6) f_data <= 32'hdead; // bug 1: data changed while stalled
else if (cyc == 13) f_vld <= 0; // bug 2: valid dropped before the transfer
end
int g_h, g_s, g_k, g_l, f_h, f_s, f_k, f_l;
dclink_checker #(.MAX_BEATS(3)) good (.clk, .rst_n, .vld(g_vld), .rdy, .last(g_last), .data(g_data), .n_hold(g_h), .n_stable(g_s), .n_known(g_k), .n_length(g_l));
dclink_checker #(.MAX_BEATS(3)) fault (.clk, .rst_n, .vld(f_vld), .rdy, .last(f_last), .data(f_data), .n_hold(f_h), .n_stable(f_s), .n_known(f_k), .n_length(f_l));
initial begin
#450;
if (g_h + g_s + g_k + g_l == 0 && f_h == 1 && f_s >= 1 && f_k + f_l == 0)
$display("PASS: checker silent on the correct producer, also across a mid-packet reset; caught the faulty one changing data while stalled and dropping valid");
else $display("FAIL: good %0d/%0d/%0d/%0d faulty %0d/%0d/%0d/%0d", g_h, g_s, g_k, g_l, f_h, f_s, f_k, f_l);
$finish;
end
endmoduleWhen run, it prints a PASS line: the checker on the correct producer stays silent, and the checker on the faulty producer records one hold violation and the stable violation, at the cycles where the bugs happened. In a real testbench, each failure would also print the rule number and stop the test.
Look at the beat counter: it is cleared by rst_n as well as by last. disable iff only switches the assertions off during reset; it does not touch the checker's own state. Without the reset term, the beat already counted before the second reset carries into the next packet, and the correct producer's next 3-beat packet is reported as too long: a false failure that makes engineers distrust the checker. A checker's state must follow the protocol's reset rules exactly as the design's does.
Disable during reset, and reset the checker's state: disable iff (!rst_n) stops reset values triggering false failures, but any counters or flags inside the checker must also be cleared on reset, or a packet cut short by reset contaminates the next one.
Name each assertion after its rule: a failure message that says "R3" points straight at the specification.
Check both directions: the same checker verifies the producer's rules (in the producer's testbench) and lets the consumer's testbench assume them.
Add cover properties: assertions that never had a chance to fail prove nothing; cover the stalls and corner cases they check (lesson 15.6).
Protocol | Assertions |
|---|---|
AMBA AXI | Arm publishes protocol assertions for AXI4, AXI4-Lite and AXI4-Stream (fact A29), built on rules A1, A2 and A21 |
Any standard interface | Verification IP includes protocol checkers (lesson 15.8) |
DCLink | Step 12a, this checker |
The opening corruption (illustrative) was found within minutes once the stable assertion was bound into the design at that interface: the failure pointed at the producer and the cycle. The same checker, bound at the other six instances of the interface in the chip, found a second producer with the same bug that had not yet corrupted anything in testing.
It is a promise about the next cycle: if this cycle is a stall, valid must still be high in the following one.
Nothing: their trigger (valid and not ready) never occurs, which is why each needs a cover property.
A standard set of interface assertions (hold, stable, known, conserved counts, bounded length and response) catches most interface bugs at their cause.
Package them as one reusable checker, named after the specification's rules, and bind it at every instance.
Disable during reset and pair every assertion with a cover property.