Ready/Valid Protocol Primer

Posted on Sun 02 August 2020 in Logic

Introduction

In digital logic design, the ready/valid protocol is a simple and common handshake process for one component to transmit data to another component in the same clock domain. Every FIFO implements a version of this protocol on its ports, whether the signals are called "ready/valid", or "full/push" and "pop/empty". Also, ready/valid signals are used as the flow control mechanism for every channel of the popular AMBA AXI high performance on-chip interconnect.

Despite its ubiquitous application, there is no de facto standard implementation. Engineers routinely implement ad hoc ready/valid logic in every codebase they work with. In this primer, we will describe the protocol in detail, propose standard naming conventions, and write some reusable SystemVerilog interface code to bolster design verification.

The code we will write is not advanced, but familiarity with SystemVerilog Assertions (SVA) will be helpful.

Protocol Description

Assume we have two components in a hardware design with a unidirectional data flow. A "Transmitter" (Tx) sends data to a "Receiver" (Rx). The Transmitter and Receiver are equal partners in this data exchange. That is, the Transmitter cannot force the Receiver to consume data, and the Receiver cannot force the Transmitter to produce data. For a transfer of data to happen, the two sides need to "shake hands". The Transmitter needs to have "valid" data, and the Receiver needs to be "ready" to receive the data.

Figure 1 shows a block diagram of the basic ready/valid/data components. Note that the "ready" and "valid" signals are single wires, but the "data" signal is a bus composed of multiple wires transmitting in parallel.

Figure 1: Data Transmitter and Receiver

Aside Regarding Component Names

Phil Karlton once said "there are only two hard things in Computer Science: cache invalidation and naming things." The book Microarchitecture of Network-on-Chip Routers uses the terms "sender" and receiver". The AMBA Specification uses the terms "source" and "destination", or "master" and "slave". I have seen other documents use terms such as "producer" and "consumer", and so on.

I have arbitrarily chosen the names "Transmitter" and "Receiver" because

  • they are unambiguous - one transmits data, the other receives it
  • they are commonly used in digital signal processing (DSP)
  • they both have the same number of syllables
  • I like the "Tx" and "Rx" abbreviations

Aside Regarding Waveform Perspective

When analyzing logical protocols that are implemented using real-world, analog technologies such as wires and transistors, we need to draw the waveform diagram with appropriate propagation delays. Unfortunately, there is no standard frame of reference for the link.

Figure 4 shows a more realistic representation of Figure 3, but from the perspective of the Transmitter. Notice that the ready signal arrives late.

Figure 4: Ready/Valid Protocol Transmit Perspective View

Figure 5 shows a similarly realistic representation of Figure 3, but this time from the perspective of the Receiver. Notice that the valid/data signals arrive late.

Figure 5: Ready/Valid Protocol Receive Perspective View

These diagrams help visualize important implementation details. For example, the designer of the Transmitter should avoid adding significant logic to the ready input, because it might violate the setup time.

In addition, considering the handshake signal propagation delays can clarify their meaning. When the Receiver drives ready=1 onto the wire, it does not yet know whether the Transmitter will send data. In plain English, it sends the message: "if you transmit data on this cycle, I am ready for it". Similarly, when the Transmitter drives valid=1 onto the wire, it sends the message: "if you are ready, I will transmit this data to you". Both the ready and valid signals contain propositional logic that can only be satisfied after the signal propagation delay.

Interface Implementation Conventions

When using ready/valid interfaces in SystemVerilog code, bundle the signals together and use a consistent naming convention. Different projects have different rules, but I recommend at least the following:

  1. Order the ready/valid/data signals consistently
  2. Use a common interface name prefix
  3. Use standard ready/valid/data name suffixes

For example, here is a good module parameter/port list for a generic FIFO.

module Fifo #(
    parameter type T = logic [7:0]
) (
    // Read Port
    input  logic read_ready,
    output logic read_valid,
    output T     read_data,

    // Write Port
    output logic write_ready,
    input  logic write_valid,
    input  T     write_data,

    input logic clock,
    input logic reset
);
    ...
endmodule : Fifo

A module may have several ready/valid interfaces for several purposes. In order to find them quickly using a command line grep or debugger glob pattern match, all members of an interface bundle should have a common prefix. This particular FIFO has a read and write port, which are given 'read_', and 'write_' prefixes, respectively.

Use the standard suffix names '_ready', '_valid', and '_data'. Do not use clever abbreviations like '_rdy', and '_vld'. Do not sacrifice clarity for brevity. The AMBA specification does not abbreviate these signals names; neither should we.

Also, do not use port direction naming conventions such as '_o' for "output" and '_i' for "input". It should be obvious to the reader which signals are inputs and outputs. For example, a FIFO's write port should have 'valid' and 'data' inputs.

