Volume 05 Advanced 5 sub-modules ~65 min read

AXI4, AXI4-Lite & AXI-Stream

AXI is the language every IP block on a modern SoC speaks. Its core is two wires and four rules, and the four rules are where custom IP goes wrong - because a violation does not produce wrong data, it produces a deadlock, and the interconnect is shared, so your broken block hangs the processor that was trying to read your status register.

5.1 The VALID/READY handshake

Every AXI channel - all five of them, plus AXI-Stream - uses the same two-wire handshake. The source drives VALID alongside its payload. The destination drives READY. A transfer happens on any rising clock edge where both are high. That is the entire mechanism.

The four rules, in the order they get broken
  1. A source must not wait for READY before asserting VALID. If it has data, it says so.
  2. A destination may wait for VALID before asserting READY. This asymmetry is what prevents deadlock.
  3. Once VALID is asserted it must stay asserted, with the payload unchanged, until the handshake completes.
  4. There must be no combinational path from an interface's inputs to its own outputs - no VALID derived from READY, and preferably no READY derived from VALID.
Handshake waveform showing three transfer cases: ready before valid, valid before ready, and both asserted simultaneously TRANSFER HAPPENS WHERE BOTH ARE HIGH clk VALID READY 1 2 3 READY waited VALID waited both at once source dest
Figure 5.1 - All three orderings are legal and all three transfer exactly one beat. What is illegal is VALID dropping before the handshake, or the payload changing while VALID is high.

Registering the interface: the skid buffer

Rule 4 forces a real design decision. You want registered outputs - a long combinational READY chain across six pipeline blocks is a guaranteed timing failure - but the naive way to register a handshake halves your throughput.

Approach Throughput Timing Cost
Pass READY straight through 100% Combinational path grows with every stage 0
Register data, pass READY through 100% Still a long READY chain W flops
Register both, no buffer 50% - one bubble per beat Clean W + 1 flops
Skid buffer 100% Clean, fully registered both directions 2W + 2 flops

// Two-entry skid buffer. Fully registered in both directions and still
// sustains one transfer per cycle. This module is the single most
// reused block in any AXI design - write it once, instantiate it
// everywhere a timing path needs breaking.
module axis_skid #(parameter int W = 32) (
  input  logic         clk,
  input  logic         rstn,

  input  logic [W-1:0] s_tdata,
  input  logic         s_tvalid,
  output logic         s_tready,

  output logic [W-1:0] m_tdata,
  output logic         m_tvalid,
  input  logic         m_tready
);

  logic [W-1:0] skid_data;
  logic         skid_valid;

  // We can accept new data whenever the overflow slot is free. Note this
  // does NOT look at m_tready - that is what makes the path registered.
  assign s_tready = !skid_valid;

  always_ff @(posedge clk) begin
    if (!rstn) begin
      m_tvalid   <= 1'b0;
      skid_valid <= 1'b0;
    end
    // ---- output register is free to advance ---------------------------
    else if (!m_tvalid || m_tready) begin
      if (skid_valid) begin
        m_tdata    <= skid_data;    // drain the overflow slot first
        m_tvalid   <= 1'b1;
        skid_valid <= 1'b0;
      end else begin
        m_tdata  <= s_tdata;        // straight through
        m_tvalid <= s_tvalid;
      end
    end
    // ---- output stalled: catch whatever we already accepted -----------
    else if (s_tvalid && s_tready) begin
      skid_data  <= s_tdata;
      skid_valid <= 1'b1;
    end
  end

endmodule
The deadlock you will actually hit The classic bug is a write-response state machine that waits for BREADY before accepting the next AWVALID, combined with a master that will not assert BREADY until it has issued the next address. Neither side is obviously wrong in isolation; together they lock solid. This is why the rule is asymmetric - the source is never permitted to wait. When you review AXI RTL, the first thing to search for is any *VALID assignment that mentions *READY.

5.2 AXI4-Lite and the five channels

A full AXI interface is five independent channels, each with its own VALID/READY pair. They are genuinely independent: a write address can be issued long before the corresponding write data, and a read can overtake a write entirely.

