Skip to content
VLSI

How to Design Safe Handshake Logic Between RTL Modules

  • 10 min read

Modern RTL designs rarely consist of one block doing everything. A typical SoC or digital system is divided into multiple modules, with each block responsible for a specific operation. One module may generate data, another may process it, and a third may store or forward the result.

The difficult part is not simply connecting these modules with wires. The receiving module may be busy when data arrives, while the transmitting module may not have data available on every clock cycle. Without proper control logic, this can lead to lost data, duplicated transactions, overwritten values, or modules waiting indefinitely for each other.

This is where handshake logic becomes important.

A handshake provides an agreed mechanism for two RTL modules to communicate. The most common approach for synchronous data transfer is the ready-valid handshake, where the source indicates that data is available using valid and the destination indicates that it can accept the data using ready. A transfer takes place when both signals are high at the active clock edge.

For some control or clock-domain-crossing applications, a request-acknowledge handshake is more appropriate.

The important point is that a handshake is not just two control signals. It is a protocol with rules that both sides must follow.

Why Is Handshake Logic Needed in RTL?

Consider two synchronous modules:

       Data

Module A ───────────────► Module B

        ────────────────

          Control

Module A produces data, while Module B consumes it.

Suppose Module A produces a new value every clock cycle, but Module B needs two cycles to process each value.

If Module A continues sending data without knowing whether Module B can accept it, some values may be overwritten or lost.

A handshake solves this problem by allowing the receiver to apply backpressure.

The source effectively says:

"I have valid data available."

The destination responds:

"I am ready to accept it."

Only when both conditions are true does the transfer occur.

This approach allows independently designed RTL modules to communicate without assuming that both sides operate at exactly the same processing rate.

Ready-Valid Handshake: The Most Common RTL Approach

A basic ready-valid interface contains three signals:

valid

ready

data

The source drives valid and data.

The destination drives ready.

The fundamental transfer condition is:

assign transfer = valid && ready;

At a rising clock edge:

valid = 0, ready = 0 → No transfer

valid = 0, ready = 1 → No transfer

valid = 1, ready = 0 → No transfer

valid = 1, ready = 1 → Transfer

This behavior is also used extensively in industry-standard streaming interfaces. AMD documentation, for example, defines a valid transfer when READY and VALID are asserted at the rising clock edge.

A useful way to remember the protocol is:

VALID means "the source has something to transfer."

READY means "the destination can accept it."

VALID && READY means "the transfer happens now."

The Source Must Hold Data When the Receiver Is Not Ready

One of the most important rules is what happens when:

valid = 1

ready = 0

The source cannot assume that the transaction has completed.

Instead, it must keep the transaction available until the destination accepts it.

For example:

always_ff @(posedge clk) begin

    if (reset) begin

        valid <= 1'b0;

        data  <= '0;

    end

    else if (valid && ready) begin

        valid <= 1'b0;

    end

    else if (new_data) begin

        data  <= input_data;

        valid <= 1'b1;

    end

end

The exact implementation will depend on the surrounding architecture, but the principle is critical:

Once a source presents a valid transaction, it must not change the transaction simply because the destination is temporarily unable to accept it.

Ready/valid references similarly emphasize that data and valid must remain stable while the source is waiting for ready.

This prevents the receiver from sampling one value while the source has already moved to another.

A Simple RTL Handshake Example

Suppose one module sends an 8-bit value to another module.

The interface could be:

module producer (

    input  logic       clk,

    input  logic       rst_n,

    input  logic       start,

    input  logic [7:0] input_data,

    output logic       valid,

    output logic [7:0] data,

    input  logic       ready

);

The basic transfer event is:

wire transfer;

assign transfer = valid && ready;

The producer can capture the data and assert valid:

always_ff @(posedge clk or negedge rst_n) begin

    if (!rst_n) begin

        valid <= 1'b0;

        data  <= 8'd0;

    end

    else begin

        if (start && !valid) begin

            data  <= input_data;

            valid <= 1'b1;

        end

        else if (transfer) begin

            valid <= 1'b0;

        end

    end

