AMBA AXI · Module 15
A Simple AXI4-Lite Slave
Build a minimal, correct AXI4-Lite slave from scratch — the five-channel interface, a clean write FSM (AW/W → B) and read FSM (AR → R), WSTRB-aware register writes, address decode with OKAY/SLVERR/DECERR, and the handshake rules that keep it deadlock-free.
Module 15 turns from concepts to construction. We begin with the smallest useful, fully correct AXI4-Lite slave: a handful of memory-mapped registers reachable over the AXI4-Lite subset. This is the canonical first piece of AXI RTL every engineer writes — a peripheral's control/status interface. We'll build it the way you should in real RTL: a clear write datapath (AW + W → B) and read datapath (AR → R), each driven by a tiny FSM, with WSTRB-aware writes, a clean address decode that produces OKAY/SLVERR/DECERR, and handshakes that obey the one rule that prevents deadlock. The goal is not cleverness — it's a slave you can trust and reuse.
1. The Slave's Interface and Structure
An AXI4-Lite slave exposes the five channels — write address (AW), write data (W), write response (B), read address (AR), read data (R) — to a small block of registers. Because Lite is single-beat with no IDs, no bursts, and fixed full-width data, the slave is mostly a pair of independent handshake engines wrapped around a register file and an address decoder.
The port list is the AXI4-Lite subset — note there are no *LEN, *SIZE, *BURST, *ID, or *LAST signals:
module axil_slave #(
parameter int ADDR_W = 8, // byte address width into this slave
parameter int DATA_W = 32 // 32 or 64 for AXI4-Lite
) (
input logic aclk,
input logic aresetn, // active-low
// Write address channel
input logic [ADDR_W-1:0] s_awaddr,
input logic s_awvalid,
output logic s_awready,
// Write data channel
input logic [DATA_W-1:0] s_wdata,
input logic [DATA_W/8-1: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 [ADDR_W-1:0] s_araddr,
input logic s_arvalid,
output logic s_arready,
// Read data channel
output logic [DATA_W-1:0] s_rdata,
output logic [1:0] s_rresp,
output logic s_rvalid,
input logic s_rready
);2. The Write FSM (AW + W → B)
The write side must collect both an address (AW) and a data beat (W) — which can arrive in either order or simultaneously — then write the register and emit exactly one response (B). A clean three-state FSM captures this: IDLE (accept AW and W), WRITE (commit to the register file), RESP (hold BVALID until BREADY).
The write logic accepts the two address/data handshakes, decodes the offset, and applies WSTRB byte-by-byte. Writing only the strobed bytes is what makes partial-word and byte-field writes correct:
typedef enum logic [1:0] { W_IDLE, W_WRITE, W_RESP } wstate_e;
wstate_e wstate;
logic [ADDR_W-1:0] awaddr_q;
logic aw_done, w_done;
logic [DATA_W-1:0] wdata_q;
logic [DATA_W/8-1:0] wstrb_q;
// Accept AW and W independently while in IDLE
assign s_awready = (wstate == W_IDLE) && !aw_done;
assign s_wready = (wstate == W_IDLE) && !w_done;
always_ff @(posedge aclk) begin
if (!aresetn) begin
wstate <= W_IDLE; aw_done <= 1'b0; w_done <= 1'b0;
s_bvalid <= 1'b0; s_bresp <= 2'b00;
end else begin
// Latch address/data as each handshake completes
if (s_awvalid && s_awready) begin awaddr_q <= s_awaddr; aw_done <= 1'b1; end
if (s_wvalid && s_wready ) begin wdata_q <= s_wdata; wstrb_q <= s_wstrb; w_done <= 1'b1; end
case (wstate)
W_IDLE: if ((aw_done || (s_awvalid && s_awready)) &&
(w_done || (s_wvalid && s_wready ))) wstate <= W_WRITE;
W_WRITE: begin
// Address decode + WSTRB-masked write happen here (see §4)
s_bresp <= decode_err ? 2'b11 /*DECERR*/ : 2'b00 /*OKAY*/;
s_bvalid <= 1'b1;
wstate <= W_RESP;
end
W_RESP: if (s_bvalid && s_bready) begin
s_bvalid <= 1'b0; aw_done <= 1'b0; w_done <= 1'b0;
wstate <= W_IDLE;
end
endcase
end
end3. The Read FSM (AR → R)
The read side is simpler — one address in, one data beat out. A two-state FSM suffices: IDLE (accept AR), RESP (drive RVALID with the looked-up data and response, hold until RREADY).
typedef enum logic { R_IDLE, R_RESP } rstate_e;
rstate_e rstate;
assign s_arready = (rstate == R_IDLE);
always_ff @(posedge aclk) begin
if (!aresetn) begin
rstate <= R_IDLE; s_rvalid <= 1'b0; s_rresp <= 2'b00;
end else begin
case (rstate)
R_IDLE: if (s_arvalid && s_arready) begin
// Decode s_araddr, read register into s_rdata (see §4)
s_rresp <= rdecode_err ? 2'b11 : 2'b00;
s_rvalid <= 1'b1;
rstate <= R_RESP;
end
R_RESP: if (s_rvalid && s_rready) begin
s_rvalid <= 1'b0;
rstate <= R_IDLE;
end
endcase
end
end4. Address Decode and WSTRB Writes
The decode maps a byte offset to a register and signals an error for unmapped offsets. A two-register example (CTRL at 0x0, STATUS at 0x4, read-only) shows the pattern, including the per-byte WSTRB masking that preserves un-strobed bytes:
logic [DATA_W-1:0] reg_ctrl; // RW
logic [DATA_W-1:0] reg_status; // RO (driven by hardware)
logic decode_err, rdecode_err;
// --- Write commit (inside W_WRITE), word offset = awaddr_q[ADDR_W-1:2] ---
always_comb decode_err = 1'b0;
always_ff @(posedge aclk) begin
if (aresetn && wstate == W_WRITE) begin
case (awaddr_q[ADDR_W-1:2])
'h0: for (int b = 0; b < DATA_W/8; b++)
if (wstrb_q[b]) reg_ctrl[b*8 +: 8] <= wdata_q[b*8 +: 8];
'h1: ; // STATUS is read-only: ignore the write data, respond per policy
default: decode_err <= 1'b1; // unmapped → DECERR
endcase
end
end
// --- Read mux (registered into s_rdata at AR handshake) ---
always_ff @(posedge aclk) begin
if (aresetn && rstate == R_IDLE && s_arvalid && s_arready) begin
rdecode_err <= 1'b0;
case (s_araddr[ADDR_W-1:2])
'h0: s_rdata <= reg_ctrl;
'h1: s_rdata <= reg_status;
default: begin s_rdata <= '0; rdecode_err <= 1'b1; end // DECERR
endcase
end
endThe two transactions — a write to CTRL followed by a read of STATUS — exercise both FSMs end to end:
AXI4-Lite write then read
12 cycles5. Common Misconceptions
6. Debugging Insight
7. Verification Insight
8. Interview Questions
9. Summary
A minimal AXI4-Lite slave is two small handshake engines wrapped around a register file. The write path collects an address (AW) and a data beat (W) — which may arrive in any order — using aw_done/w_done latches, commits the WSTRB-masked data to the decoded register, and emits exactly one B response (OKAY/DECERR), held until BREADY. The read path accepts an address (AR), registers the decoded register value onto RDATA with RRESP, and holds RVALID until RREADY. The address decode must be total — an explicit default returns DECERR for unmapped offsets — and writes must honor WSTRB byte-by-byte to preserve neighbors. The one non-negotiable handshake rule is that *VALID (and its payload) must be driven independently of *READY and held stable until accepted; violating it deadlocks the bus. Reads and writes are independent and run on separate FSMs.
Because the slave is single-beat with no bursts, IDs, or outstanding, it's verifiable with directed register tests (read-after-write, RO-write, WSTRB neighbor preservation, unmapped→DECERR, AW/W ordering) plus always-on handshake assertions. This skeleton — clean FSMs, total decode, WSTRB masking, independent handshakes — is the reusable foundation for everything in Module 15.
10. What Comes Next
You've built one slave with hand-coded registers; next we parameterise it into a reusable bank:
- 15.2 — AXI4-Lite Register Bank (coming next) — generalize the decode and register file into a parameterised, table-driven register bank you can drop into any peripheral.
Previous: 14.5 — CDC Handshake Safety. Related: 10.2 — Register Access with AXI4-Lite for the access-side view, 6.7 — Write Strobes (WSTRB) for byte-enable semantics, and 10.5 — AXI4-Lite Verification Checklist for how to verify what you just built.