The five AXI channels between master and slave: write address, write data and write response flowing one way, read address and read data the other MASTER CPU / DMA SLAVE your IP AW write address W write data + WSTRB B write response AR read address R read data + RRESP FIVE INDEPENDENT CHANNELS, ONE HANDSHAKE EACH independent means a read may complete before a write that was issued first
Figure 5.2 - Write and read are entirely separate paths. The only ordering the protocol guarantees is that a write response follows its own write data, and that responses with the same ID stay in order.
AXI4-Lite AXI4 (full) AXI4-Stream
Channels 5 5 1
Addresses Yes Yes None
Bursts No - one beat per transaction 1-256 beats Unbounded, framed by TLAST
Data width 32 or 64 only 8-1024 Any multiple of 8
Outstanding transactions Typically 1 Many, tracked by ID N/A
Use for Control and status registers Memory-mapped bulk transfer Continuous data: video, ADC, packets
Choosing, in one sentence each If a CPU needs to poke a register, use AXI4-Lite. If a block needs to move megabytes to and from DDR, use AXI4 with bursts. If data flows continuously in one direction and nobody addresses anything, use AXI4-Stream. Most real accelerators use two of the three: Lite for control, Stream for data.

5.3 Building a compliant AXI4-Lite slave

Vivado's IP packager will generate an AXI4-Lite slave template for you. It works, and it is four hundred lines of generated code that nobody on your team understands. Here is the whole thing in about eighty, written so every line has a reason.

The design is a four-register control block: two read/write registers, one write-only command register that self-clears, and one read-only status register.


module axil_regs (
  input  logic        clk,
  input  logic        rstn,

  // ---- write address channel ----
  input  logic [ 3:0] s_awaddr,
  input  logic        s_awvalid,
  output logic        s_awready,

  // ---- write data channel ----
  input  logic [31:0] s_wdata,
  input  logic [ 3:0] s_wstrb,
  input  logic        s_wvalid,
  output logic        s_wready,

  // ---- write response channel ----
  output logic [ 1:0] s_bresp,
  output logic        s_bvalid,
  input  logic        s_bready,

  // ---- read address channel ----
  input  logic [ 3:0] s_araddr,
  input  logic        s_arvalid,
  output logic        s_arready,

  // ---- read data channel ----
  output logic [31:0] s_rdata,
  output logic [ 1:0] s_rresp,
  output logic        s_rvalid,
  input  logic        s_rready,

  // ---- to the rest of the design ----
  output logic [31:0] cfg_a,
  output logic [31:0] cfg_b,
  output logic        start_pulse,
  input  logic [31:0] status
);

  localparam logic [1:0] RESP_OKAY = 2'b00;
  localparam logic [1:0] RESP_SLVERR = 2'b10;

  // Byte address 0x0, 0x4, 0x8, 0xC -> word index [3:2]
  logic [1:0] aw_word, ar_word;

  logic        aw_done, w_done;
  logic [ 3:0] awaddr_q;
  logic [31:0] wdata_q;
  logic [ 3:0] wstrb_q;

  assign aw_word = awaddr_q[3:2];
  assign ar_word = s_araddr[3:2];

  // RULE 2 in action: ready depends only on our own state, never on the
  // incoming valid. No combinational path from input to output.
  assign s_awready = !aw_done && !s_bvalid;
  assign s_wready  = !w_done  && !s_bvalid;
  assign s_arready = !s_rvalid;

  // ---- capture address and data on their own handshakes ---------------
  always_ff @(posedge clk) begin
    if (s_awvalid && s_awready) awaddr_q <= s_awaddr;
    if (s_wvalid  && s_wready ) begin
      wdata_q <= s_wdata;
      wstrb_q <= s_wstrb;
    end
  end

  // ---- write commit and response --------------------------------------
  always_ff @(posedge clk) begin
    if (!rstn) begin
      aw_done     <= 1'b0;
      w_done      <= 1'b0;
      s_bvalid    <= 1'b0;
      s_bresp     <= RESP_OKAY;
      cfg_a       <= 32'd0;
      cfg_b       <= 32'd0;
      start_pulse <= 1'b0;
    end else begin
      start_pulse <= 1'b0;              // one cycle wide, always

      if (s_awvalid && s_awready) aw_done <= 1'b1;
      if (s_wvalid  && s_wready ) w_done  <= 1'b1;

      // Both halves have arrived and no response is outstanding: commit.
      if (aw_done && w_done && !s_bvalid) begin
        s_bvalid <= 1'b1;
        s_bresp  <= RESP_OKAY;

        unique case (aw_word)
          2'd0: for (int i = 0; i < 4; i++)
                  if (wstrb_q[i]) cfg_a[i*8 +: 8] <= wdata_q[i*8 +: 8];
          2'd1: for (int i = 0; i < 4; i++)
                  if (wstrb_q[i]) cfg_b[i*8 +: 8] <= wdata_q[i*8 +: 8];
          2'd2: start_pulse <= wdata_q[0];   // command: self-clearing
          2'd3: s_bresp <= RESP_SLVERR;      // status is read-only
        endcase
      end

      // Response accepted: clear everything for the next transaction.
      if (s_bvalid && s_bready) begin
        s_bvalid <= 1'b0;
        aw_done  <= 1'b0;
        w_done   <= 1'b0;
      end
    end
  end

  // ---- read path -------------------------------------------------------
  always_ff @(posedge clk) begin
    if (!rstn) begin
      s_rvalid <= 1'b0;
      s_rresp  <= RESP_OKAY;
    end else if (s_arvalid && s_arready) begin
      s_rvalid <= 1'b1;
      s_rresp  <= RESP_OKAY;
      unique case (ar_word)
        2'd0: s_rdata <= cfg_a;
        2'd1: s_rdata <= cfg_b;
        2'd2: s_rdata <= 32'd0;          // command reads back as zero
        2'd3: s_rdata <= status;
      endcase
    end else if (s_rvalid && s_rready) begin
      s_rvalid <= 1'b0;
    end
  end

