When a stream passes through many blocks, two problems appear: the logic between registers gets too long, and the TREADY signal ripples backwards through every block in a single cycle. The fix is to put registers in the stream. Doing that without halving the throughput is the trick: a simple register stage passes only one transfer every two cycles, while a skid buffer passes one every cycle with fully registered outputs. This article compares the two in a simulation and then adds a FIFO for absorbing longer stalls.

9 min read | Intermediate

What You'll Learn

  • Explain why a simple register stage halves stream throughput
  • Design a skid buffer with a registered TREADY
  • Use a FIFO to absorb stalls and bursts
  • Choose between a register slice, a skid buffer and a FIFO

The Simple Register Stage

The obvious way to register a stream is to store each transfer in an output register and accept a new one only when the register is empty:


module axis_reg_simple (
  input             aclk,
  input             aresetn,
  input      [31:0] s_tdata,
  input             s_tvalid,
  output            s_tready,
  output reg [31:0] m_tdata,
  output reg        m_tvalid,
  input             m_tready
);
  assign s_tready = !m_tvalid;

  always @(posedge aclk) begin
    if (!aresetn) begin
      m_tvalid <= 1'b0;
    end
    else begin
      if (m_tvalid && m_tready)
        m_tvalid <= 1'b0;
      if (s_tvalid && s_tready) begin
        m_tdata  <= s_tdata;
        m_tvalid <= 1'b1;
      end
    end
  end
endmodule

Its s_tready depends only on its own register, so there is no path from m_tready back to s_tready. But the register is full in every cycle in which it is sending, and a full register cannot accept. The stage alternates: accept, send, accept, send. Half the bandwidth is gone.

Making it full speed with s_tready = !m_tvalid || m_tready brings the combinational path back. The skid buffer gets both properties at once.

The Skid Buffer

A skid buffer has two registers. Normally data goes straight into the output register, one transfer per cycle, and s_tready stays high. When the output stalls, the upstream block does not find out until the next cycle, because s_tready is a register. The one transfer that arrives in that cycle "skids" into the second register. When the output starts moving again, the skid register is emptied first:


module axis_skid (
  input             aclk,
  input             aresetn,
  input      [31:0] s_tdata,
  input             s_tvalid,
  output reg        s_tready,
  output reg [31:0] m_tdata,
  output reg        m_tvalid,
  input             m_tready
);
  reg [31:0] skid_data;
  reg        skid_full;

  always @(posedge aclk) begin
    if (!aresetn) begin
      s_tready  <= 1'b1;
      m_tvalid  <= 1'b0;
      skid_full <= 1'b0;
    end
    else begin
      if (!m_tvalid || m_tready) begin
        // The output register is free (or being emptied): refill it
        if (skid_full) begin
          m_tdata   <= skid_data;
          m_tvalid  <= 1'b1;
          skid_full <= 1'b0;
          s_tready  <= 1'b1;
        end
        else begin
          m_tdata  <= s_tdata;
          m_tvalid <= s_tvalid && s_tready;
        end
      end
      else if (s_tvalid && s_tready) begin
        // The output is stalled and a beat arrives: park it in the skid register
        skid_data <= s_tdata;
        skid_full <= 1'b1;
        s_tready  <= 1'b0;
      end
    end
  end
endmodule

Every output of the skid buffer, including s_tready, comes straight from a register. That is why skid buffers, also called register slices, are placed between blocks, at the edges of IP and on long routes across the chip.

Quick Check

Q: Why does the skid buffer need exactly one extra register, not two or three?

Show Answer

Because s_tready is registered, the upstream block learns about a stall exactly one cycle late, so at most one transfer can arrive after the stall begins. One skid register holds it.

Side by Side

The testbench feeds both stages from sources that always have data, and connects both outputs to the same receiver, which stalls for two cycles:

Output
after 20 cycles: simple stage 9 beats, skid buffer 17 beats
last beats out:  simple stage 0x9, skid buffer 0x11

The simple stage delivers a transfer every other cycle. The skid buffer delivers one every cycle except during the stall. In the waveform, its s_tready falls one cycle into the stall, and skid_full shows the transfer that arrived in that cycle being parked and then sent first when the receiver is ready again. The sequence 0, 1, 2 ... on its output has no gaps and no repeats: nothing was lost.