Do not use a SystemVerilog interface to implement the ports. SV interfaces provide ergonomic benefit for passing around large bundles of signals, but they will end up costing more in tool support and maintenance issues than they ever pay in benefits for a collection of only three signals. Reserve usage of interfaces for verification components such as UVM agents.

Formal Checks

SystemVerilog assertions are one of the most productive ways of finding and fixing logical errors and coverage holes. In this section, we will write a standard suite of ready/valid protocol assertions that can be copied and pasted for every interface instance.

Before delving into the implementation details, to reduce the amount of boilerplate required to write concurrent assumptions, assertions, and cover properties, we will first define three text replacement preprocessor macros. For background on this best practice, read section 8 of SystemVerilog Assertions Bindfiles & Best Known Practices for Simple SVA Usage, presented at Synopsys User Group (SNUG) 2016.

`define ASSUME(name, expr, clock, reset) \
name: assume property ( \
    @(posedge clock) disable iff (reset) (expr) \
);

`define ASSERT(name, expr, clock, reset) \
name: assert property ( \
    @(posedge clock) disable iff (reset) (expr) \
);

`define COVER(name, expr, clock, reset) \
name: cover property ( \
    @(posedge clock) disable iff (reset) (expr) \
);

Different tools treat assumptions, assertions, and cover properties differently. For example, for simulators there is no difference between an assumption and an assertion -- they are both just dynamic checks. Formal verification (FV) tools, on the other hand, create logic proofs using assertions, and use assumptions to constrain the stimulus for those proofs. See IEEE 1800-2017 for a more detailed description. For our purposes, we will use assumptions for module inputs, and assertions for module outputs.

Note that applying checks to both inputs and outputs will cause a small but measurable simulation performance degradation due to duplicated work. The inputs of one module are the outputs of another, so we end up doing each check twice. The benefits of debuggability outweigh the costs of simulation time, but if we desire maximum efficiency, we can fix this overlap by disabling all assumptions in simulation.

Always Check for Xes on Control Signals

Interface control signals should never be unknown (i.e. "X", not in \(\{0, 1\}\)). Also, whenever valid=1 is asserted, its corresponding data should never have have unknown bits. Never underestimate the amount of problems your team will discover by writing X checks to catch uninitialized state.

Using the FIFO module ports from above, with "write" and "read" ready/valid interfaces, the code for this is straightforward:

/*** Transmit (Tx) ***/
// Ready must never be unknown
`ASSUME(RV_Tx_NeverReadyUnknown,
    !$isunknown(read_ready),
    clock, reset)

// Valid/Data must never be unknown
`ASSERT(RV_Tx_NeverValidUnknown,
    !$isunknown(read_valid),
    clock, reset)
`ASSERT(RV_Tx_NeverDataUnknown,
    read_valid |-> !$isunknown(read_data),
    clock, reset)

/*** Receive (Rx) ***/
// Ready must never be unknown
`ASSERT(RV_Rx_NeverReadyUnknown,
    !$isunknown(write_ready),
    clock, reset)

// Valid/Data must never be unknown
`ASSUME(RV_Rx_NeverValidUnknown,
    !$isunknown(write_valid),
    clock, reset)
`ASSUME(RV_Rx_NeverDataUnknown,
    write_valid |-> !$isunknown(write_data),
    clock, reset)

Note that the SystemVerilog $isunknown system task will return 1 if any bits of the input are unknown.

Protocol Violations

Using Figure 2 as a reference, the Transmitter or Receiver commit a protocol violation whenever they cause an invalid state transition on the link. When the link is in "Wait for Ready" state, the Transmitter may not deassert valid on the next cycle. For the strict protocol with stable ready requirement, when the link is in "Wait for Valid" state, the Receiver may not deassert ready on the next cycle. Finally, the Transmitter may not put new data onto the link until after the current data has been successfully transferred.

For the less strict protocol with no stable ready requirement, do NOT use the assumptions/assertions with *_ReadyStable suffix.

/*** Transmit (Tx) ***/
// Ready must remain stable until Valid/Data
// Note: Optional
`ASSUME(RV_Tx_ReadyStable,
    (read_ready && !read_valid) |=> read_ready,
    clock, reset)

// Valid/Data must remain stable until Ready
`ASSERT(RV_Tx_ValidDataStable,
    (!read_ready && read_valid) |=> (read_valid && $stable(read_data)),
    clock, reset)

/*** Receive (Rx) ***/
// Ready must remain stable until Valid/Data
// Note: Optional
`ASSERT(RV_Rx_ReadyStable,
    (write_ready && !write_valid) |=> write_ready,
    clock, reset)