endmodule
Review checklist for any Lite slave
  • Does any *ready assignment mention the matching *valid? If yes, justify it or remove it.
  • Does every write produce exactly one BVALID? Two responses for one write corrupts the master's outstanding-transaction count.
  • Is an unmapped address answered with SLVERR rather than ignored? A silently dropped write means the CPU hangs waiting for BVALID.
  • Are WSTRB bits honoured? A byte-write to a 32-bit register that ignores the strobe corrupts the other three bytes, and drivers do use byte writes.
  • Does reset leave every *VALID low? A slave that comes out of reset with BVALID high desynchronises the master immediately.
Interview grilling - "The CPU hangs when it writes to your peripheral. Debug it."

A hung AXI write means the master is waiting for a response that never comes. Work backwards along the channels:

  • Is BVALID ever asserted? Probe it with an ILA. If not, the slave never reached its commit condition - usually because it is waiting for both AWVALID and WVALID in the same cycle, which the protocol does not promise.
  • Did the address decode? If the transaction never reached your block, the interconnect is waiting for a slave that does not exist and will eventually return DECERR - or hang, if the address map has a hole.
  • Is AWREADY stuck low? A slave that only asserts AWREADY when WVALID is already high deadlocks against a master that sends the address first. This is the single most common custom-IP bug.
  • Is reset released cleanly? AXI reset is active-low and must be deasserted synchronously to the AXI clock. A slave still in reset accepts nothing.

The tooling answer that earns credit: "I would drop an ILA on all five channels and trigger on awvalid && !awready persisting for more than a few hundred cycles. Vivado also ships an AXI protocol checker IP that flags the violation directly rather than making me infer it."

5.4 AXI4 bursts, IDs and ordering

Full AXI adds bursts, which is the entire reason it exists. One address phase followed by up to 256 data beats amortises the address overhead and lets a DDR controller open a row once and stream out of it.

Signal Meaning Gotcha
AWLEN Beats in the burst, minus one AWLEN = 0 is a single beat, not zero beats
AWSIZE Bytes per beat, as 2^AWSIZE A size smaller than the bus width is a narrow transfer and needs lane steering
AWBURST FIXED / INCR / WRAP WRAP length must be 2, 4, 8 or 16 - nothing else
AWID Transaction tag for reordering Responses may return out of order between IDs, never within one
WLAST Marks the final data beat Miscounting it desynchronises the slave for every later transaction
The 4KB rule is absolute
Burst legality start_addr[31:12] == end_addr[31:12]
A burst must never cross a 4KB address boundary, because 4KB is the smallest page granularity in the address map - a crossing burst could span two different slaves. A master issuing a 2KB transfer starting at 0x0000_0C00 must split it into two bursts. Interconnects do not fix this for you; a violating master produces a protocol error, and in some implementations, silently wrong data.