FIFOs: Absorbing Longer Stalls

A skid buffer absorbs one transfer. When the producer and consumer have different burst patterns, for example a DMA that sends a burst of 16 words while the consumer takes one word every few cycles, a FIFO absorbs the difference:


module axis_fifo #(
  parameter DEPTH = 4,
  parameter AW    = 2
) (
  input             aclk,
  input             aresetn,
  input      [31:0] s_tdata,
  input             s_tvalid,
  output            s_tready,
  output     [31:0] m_tdata,
  output            m_tvalid,
  input             m_tready
);
  reg [31:0] mem [0:DEPTH-1];
  reg [AW:0] wr_ptr;
  reg [AW:0] rd_ptr;

  wire [AW:0] count = wr_ptr - rd_ptr;

  assign s_tready = (count != DEPTH);
  assign m_tvalid = (count != 0);
  assign m_tdata  = mem[rd_ptr[AW-1:0]];

  always @(posedge aclk) begin
    if (!aresetn) begin
      wr_ptr <= 0;
      rd_ptr <= 0;
    end
    else begin
      if (s_tvalid && s_tready) begin
        mem[wr_ptr[AW-1:0]] <= s_tdata;
        wr_ptr <= wr_ptr + 1'b1;
      end
      if (m_tvalid && m_tready)
        rd_ptr <= rd_ptr + 1'b1;
    end
  end
endmodule

The pointers are one bit wider than the address, so a full FIFO (count = DEPTH) and an empty one (count = 0) look different. s_tready and m_tvalid depend only on the pointers, so there is no combinational path from one side to the other.

Quick Check

Q: A FIFO's m_tdata comes from the memory addressed by the read pointer, without an output register. Is m_tdata stable while the receiver stalls?

Show Answer

Yes. While m_tready is low, the read pointer does not move, and writes go to other entries, so the addressed entry does not change.

Choosing

  • Simple register stage: only where half bandwidth is acceptable, such as a slow control stream.
  • Skid buffer: to break timing paths on both data and TREADY at full throughput. One per long route or IP boundary.
  • FIFO: to absorb bursts and stalls between blocks with different rhythms, or to cross clock domains with an asynchronous FIFO.

Running It in ChipVerify Lab

The example runs the simple stage and the skid buffer side by side. Change the stall pattern and compare the beat counts.

Common Mistakes

Mistake #1: Assuming a register stage is free

A register stage that accepts only when empty halves the throughput of the whole stream. Use a skid buffer when the stream must run at full rate.

Mistake #2: A skid buffer that forgets its skid register

When the output frees up, the parked transfer must go first. Taking the new input first reorders the stream.

Mistake #3: Telling full from empty with equal pointers

With pointers exactly as wide as the address, a full FIFO and an empty FIFO have equal pointers. Use one extra pointer bit, or a separate count.

Practice Exercise

Challenge: A FIFO in Place of the Skid Buffer

Replace the skid buffer in the testbench with a 4-entry FIFO, and make the receiver stall for six cycles instead of two. How many beats does each stage deliver in 20 cycles, and when does the FIFO's s_tready fall?

Hint

The FIFO keeps accepting during the stall until all four entries are full.

Solution
Output
after 20 cycles: simple stage 7 beats, FIFO 13 beats
last beats out:  simple stage 0x7, FIFO 0xd

The FIFO keeps s_tready high for three cycles of the stall while it fills, then holds it low until the receiver starts taking data again. It delivers 13 beats against 7 for the simple stage. After the stall the four stored words go out first, so the source restarts without losing anything.

Open the ready-made example in the lab and run the simulation.

Summary

  • A register stage that accepts only when empty passes one transfer every two cycles.
  • A skid buffer adds one skid register so that every output, including TREADY, is registered at full throughput.
  • The skid register holds the single transfer that arrives in the cycle a stall is noticed, and empties first.
  • A FIFO absorbs longer stalls and bursts; one extra pointer bit tells full from empty.
  • Use skid buffers to break timing paths, FIFOs to decouple blocks with different rhythms.
Did you find what you were looking for?

Quiz

No quiz questions available for this article.