Two blocks at opposite corners of a die are joined by a valid/ready channel. Timing fails twice: the data arrives too late at the receiver, and the ready signal arrives too late back at the sender. The integrator is told to "add a register slice", and then asked which kind.
What exactly does a register slice cut, and why are there several kinds?
The obvious fix for a long path is to put a flip-flop in it. On the data and valid wires that is easy: delay them by a cycle and both sides still agree, because the slice simply becomes a one-entry stage. Ready is the problem. If you put a flip-flop in the ready wire, the sender learns about a stall one cycle late and sends one more word into a receiver that already said it cannot take it. A slice that registers ready must have somewhere to put that extra word.
Slice | Registers | Ready path | Storage | Throughput |
|---|---|---|---|---|
Forward | valid and data | Still combinational: in_ready = !full or out_ready | One entry | Full |
Backward | ready | Cut | Needs a spare entry for the word that arrives after a stall | Full, if it has the spare entry |
Full (both) | valid, data and ready | Cut | Two entries (a skid buffer) | Full |
Every slice adds one cycle of latency for data. What differs is which long path it removes and how much storage it needs. The forward slice is the cheapest and is what lesson 3.10 calls a good pipeline stage. The full slice is the skid buffer of lesson 3.9.
The listing registers valid and data and computes ready as !out_vld || out_rdy: the slice can take a new word if it is empty, or if its word is leaving this cycle. A source that always has data feeds it; the sink stalls in a fixed pattern. The checker confirms order and that a word leaves in every cycle the sink is ready.
// A forward register slice on a valid/ready channel: valid and data are registered
// (cutting the long forward path), while ready passes straight through as
// in_rdy = !out_vld || out_rdy. The slice can accept a new word in the same cycle it
// hands one on, so it adds one cycle of latency but loses no throughput. The source
// always has data; the sink stalls in a fixed pattern. The checker confirms order and
// that every cycle the sink is ready, a word moves out.
module fwd_slice (
input logic clk, rst_n,
input logic in_vld, output logic in_rdy, input logic [7:0] in_data,
output logic out_vld, input logic out_rdy, output logic [7:0] out_data
);
assign in_rdy = !out_vld || out_rdy; // combinational: the backward path is not cut
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) out_vld <= 0;
else if (in_rdy) begin
out_vld <= in_vld;
out_data <= in_data;
end
endmodule
module tb;
logic clk = 0;
always #5 clk = ~clk;
logic rst_n = 0;
initial begin
$dumpfile("wave.vcd"); $dumpvars(0, tb);
#12 rst_n = 1;
end
int cyc = 0;
always @(posedge clk) if (rst_n) cyc <= cyc + 1;
logic [7:0] next_word = 0;
wire in_vld = 1; // the source always has a word
wire in_rdy, out_vld;
wire [7:0] out_data;
localparam logic [15:0] RDY = 16'b1111_1001_1111_0111; // sink ready pattern, bit 0 first
wire out_rdy = RDY[cyc[3:0]];
fwd_slice u (.clk, .rst_n, .in_vld, .in_rdy, .in_data(next_word), .out_vld, .out_rdy, .out_data);
always @(posedge clk) if (rst_n && in_vld && in_rdy) next_word <= next_word + 1;
logic [7:0] expect_w = 0; int errors = 0, ready_cycles = 0, moved = 0;
always @(posedge clk) if (rst_n && cyc >= 1 && cyc < 16) begin
if (out_rdy) ready_cycles++;
if (out_vld && out_rdy) begin
if (out_data != expect_w) errors++;
expect_w++; moved++;
end
end
initial begin
#190;
if (errors == 0 && moved == ready_cycles)
$display("PASS: in order, and a word left in every one of the %0d cycles the sink was ready", ready_cycles);
else $display("FAIL: errors %0d, moved %0d of %0d ready cycles", errors, moved, ready_cycles);
$finish;
end
endmoduleHow to read it, slot by slot:
Slot 0: the slice is empty (out_vld = 0), so in_rdy is 1 and the first word enters.
From slot 1: the slice always holds a word, and out_data steps 0, 1, 2 on every cycle the sink is ready: full throughput.
Slots 3, 9 and 10: the sink stalls (out_rdy = 0). In the same cycles in_rdy drops to 0, and the word on the output is not taken, so it is still there in the next slot (2 in slots 3 and 4). That is the ready path passing straight through the slice: the forward slice cut the data path, not the ready path.
Only the data/valid path fails timing: forward slice. Cheapest, full throughput.
The ready path fails timing: a slice that registers ready, which means a skid buffer (two entries). Never just a flip-flop in the ready wire.
Both fail, or the route is very long: full slices, possibly several in a row; each adds a cycle of latency and, for the full kind, two entries of storage.
The route is extremely long: consider credits (Part 4) instead of a chain of slices.
Slice | Latency added | Storage | Paths cut |
|---|---|---|---|
None | 0 | 0 | None |
Forward | 1 cycle | 1 entry (data width) | valid and data |
Backward | 0 or 1 cycle depending on design | 1 spare entry | ready |
Full (skid buffer) | 1 cycle | 2 entries | valid, data and ready |
Where | Choice |
|---|---|
AXI interconnects and long on-chip routes | Register slices inserted per channel; each AXI channel has its own VALID/READY, so each can be sliced independently, and the handshake rules (VALID never waits for READY) keep the sliced channel deadlock-free. |
To fix a failing ready path, someone inserts a single flip-flop in the ready wire and nothing else (illustrative). Now the sender sees ready one cycle late. When the receiver stalls, the sender still believes it is ready and sends one more word, which the receiver cannot accept and the slice has nowhere to keep. One word per stall is lost, and only under back-pressure. The rule to remember: registering ready always needs a spare entry. That spare entry is the skid buffer, next lesson.
in_rdy drops in the same cycle as out_rdy (slots 3, 9 and 10): the stall passes straight through.
Its ready is !full || out_ready, so it accepts a new word in the same cycle its current word leaves.
Three cycles of latency and six entries of storage (two per slice).
A forward slice registers valid and data, keeps ready combinational, needs one entry and keeps full throughput.
Registering ready always needs a spare entry for the word sent after a stall: that is a skid buffer.
Every slice adds a cycle of latency; choose the cheapest one that cuts the path that actually fails.