// Split a transfer at the next 4KB boundary. Every custom AXI master
// needs this, and forgetting it is the classic first-bring-up failure.
function automatic logic [8:0] beats_to_boundary
  (input logic [31:0] addr, input int bytes_per_beat, input int want_beats);

  logic [31:0] bytes_left;
  int          max_beats;

  // How many bytes remain in this 4KB page?
  bytes_left = 32'h1000 - {20'd0, addr[11:0]};
  max_beats  = bytes_left / bytes_per_beat;

  // AXI4 also caps a single burst at 256 beats.
  if (max_beats > 256)        max_beats = 256;
  if (want_beats < max_beats) max_beats = want_beats;

  return max_beats[8:0];
endfunction

// Usage: awlen = beats_to_boundary(addr, 8, remaining) - 1;

What IDs actually buy you

AWID and ARID let a master have several transactions in flight and receive their responses out of order. The ordering rules are precise:

Where the bandwidth actually goes A DDR3 controller has a row-activate penalty measured in tens of nanoseconds. Single-beat transactions pay it every time; a 16-beat burst pays it once and then streams. On a Zynq HP port at 64 bits and 150 MHz the theoretical ceiling is 1.2 GB/s, and single-beat traffic will get you roughly a tenth of that. If a DMA engine is underperforming, check AWLEN before you check anything else.

5.5 AXI4-Stream and DMA datapaths

AXI-Stream throws away addressing entirely. There is one channel, data flows one way, and the only structure is packet framing. For video, ADC samples, FFT results and network packets this is exactly right - none of them have addresses.

Signal Purpose Optional?
TDATA The payload Yes - a pure-TUSER sideband stream is legal
TVALID / TREADY The handshake TVALID never; TREADY may be omitted (no back-pressure)
TLAST Final beat of a packet or video line Yes, for endless streams
TKEEP / TSTRB Which byte lanes carry real data Yes - needed for non-multiple-of-width packets
TUSER Arbitrary sideband, e.g. start-of-frame Yes
TDEST / TID Routing through a stream switch Yes

// A minimal, correct AXI-Stream processing block: apply a gain and pass
// TLAST through. The whole art is the back-pressure handling.
module axis_gain (
  input  logic        clk,
  input  logic        rstn,
  input  logic [15:0] gain,        // from an AXI4-Lite register

  input  logic [31:0] s_tdata,
  input  logic        s_tvalid,
  input  logic        s_tlast,
  output logic        s_tready,

  output logic [31:0] m_tdata,
  output logic        m_tvalid,
  output logic        m_tlast,
  input  logic        m_tready
);

  // The single most important line in any stream block: accept input
  // only when the output can move. Everything else follows from it.
  assign s_tready = !m_tvalid || m_tready;

  always_ff @(posedge clk) begin
    if (!rstn) begin
      m_tvalid <= 1'b0;
    end else if (s_tready) begin
      m_tvalid <= s_tvalid;
      if (s_tvalid) begin
        // Sideband travels WITH the beat it belongs to. Registering
        // TLAST separately from TDATA is how frames get off by one.
        m_tdata <= (s_tdata * gain) >> 8;
        m_tlast <= s_tlast;
      end
    end
  end

endmodule
TLAST is not decoration A DMA engine in scatter-gather mode uses TLAST to decide where one packet ends and to write the completion descriptor. Assert it one beat early and every subsequent frame is misaligned; never assert it and the DMA waits forever for a packet that never ends, producing the classic "the transfer starts and then nothing happens" symptom. When a video pipeline shows a picture sheared diagonally, TLAST timing is the first suspect.

The standard accelerator shape

Almost every custom accelerator on a Zynq or MPSoC ends up with the same topology, and it is worth recognising because it tells you which volume solves which problem:

Width conversion is not free An AXI-Stream width converter from 32 to 128 bits packs four beats into one, which means the output stalls for three cycles out of four unless the input is four times faster. Getting this wrong produces a pipeline that runs at a quarter of the expected rate and looks, from the outside, like a slow accelerator. Draw the beats-per-second arithmetic at every width change before you build it.

Volume 05 recap

Concept The one thing to remember
Handshake Source never waits for READY. Destination may wait for VALID.
VALID stability Once high, it and the payload hold until the transfer.
Skid buffer Registers both directions at full throughput. Two entries.
Five channels Independent. A read can beat a write issued earlier.
Lite slave One BVALID per write. Honour WSTRB. Answer bad addresses.
Bursts AWLEN is beats - 1, and never cross 4KB.
Stream s_tready = !m_tvalid || m_tready. Carry TLAST with its beat.