Blocks talk to each other through handshakes. A sender offers data and a receiver takes it, and both sides follow a few simple rules so that no word is lost or duplicated. Those rules are among the most valuable properties you can write, because handshake bugs show up only with particular timing between the two sides, exactly the timing a test rarely produces. This article covers the request/acknowledge and valid/ready handshakes and finds a real bug in a valid/ready sender.
10 min read | Beginner to Intermediate
What You'll Learn
- State the rules of the request/acknowledge and valid/ready handshakes as properties
- Check that a sender holds its request and keeps its data stable while waiting
- Decide which rules are assertions and which are assumptions for a given block
- Find a data-stability bug in a valid/ready sender
Request and Acknowledge
In a request/acknowledge handshake, the sender raises req and waits. The receiver answers with ack when it has taken the data. The usual rules:
// the sender holds the request until it is acknowledged
a_req_hold: assert property (@(posedge clk) disable iff (rst) req && !ack |=> req);
// the data does not change while the request waits
a_req_data: assert property (@(posedge clk) disable iff (rst) req && !ack |=> $stable(data));
// the receiver only acknowledges a request
a_ack_valid: assert property (@(posedge clk) disable iff (rst) ack |-> req);
Many protocols add a timing rule for the receiver, such as req |-> ##[1:8] ack, so that the sender is never left waiting forever.
Valid and Ready
The valid/ready handshake is the standard for streaming data inside chips and on buses such as AXI. The sender drives valid and data, the receiver drives ready, and a word moves in every cycle where both are high. There is no separate acknowledge. A handshake can happen in every cycle, so the interface can move one word per cycle.
The rules for the sender:
// once valid, stay valid until the handshake
a_valid_hold: assert property (@(posedge clk) disable iff (rst) valid && !ready |=> valid);
// and keep the data stable until then
a_data_hold: assert property (@(posedge clk) disable iff (rst) valid && !ready |=> $stable(data));
The receiver may raise and drop ready whenever it likes. And the sender must not wait for ready before it raises valid, otherwise two blocks that both wait for the other would never start. That last rule is about the structure of the logic, not about a value in a cycle, so it is checked by review rather than by a property.
Quick Check
Q: In a valid/ready interface, valid and ready are both high for three cycles in a row. How many words were transferred?
Show Answer
Three. Every cycle with both signals high is a handshake that moves one word, so the data must change to the next word after each one.
Assertion or Assumption?
The same rule can be an assertion or an assumption, depending on which block you check. When you check the sender, the sender rules are assertions: the sender must follow them. Rules for the receiver are assumptions: they describe the environment the sender can rely on. When you check the receiver, it is the other way round.
For a valid/ready sender, there is almost nothing to assume, because the receiver may drive ready freely. That is good news: the tool tries every possible pattern of ready, including the cruel ones, such as a receiver that is never ready.
The Design: a Valid/Ready Sender
This sender produces the words 0, 1, 2 and so on, one per handshake. go asks it to offer the next word:
module vr_sender (input clk, input rst, input go, input ready,
output reg valid, output reg [7:0] data);
always @(posedge clk)
if (rst) begin
valid <= 1'b0;
data <= 8'd0;
end else begin
if (go) valid <= 1'b1;
else if (ready) valid <= 1'b0;
if (valid) data <= data + 8'd1; // bug: should be (valid && ready)
end
endmodule
The data counter advances whenever valid is high, not only when a handshake happens. With a receiver that is always ready, which is what a simple testbench often uses, the two are the same and the bug is invisible.
Running It in ChipVerify Lab
The |=> rules become checks on the previous cycle:
`ifdef FORMAL
reg init = 1'b1;
always @(posedge clk) init <= 1'b0;
always @(*) if (init) assume (rst);
// valid is not withdrawn before the handshake
always @(posedge clk) if (!init && !$past(rst) && $past(valid) && !$past(ready)) assert (valid);
// data stays stable while waiting
always @(posedge clk) if (!init && !$past(rst) && $past(valid) && !$past(ready)) assert (data == $past(data));
`endif
There is no assumption on ready or go. The run fails at step 4:
The tool chose a receiver that is never ready. valid rises in step 2 with word 0, and in step 3 the data is already 1, although no handshake has happened. Word 0 is lost. Change the condition to if (valid && ready) and the run passes.
Try the other classic sender bug too: replace else if (ready) valid <= 1'b0; with else valid <= 1'b0;. Now the sender withdraws its offer when go drops, whether or not the word was taken. The first assertion fails at step 4.
Quick Check
Q: A testbench drives ready high in every cycle. Can it find the data bug in this sender?
Show Answer
No. With ready always high, every cycle with valid is a handshake, so advancing on valid alone is correct. The bug appears only when the receiver stalls, which the formal tool tries on its own.
Common Mistakes
Mistake #1: Assuming the receiver is always ready
An assumption such as assume (ready) makes the check easy and removes exactly the cases where handshake bugs hide. Let the tool drive ready freely unless the specification limits it.
Mistake #2: Checking only valid, not the data
A sender can keep valid correct and still change the data while waiting, as this one does. Always pair the "hold valid" rule with a "hold data" rule.
Mistake #3: Mixing up the two sides
When checking a sender, the sender rules are assertions and the receiver rules are assumptions. Writing a sender rule as an assumption hides the sender's bugs, and writing a receiver rule as an assertion fails on legal receiver behavior.
Practice Exercise
Challenge: Words in Order
Open the example, run it, and fix the bug. Then add a property that checks the content of the stream: after every handshake, the next word is the previous word plus 1. Change the increment in the RTL to 2 and check that your property catches it.
Hint
A handshake in the previous cycle is $past(valid && ready). The previous word is $past(data).
Solution
// after a handshake, the next word is the previous word plus 1
always @(posedge clk)
if (!init && !rst && !$past(rst) && $past(valid && ready))
assert (data == $past(data) + 8'd1);
In SVA: assert property (@(posedge clk) disable iff (rst) valid && ready |=> data == $past(data) + 8'd1);. It passes on the fixed sender. With an increment of 2 it fails at step 4. The two handshake rules alone would not see this bug, because the data is stable while waiting. It is simply the wrong data.
Open the ready-made example in the lab and run the formal check from the formal panel. The free tier allows a depth of up to 10 cycles, and PRO allows up to 100.
Summary
- A request is held until it is acknowledged, and an acknowledge only comes with a request.
- In valid/ready, a word moves in every cycle where both are high. The sender holds
validand the data until then. The receiver may drivereadyfreely. - Rules for the block under check are assertions; rules for its neighbors are assumptions.
- Leaving
readyunconstrained let the tool find a sender that changed its data while waiting, a bug that an always-ready testbench cannot see. - Handshake rules check timing. Add a data rule to check the content of the stream.