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.
- A source must not wait for
READYbefore assertingVALID. If it has data, it says so. - A destination may wait for
VALIDbefore assertingREADY. This asymmetry is what prevents deadlock. - Once
VALIDis asserted it must stay asserted, with the payload unchanged, until the handshake completes. - There must be no combinational path from an interface's inputs to
its own outputs - no
VALIDderived fromREADY, and preferably noREADYderived fromVALID.
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
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.
| 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 |
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
- Does any
*readyassignment 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
SLVERRrather than ignored? A silently dropped write means the CPU hangs waiting forBVALID. - Are
WSTRBbits 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
*VALIDlow? A slave that comes out of reset withBVALIDhigh 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
BVALIDever asserted? Probe it with an ILA. If not, the slave never reached its commit condition - usually because it is waiting for bothAWVALIDandWVALIDin 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
AWREADYstuck low? A slave that only assertsAWREADYwhenWVALIDis 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 |
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:
- Transactions with the same ID complete in order. Always.
- Transactions with different IDs may complete in any order.
- Read data beats within one burst arrive in order, tagged with
RID. - A slave that never reorders can simply return the ID it was given and ignore the feature entirely - which is what almost every custom slave should do.
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 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:
- AXI4-Lite slave for control and status - §5.3 above.
- AXI4-Stream in and out for the data - §5.5.
- An AXI DMA or datamover converting between Stream and memory-mapped AXI4, so the processor sees ordinary buffers in DDR - Volume 06.
- An interrupt back to the CPU when a transfer completes - also Volume 06.
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. |