Ready/Valid Pipeline: Stream (Skid) Buffer
Ready/Valid pipelined interfaces are one of the most widely designed and implemented logic in ASICs and FPGAs. Getting the pipelining fundamentals right is important and critical for interface designs. Let's take a detailed look into it. Here, a Valid indicates the sender has valid data to send and Ready indicates the receiver is ready to receive new data. This is very typical of interfaces for example AXI4 follows the ready/valid handshaking for data transfers between the sender and receiver.
The figure shows the pipelined approach to breaking the long combinational ready signal path. When the receiver is not ready to receive the incoming valid data it will pull the i_ready to low, indicating stall.
As the o_ready going to the sender is registered, o_ready will not see the stall and pull its line low until the next clock cycle. During which if the data was valid needs to be stored in a buffer so that it won't get lost/overwritten. That's the idea behind ready/valid buffered implementation. Here, the input channel signals are registered (o_ready) and the output channel signals are combinational (o_valid, o_data).
Implementation of Buffered Handshake
Let's find an approach to how such logic can be implemented based on the operation.
The incoming o_ready signal to the upstream channel is registered. Meaning, if the downstream channel stalls, !i_ready, then upstream will see the stalled signal in the next clock cycle. During this time, from downstream channel stall and when upstream channel sees the stall, if the valid incoming data is not correctly handled may end up getting overwritten or dropped. That is the essence of a ready/valid pipeline.
Anytime i_ready is low and i_valid is high will imply we have valid data to send but the downstream channel is stalled. This is the state where we want to save the valid data in a buffer. As the upstream channel thinks at this time that the downstream channel can accept this data.
We only want to stall the upstream channel only if the downstream channel is stalled and there is already data in the buffer.
If the buffer is valid we set the o_data using this else we assign i_data. This is implemented using a MUX.
o_valid is any time incoming data is valid or if there is valid data in the buffer.
Now let's capture the above logic description in RTL using SystemVerilog. First, lets set the o_ready as a registered signal.
Here, the o_ready should only go low if i_ready drops and there is data in the buffer. That is captured by the boolean logic via (i_ready || !o_valid). Remember, o_valid is high will imply we have valid data in the buffer and o_ready needs to be set low.
always_ff @(posedge clk, negedge rst_n)
if (!rst_n)
o_ready <= 1'b1;
else
o_ready <= i_ready || !o_valid;
i_data needs to be registered any time we have stalled downstream channel and valid data at the input. This logic can be captured using the condition as described in the HDL logic. We use an internal register to buffer i_data. Later on, we can select between this buffered data and i_data for assigning the o_data.
always_ff @(posedge clk, negedge rst_n)
if (!rst_n)
buffer_data <= '0;
else if (!i_ready && o_ready && i_valid)
buffer_data <= i_data;
o_valid should be high anytime we have valid data in the buffer or if we have valid data coming in. For setting the o_data we implement a MUX and select between the buffered data and the i_data based as described previously.
assign o_valid = i_valid || !o_ready;
assign o_data = !o_ready ? buffer_data : i_data;