Skip to content

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.

AXI4-Lite slave: AW and W feed a write engine into the register file; B returns the response; AR feeds a read engine that reads the register file and drives R; an address decoder selects registers.AW channelwrite addressWrite engineAW+W → BB channelwrite responseW channeldata + WSTRBRegister filedecoded by offsetAddress decodeselect / DECERRAR channelread addressRead engineAR → RR channelread data + resp12
Figure 1 — the structure of a minimal AXI4-Lite slave. The write path (AW + W) lands in the register file under WSTRB control and emits a B response; the read path (AR) reads the register file and drives R. An address decoder selects the target register and flags unmapped offsets. The two paths are independent — a read and a write can be in flight at once.

The port list is the AXI4-Lite subset — note there are no *LEN, *SIZE, *BURST, *ID, or *LAST signals:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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).

Write FSM: IDLE to WRITE when both AW and W captured, WRITE to RESP after commit, RESP to IDLE on BREADY.AW & W capturedcommit regBREADYIDLEWRITERESP
Figure 2 — the write FSM. IDLE accepts the write address and write data handshakes; once both AW and W have been captured, the FSM commits the WSTRB-masked data to the selected register (WRITE) and asserts BVALID with the decoded response (RESP), holding it until the manager accepts with BREADY, then returns to IDLE.

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
end

3. 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).

Read FSM: IDLE to RESP on ARVALID/ARREADY, RESP to IDLE on RREADY.AR handshakeRREADYIDLERESP
Figure 3 — the read FSM. IDLE accepts the read address handshake; the slave then decodes the offset, reads the selected register, and presents it on RDATA with RRESP while asserting RVALID (RESP). It holds the beat until the manager accepts with RREADY, then returns to IDLE.
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
end

4. 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
end

The two transactions — a write to CTRL followed by a read of STATUS — exercise both FSMs end to end:

AXI4-Lite write then read

12 cycles
Write address and data accepted, B response OKAY, then read address accepted and R data returned with OKAY.write addr+dataB = OKAYread addrR data + OKAYwrite commitswrite commitsread returnsread returnsACLKAWVALIDAWREADYWVALIDWREADYBVALIDBRESP...OK OK....ARVALIDARREADYRVALIDRDATA.......D D..RRESP.......OK OKt0t1t2t3t4t5t6t7t8t9t10t11
Figure 4 — a write followed by a read on the AXI4-Lite slave. The write address and data handshake (AWVALID/AWREADY, WVALID/WREADY), the slave commits the WSTRB-masked data and returns BVALID/BRESP=OKAY. Then the read address handshakes (ARVALID/ARREADY) and the slave returns the register value on RDATA with RVALID/RRESP=OKAY. Each transaction is a single beat with no burst signalling.

5. 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.