end

The receiving module can indicate that it is able to accept data:

assign ready = !busy;

The actual implementation may be more complex, but the fundamental relationship remains the same.

Never Make VALID Wait for READY

One of the most common handshake mistakes is creating a dependency such as:

if (ready)

    valid = 1'b1;

This may appear logical:

"I will only say that I have data when the receiver says it is ready."

But it can create a deadlock.

Imagine the receiver follows the opposite rule:

"I will assert READY only after I see VALID."

Now:

Source waits for READY

        ↓

Destination waits for VALID

        ↓

Neither signal changes

        ↓

Deadlock

The source should therefore be able to assert valid independently when it has a transaction to send.

The destination can assert ready independently when it has capacity.

This independence is one of the fundamental rules of a robust ready-valid interface.

Avoid Combinational Loops Between Modules

Another important design problem occurs when valid and ready are allowed to depend directly on each other through combinational logic.

For example:

Module A

  VALID ─────► Module B

     ▲            │

     │            ▼

     └──── READY ─┘

If Module A's valid depends on Module B's ready, while Module B's ready depends on Module A's valid, a combinational loop can be created.

This can cause:

  • synthesis problems
  • timing problems
  • unstable behavior
  • difficult timing closure
  • unexpected simulation behavior

A safer architecture uses registered state or carefully structured control logic to break such paths. Ready/valid design references specifically identify combinational loops between source and destination control as a common pitfall.

This becomes particularly important when several ready-valid interfaces are connected together in a pipeline.

Backpressure Is Part of the Handshake

Backpressure is simply the receiver's ability to tell the sender:

"Do not send the next transaction yet."

For example:

Producer                 Consumer

VALID = 1  ───────────►

DATA       ───────────►

                         READY = 0

                         ↓

                      Consumer busy

The producer must wait.

Once the consumer becomes available:

VALID = 1

READY = 1

the transaction completes.

This mechanism allows modules with different processing rates to work together.

Backpressure is particularly important in:

  • pipelined RTL
  • packet processing
  • DMA engines
  • FIFOs
  • AXI-stream interfaces
  • accelerators
  • SoC interconnects
  • streaming datapaths

Modern RTL pipelines commonly use ready-valid handshaking to control whether a pipeline stage can accept and forward data.

Why a Buffer May Be Necessary

Suppose a source has valid data but the destination suddenly becomes unavailable.

If the source cannot hold the data internally, the design needs a storage element such as:

  • register
  • skid buffer
  • FIFO

A one-entry buffer can be enough for simple interfaces.

Conceptually:

Source

  │

  ▼

┌──────────────┐

│ Data Buffer  │

│ Valid State  │

└──────────────┘

  │

  ▼

Destination

The buffer stores the transaction while the downstream module is stalled.

This is one reason handshake-aware pipeline registers and skid buffers are common in high-performance RTL.

Request-Acknowledge Handshake

Ready-valid is not the only handshake method.

For control transfers, particularly when communicating between different clock domains, a request-acknowledge handshake may be more appropriate.

The basic sequence is:

Source                    Destination

REQ ───────────────────►

       Data transfer

ACK ◄───────────────────

The source asserts REQ and keeps the associated data stable.

The destination detects the request, captures the data, and returns ACK.

The source can then complete the transaction.

A request-acknowledge protocol is particularly useful when data transfers are relatively infrequent and the additional latency is acceptable. It can also be used as part of a clock-domain-crossing architecture, provided the handshake control signals are synchronized appropriately.

This is different from simply connecting a ready-valid interface across unrelated clocks.

Handshake Logic Across Clock Domains Needs Extra Care

A critical distinction is whether both RTL modules use the same clock.

For a same-clock design:

Module A ─────► Module B

       clk

a synchronous ready-valid handshake may be sufficient.

For different clocks:

Module A                 Module B

clk_A                    clk_B

   REQ ────────────────►

   

   ACK ◄────────────────

the control signals cross clock domains.

