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 valid and the data until then. The receiver may drive ready freely.
  • Rules for the block under check are assertions; rules for its neighbors are assumptions.
  • Leaving ready unconstrained 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.
Did you find what you were looking for?

Quiz

No quiz questions available for this article.