AXI-Stream interfaces

FPGA design · Interfaces · Jul 2026

Modules have to talk to each other, and a design falls apart quickly if every pair invents its own way to do it. Modern FPGA designs settle on a small handshake called AXI-Stream, the streaming member of the Advanced eXtensible Interface family, and once it is second nature the blocks in a system snap together. The whole protocol for moving a stream of data is two signals plus the data, and understanding the one rule that governs them is most of what there is to know.

The valid and ready handshake

A stream flows from a source to a sink over three things, the data itself, a valid signal the source raises when the data is real, and a ready signal the sink raises when it can accept. A transfer happens on exactly the cycles where both valid and ready are high on the same edge. When the source has nothing it drops valid. When the sink is busy it drops ready, which is called backpressure and makes the source wait. This single mechanism lets fast and slow blocks connect without either overrunning the other, and it is why a whole pipeline can be assembled from parts that each run at their own pace.

There is one rule that keeps the handshake lossless, and violating it is the classic AXI-Stream bug. Once a source raises valid it must hold both valid and the data steady until it sees ready, so it may not offer a word, get stalled, and then change its mind or swap the data. A transfer that is offered must remain on the table until it is taken. Every correct AXI-Stream block obeys this, and every stream utility exists to obey it while doing something useful in between.

The skid buffer, pipelining a stream

The problem a register slice solves is small but everywhere. To hit a high clock frequency you want to register the signals crossing between two blocks, cutting the long path between them. Registering the data and valid going forward is easy. The trouble is ready, which flows backward, so registering it adds a cycle of delay before the source hears that the sink has stalled, and in that cycle the source may already have sent a word that now has nowhere to go. A skid buffer solves this by holding one extra register, the skid, that catches exactly that in-flight word so nothing is lost, while still registering the outputs and keeping full throughput when the stream runs freely.

axis_skid_buffer.svmodule axis_skid_buffer #(
    parameter int W = 8
)(
    input  logic         clk,
    input  logic         rst,
    input  logic [W-1:0] s_data,    // upstream (slave) port
    input  logic         s_valid,
    output logic         s_ready,
    output logic [W-1:0] m_data,    // downstream (master) port
    output logic         m_valid,
    input  logic         m_ready
);
    logic [W-1:0] skid_data;
    logic         skid_valid;
    logic [W-1:0] m_data_r;
    logic         m_valid_r;

    assign s_ready = !skid_valid;              // room while the skid is empty
    assign m_valid = m_valid_r;
    assign m_data  = m_data_r;

    always_ff @(posedge clk) begin
        if (rst) begin
            m_valid_r <= 1'b0; skid_valid <= 1'b0;
        end else if (m_ready || !m_valid_r) begin
            // output slot is free to advance, take the skid first if it holds a beat
            if (skid_valid) begin
                m_data_r <= skid_data; m_valid_r <= 1'b1; skid_valid <= 1'b0;
            end else if (s_valid && s_ready) begin
                m_data_r <= s_data; m_valid_r <= 1'b1;
            end else begin
                m_valid_r <= 1'b0;
            end
        end else if (s_valid && s_ready) begin
            // output is stalled, park the incoming beat in the skid register
            skid_data <= s_data; skid_valid <= 1'b1;
        end
    end
endmodule

The design accepts an input word as long as the skid is empty, which is what s_ready = !skid_valid says. When the downstream sink stalls with a word already at the output, one more word can still arrive, and it goes into the skid rather than being dropped. When the output frees up, the skid feeds it before any new input, preserving order. The testbench in the repository drives the input with random gaps and the output with random stalls, two thousand beats in all, and checks every word comes out exactly once and in order under every backpressure pattern. This little block is the glue that lets a design register its interfaces freely without ever thinking about losing data.

With a compute tile, memory, pipelining, and a way to connect blocks, the pieces are in place to build something whole. The next page is the capstone, a matrix-multiply engine assembled from what came before.

Next, a matrix-multiply accelerator.