A raw asynchronous control signal should not simply be sampled by the other domain.

CDC techniques such as synchronizers, request-acknowledge protocols, toggle synchronizers, or asynchronous FIFOs may be appropriate depending on the data rate and transfer requirements.

For multi-bit data, the design must also ensure that the destination does not capture a changing or partially synchronized bus. In a request-acknowledge CDC design, one common strategy is for the source to hold the data stable for the duration of the handshake.

Common Handshake Design Mistakes

1. Dropping VALID Too Early

Incorrect behavior:

VALID = 1

READY = 0

VALID = 0  ← transaction disappears

The transaction has not completed.

The source should continue presenting it.

2. Changing DATA While VALID Is Waiting

If:

VALID = 1

READY = 0

the source should not replace the data with another transaction.

Otherwise, the receiver may capture the wrong value when READY eventually becomes high.

3. Making VALID Dependent on READY

This can create deadlock when the destination also waits for VALID.

4. Creating Combinational READY/VALID Loops

Always inspect the control path between connected modules.

5. Forgetting Backpressure

A source that continuously generates data must have a strategy for what happens when the destination stalls.

6. Treating a CDC Handshake Like a Same-Clock Handshake

A ready-valid interface does not automatically make crossing unrelated clock domains safe.

CDC synchronization requirements still apply.

7. Ignoring Reset Behavior

The interface should define what happens to valid, ready, pending data, and handshake state during reset.

Verifying Handshake Logic

Handshake logic should not be tested only with the ideal case where the receiver is always ready.

A good testbench should deliberately introduce stalls.

For example:

Test 1:

VALID = 1

READY = 1

→ Transfer should occur

Test 2:

VALID = 1

READY = 0

→ Data must remain stable

Test 3:

VALID = 0

READY = 1

→ No transfer

Test 4:

READY changes from 0 to 1

→ Pending transaction should transfer

Test 5:

Receiver repeatedly stalls

→ No transaction should be lost

SystemVerilog Assertions can make these protocol rules easier to verify. For example, a useful property is that when valid is asserted while ready is low, the source continues to hold valid and the data stable until the transfer occurs. Such protocol assertions are commonly used to catch handshake violations.

A conceptual assertion is:

property hold_valid_data;

    @(posedge clk)

    valid && !ready |=> valid && $stable(data);

endproperty

The exact assertion should be adapted to the interface specification and reset behavior.

Ready-Valid vs Request-Acknowledge

The two approaches solve related but different problems.

Feature

Ready-Valid

Request-Acknowledge

Common use

Streaming data

Control/data transaction

Typical signals

valid, ready, data

req, ack, data

Same-clock use

Very common

Common

Backpressure

Natural

Usually explicit

High throughput

Excellent

Lower

CDC use

Requires CDC architecture

Common for low-rate CDC

Typical applications

Pipelines, streams, AXI

Control transfers, CDC

The protocol should therefore be selected based on the architecture rather than simply choosing whichever interface has fewer signals.

Final Thoughts

Safe handshake logic is one of those RTL topics that looks simple until a design has to deal with stalls, backpressure, buffering, multiple pipeline stages, or different clock domains.

The fundamental ready-valid rule is straightforward:

A transaction occurs when VALID and READY are both high on the active clock edge.

The challenge is maintaining the rules around that transaction.

The source must not lose or change a pending transaction simply because the destination is busy. VALID should not wait for READY, and the connected modules should not create combinational loops through their handshake signals. For higher-throughput designs, buffering and carefully designed pipeline stages may be necessary. For clock-domain crossings, additional synchronization techniques are required.

Once these principles are understood, handshake logic becomes a reusable design pattern rather than a collection of ad-hoc control signals.

For RTL engineers, mastering handshake protocols is especially valuable because the same ideas appear repeatedly in pipelines, FIFOs, streaming interfaces, SoC integration, AXI-based systems, and CDC architectures. Ready-valid handshaking is also a common concept in RTL design interviews, making it a useful topic to understand from both a design and verification perspective.

Share LinkedIn X WhatsApp