// Valid/Data must remain stable until Ready
`ASSUME(RV_Rx_ValidDataStable,
    (!write_ready && write_valid) |=> (write_valid && $stable(write_data)),
    clock, reset)

A full explanation of SystemVerilog Assertions (SVA) is beyond the scope of this primer, but let's take a closer look at the RV_Tx_ValidDataStable code. The statement X |=> Y means: "If X is true on this clock cycle, then check that Y is true on the next clock cycle". The $stable(X) statement means: "The value of X on this clock cycle is the same as it was on the previous clock cycle." Putting it all together, in plain English the assertions means: "If the link is in Wait for Ready state on this clock cycle, then both valid/data must stay the same on the next clock cycle".

Cover Properties

After we spend the time to write a thorough test suite, coverage collection will help verify that the tests are exercising all the desired functionality.

First, we want to cover data transfers. If no data transfers have occurred on the interface, we have not actually tested anything.

The following code increments a coverage counter whenever the links are in the "Transfer" state.

// Cover data transfer
`COVER(RV_Tx_Transfer, read_ready && read_valid, clock, reset)
`COVER(RV_Rx_Transfer, write_ready && write_valid, clock, reset)

Another important cover property is "backpressure". When downstream components are blocking new data from being received, they are said to "backpressure" the upstream components. Without going into a full discussion of queueing fundamentals, it is important to exercise backpressure on all components in order to verify that storage buffers are adequately sized.

The following code increments a coverage counter whenever the links are in the "Wait for Valid" (backpressure) state:

// Cover backpressure
`COVER(RV_Tx_Backpressure, !read_ready && read_valid, clock, reset)
`COVER(RV_Rx_Backpressure, !write_ready && write_valid, clock, reset)

Checker Component Methodology

In the previous sections, we have defined several assumptions, assertions, and coverpoints. Good coding practice suggests we group these related items together in a container.

SystemVerilog provides a design element called a "checker" for this purpose, but I do not recommend using it. Not only are checkers not parameterizable, but they are not supported by EDA tools nearly as well as modules. Keep it simple, and just use a module for a reusable checker container.

In addition to a type parameter for the data inputs, we should have parameters for the following:

  • Selectively enable transmitter and receiver checks.
  • Selectively enable the strict ready stable requirement.

For example:

module CheckReadyValid #(
    parameter type T = logic [7:0]
    parameter bit Tx = 1'b0,
    parameter bit TxReadyStable = 1'b0,
    parameter bit Rx = 1'b0,
    parameter bit RxReadyStable = 1'b0
) (
    input logic ready,
    input logic valid,
    input T     data,

    input logic clock,
    input logic reset
);

// Check either Tx or Rx interface, not both (or neither)
if (Tx == Rx) begin
    $fatal(1, "Expected Tx != Rx, got Tx=%0d Rx=%0d", Tx, Rx);
end

if (Tx) begin : gen_tx_checks
    `ASSUME(NeverReadyUnknown, !$isunknown(read_ready), clock, reset)
    `ASSERT(NeverValidUnknown, !$isunknown(read_valid), clock, reset)
    `ASSERT(NeverDataUnknown,
        read_valid |-> !$isunknown(read_data),
        clock, reset)
    if (TxReadyStable) begin : gen_ready_stable
        `ASSUME(ReadyStable,
            (read_ready && !read_valid) |=> read_ready,
            clock, reset)
    end : gen_ready_stable
    `ASSERT(ValidDataStable,
        (!read_ready && read_valid) |=> (read_valid && $stable(read_data)),
        clock, reset)
    `COVER(Transfer, read_ready && read_valid, clock, reset)
    `COVER(Backpressure, !read_ready && read_valid, clock, reset)
end : gen_tx_checks

if (Rx) begin : gen_rx_checks
    `ASSERT(NeverReadyUnknown, !$isunknown(write_ready), clock, reset)
    `ASSUME(NeverValidUnknown, !$isunknown(write_valid), clock, reset)
    `ASSUME(NeverDataUnknown,
        write_valid |-> !$isunknown(write_data),
        clock, reset)
    if (RxReadyStable) begin : gen_ready_stable
        `ASSERT(ReadyStable,
            (write_ready && !write_valid) |=> write_ready,
            clock, reset)
    end : gen_ready_stable
    `ASSUME(ValidDataStable,
        (!write_ready && write_valid) |=> (write_valid && $stable(write_data)),
        clock, reset)
    `COVER(Transfer, write_ready && write_valid, clock, reset)
    `COVER(Backpressure, !write_ready && write_valid, clock, reset)
end : gen_rx_checks

endmodule : CheckReadyValid

Conclusion

The ready/valid protocol is a fundamental tool in the logic designer's toolbox. A modern CPU or GPU performs billions of ready/valid data transfers per second. This simple, two-wire handshake is at the heart of computer science and engineering. We must carefully study, master, and standardize every aspect of it.

Whether you are integrating an IP core into a bleeding edge SoC with a sophisticated AMBA on-chip interconnect, or just making the LEDs blink on your weekend FPGA side project, I sincerely hope this ready/valid primer using SystemVerilog will come in handy.