AXI4-Lite Slave in Verilog: Write and Read Example

AXI4-Lite is the lightweight, register-mapped member of the AMBA AXI family. It drops bursts, transaction IDs, and narrow transfers, leaving a simple memory-mapped interface that is ideal for control and status registers inside an SoC. When you need to expose a handful of configuration registers to a CPU or a bus fabric, an AXI4-Lite slave is almost always the right building block. This tutorial walks through complete, runnable AXI lite slave Verilog code: the five channels, a four-word register bank, the VALID/READY handshake, and a self-checking testbench that performs one write followed by one read. Every listing compiles and simulates with Icarus Verilog, so you can paste it, run it, and read the transaction log yourself.

The five AXI4-Lite channels

AXI4-Lite splits a transaction across five independent channels. Writes use three of them, reads use the other two. Each channel is just a bundle of payload signals plus a VALID/READY pair that governs flow control.

ChannelDirectionKey signalsPurpose
Write address (AW)Master to slaveAWADDR, AWVALID, AWREADYWhere to write
Write data (W)Master to slaveWDATA, WSTRB, WVALID, WREADYWhat to write
Write response (B)Slave to masterBRESP, BVALID, BREADYAcknowledge the write
Read address (AR)Master to slaveARADDR, ARVALID, ARREADYWhere to read
Read data (R)Slave to masterRDATA, RRESP, RVALID, RREADYReturn the data

The VALID/READY handshake

The handshake is the heart of every AXI channel, and it is deliberately simple. The source raises VALID when it has payload to offer. The destination raises READY when it can accept. A transfer happens on exactly one rising clock edge: the edge where both VALID and READY are high at the same time. Either side may assert its signal first, and neither is allowed to retract VALID before the transfer completes. Because both signals are registered, there is no combinational path from one side’s VALID straight into the other side’s READY, which keeps timing clean and avoids loops.

Complete slave and testbench

Here is the full, synthesizable slave plus a testbench that resets the bus, writes 0xDEADBEEF to register 1, then reads it back and checks the value. The design accepts the write address and write data together, generates a single OKAY response, and returns registered read data. Run it with iverilog -g2005 -o sim axi_lite_slave.v && vvp sim.

// axi_lite_slave.v  -- AXI4-Lite slave with a 4-word register bank
module axi_lite_slave #(
    parameter ADDR_WIDTH = 4,   // 4 bits -> 16 bytes -> 4 words
    parameter DATA_WIDTH = 32
)(
    input  wire                     ACLK,
    input  wire                     ARESETN,   // active-low reset

    // Write address channel
    input  wire [ADDR_WIDTH-1:0]    AWADDR,
    input  wire                     AWVALID,
    output reg                      AWREADY,

    // Write data channel
    input  wire [DATA_WIDTH-1:0]    WDATA,
    input  wire [DATA_WIDTH/8-1:0]  WSTRB,
    input  wire                     WVALID,
    output reg                      WREADY,

    // Write response channel
    output reg  [1:0]               BRESP,
    output reg                      BVALID,
    input  wire                     BREADY,

    // Read address channel
    input  wire [ADDR_WIDTH-1:0]    ARADDR,
    input  wire                     ARVALID,
    output reg                      ARREADY,

    // Read data channel
    output reg  [DATA_WIDTH-1:0]    RDATA,
    output reg  [1:0]               RRESP,
    output reg                      RVALID,
    input  wire                     RREADY
);
    localparam OKAY = 2'b00;

    // Register bank: 4 x 32-bit
    reg [DATA_WIDTH-1:0] regbank [0:3];
    reg [ADDR_WIDTH-1:0] awaddr_q;   // latched write address
    reg [ADDR_WIDTH-1:0] araddr_q;   // latched read address
    integer i;

    // ---- Write address handshake ----
    always @(posedge ACLK) begin
        if (!ARESETN) begin
            AWREADY  <= 1'b0;
            awaddr_q <= {ADDR_WIDTH{1'b0}};
        end else if (!AWREADY && AWVALID && WVALID) begin
            AWREADY  <= 1'b1;        // accept address when both valids are up
            awaddr_q <= AWADDR;
        end else begin
            AWREADY  <= 1'b0;
        end
    end

    // ---- Write data handshake + register update ----
    always @(posedge ACLK) begin
        if (!ARESETN) begin
            WREADY <= 1'b0;
            for (i = 0; i < 4; i = i + 1)
                regbank[i] <= {DATA_WIDTH{1'b0}};
        end else if (!WREADY && WVALID && AWVALID) begin
            WREADY <= 1'b1;          // accept data in the same cycle as address
            regbank[AWADDR[3:2]] <= WDATA;
        end else begin
            WREADY <= 1'b0;
        end
    end

    // ---- Write response channel ----
    always @(posedge ACLK) begin
        if (!ARESETN) begin
            BVALID <= 1'b0;
            BRESP  <= OKAY;
        end else if (AWREADY && WREADY && !BVALID) begin
            BVALID <= 1'b1;          // one response per write
            BRESP  <= OKAY;
        end else if (BVALID && BREADY) begin
            BVALID <= 1'b0;          // master took the response
        end
    end

    // ---- Read address handshake (latch the address on accept) ----
    always @(posedge ACLK) begin
        if (!ARESETN) begin
            ARREADY  <= 1'b0;
            araddr_q <= {ADDR_WIDTH{1'b0}};
        end else if (!ARREADY && ARVALID && !RVALID) begin
            ARREADY  <= 1'b1;
            araddr_q <= ARADDR;
        end else begin
            ARREADY  <= 1'b0;
        end
    end

    // ---- Read data channel (keyed off ARREADY, not the master's ARVALID) ----
    always @(posedge ACLK) begin
        if (!ARESETN) begin
            RVALID <= 1'b0;
            RRESP  <= OKAY;
            RDATA  <= {DATA_WIDTH{1'b0}};
        end else if (ARREADY && !RVALID) begin
            RVALID <= 1'b1;
            RRESP  <= OKAY;
            RDATA  <= regbank[araddr_q[3:2]];
        end else if (RVALID && RREADY) begin
            RVALID <= 1'b0;          // master took the data
        end
    end
endmodule

// ---------------- Testbench: one write then one read ----------------
module tb_axi_lite;
    reg         ACLK, ARESETN;
    reg  [3:0]  AWADDR, ARADDR;
    reg  [31:0] WDATA;
    reg  [3:0]  WSTRB;
    reg         AWVALID, WVALID, BREADY, ARVALID, RREADY;
    wire        AWREADY, WREADY, BVALID, ARREADY, RVALID;
    wire [1:0]  BRESP, RRESP;
    wire [31:0] RDATA;

    axi_lite_slave dut (
        .ACLK(ACLK), .ARESETN(ARESETN),
        .AWADDR(AWADDR), .AWVALID(AWVALID), .AWREADY(AWREADY),
        .WDATA(WDATA), .WSTRB(WSTRB), .WVALID(WVALID), .WREADY(WREADY),
        .BRESP(BRESP), .BVALID(BVALID), .BREADY(BREADY),
        .ARADDR(ARADDR), .ARVALID(ARVALID), .ARREADY(ARREADY),
        .RDATA(RDATA), .RRESP(RRESP), .RVALID(RVALID), .RREADY(RREADY)
    );

    // 100 MHz clock (period = 10 time units)
    always #5 ACLK = ~ACLK;

    initial begin
        // init
        ACLK = 0; ARESETN = 0;
        AWADDR = 0; WDATA = 0; WSTRB = 4'hF;
        AWVALID = 0; WVALID = 0; BREADY = 0;
        ARADDR = 0; ARVALID = 0; RREADY = 0;

        // hold reset for 2 clocks
        repeat (2) @(posedge ACLK);
        ARESETN = 1;
        @(posedge ACLK);

        // ---- WRITE 32'hDEAD_BEEF to register 1 (byte addr 0x4) ----
        AWADDR  = 4'h4; WDATA = 32'hDEAD_BEEF;
        AWVALID = 1;    WVALID = 1; BREADY = 1;
        // wait until both address and data are accepted
        wait (AWREADY && WREADY); @(posedge ACLK);
        AWVALID = 0; WVALID = 0;
        // wait for the write response
        wait (BVALID); @(posedge ACLK);
        BREADY = 0;
        $display("[%0t] WRITE  addr=0x%0h data=0x%08h BRESP=%b", $time, 4'h4, 32'hDEAD_BEEF, BRESP);

        @(posedge ACLK);

        // ---- READ back register 1 ----
        ARADDR = 4'h4; ARVALID = 1; RREADY = 1;
        wait (ARREADY); @(posedge ACLK);
        ARVALID = 0;
        wait (RVALID);
        $display("[%0t] READ   addr=0x%0h data=0x%08h RRESP=%b", $time, 4'h4, RDATA, RRESP);
        if (RDATA === 32'hDEAD_BEEF)
            $display("RESULT: PASS - read value matches written value");
        else
            $display("RESULT: FAIL - expected 0xDEADBEEF got 0x%08h", RDATA);
        @(posedge ACLK);
        RREADY = 0;

        @(posedge ACLK);
        $finish;
    end
endmodule
[45] WRITE  addr=0x4 data=0xdeadbeef BRESP=00
[75] READ   addr=0x4 data=0xdeadbeef RRESP=00
RESULT: PASS - read value matches written value
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpempm143r/axi_lite_slave.v:175: $finish called at 95 (1s)

How the slave works

Accepting a write

The slave waits until both AWVALID and WVALID are high, then pulses AWREADY and WREADY together for one cycle and latches the data into the register selected by AWADDR[3:2]. Because there are four 32-bit registers spaced four bytes apart, address bits [3:2] form the word index while bits [1:0] are the byte offset within a word. One cycle later the response logic sees both ready pulses, raises BVALID with BRESP = OKAY, and clears it once the master asserts BREADY.

Serving a read

On the read side the slave accepts the address with a one-cycle ARREADY pulse and latches it into araddr_q. Notice that RVALID and RDATA are driven from the slave’s own ARREADY pulse, not from the master’s ARVALID. That decoupling is deliberate: it guarantees the address is already captured and keeps the read-data logic dependent only on the slave’s internal state, which prevents a subtle testbench race where the master lowers ARVALID on the very edge the slave samples it.

Byte strobes: writing a single byte

Real masters use WSTRB to write individual bytes without disturbing the rest of a word. Each strobe bit enables one 8-bit lane. The slave below honours the strobes; its testbench writes a full word, then overwrites only the lowest byte, and confirms the upper three bytes survive.

// axi_lite_slave_strb.v -- AXI4-Lite slave that honors WSTRB byte lanes
module axi_lite_slave_strb #(
    parameter ADDR_WIDTH = 4,
    parameter DATA_WIDTH = 32
)(
    input  wire                     ACLK,
    input  wire                     ARESETN,
    input  wire [ADDR_WIDTH-1:0]    AWADDR,
    input  wire                     AWVALID,
    output reg                      AWREADY,
    input  wire [DATA_WIDTH-1:0]    WDATA,
    input  wire [DATA_WIDTH/8-1:0]  WSTRB,
    input  wire                     WVALID,
    output reg                      WREADY,
    output reg  [1:0]               BRESP,
    output reg                      BVALID,
    input  wire                     BREADY,
    input  wire [ADDR_WIDTH-1:0]    ARADDR,
    input  wire                     ARVALID,
    output reg                      ARREADY,
    output reg  [DATA_WIDTH-1:0]    RDATA,
    output reg  [1:0]               RRESP,
    output reg                      RVALID,
    input  wire                     RREADY
);
    localparam OKAY = 2'b00;
    reg [DATA_WIDTH-1:0] regbank [0:3];
    reg [ADDR_WIDTH-1:0] araddr_q;
    integer i, b;

    // Write address + data accepted together
    always @(posedge ACLK) begin
        if (!ARESETN) AWREADY <= 1'b0;
        else if (!AWREADY && AWVALID && WVALID) AWREADY <= 1'b1;
        else AWREADY <= 1'b0;
    end

    always @(posedge ACLK) begin
        if (!ARESETN) begin
            WREADY <= 1'b0;
            for (i = 0; i < 4; i = i + 1) regbank[i] <= {DATA_WIDTH{1'b0}};
        end else if (!WREADY && WVALID && AWVALID) begin
            WREADY <= 1'b1;
            // Only update byte lanes whose strobe bit is set
            for (b = 0; b < DATA_WIDTH/8; b = b + 1)
                if (WSTRB[b])
                    regbank[AWADDR[3:2]][b*8 +: 8] <= WDATA[b*8 +: 8];
        end else begin
            WREADY <= 1'b0;
        end
    end

    always @(posedge ACLK) begin
        if (!ARESETN) begin BVALID <= 1'b0; BRESP <= OKAY; end
        else if (AWREADY && WREADY && !BVALID) begin BVALID <= 1'b1; BRESP <= OKAY; end
        else if (BVALID && BREADY) BVALID <= 1'b0;
    end

    always @(posedge ACLK) begin
        if (!ARESETN) begin ARREADY <= 1'b0; araddr_q <= 0; end
        else if (!ARREADY && ARVALID && !RVALID) begin ARREADY <= 1'b1; araddr_q <= ARADDR; end
        else ARREADY <= 1'b0;
    end

    always @(posedge ACLK) begin
        if (!ARESETN) begin RVALID <= 1'b0; RRESP <= OKAY; RDATA <= 0; end
        else if (ARREADY && !RVALID) begin
            RVALID <= 1'b1; RRESP <= OKAY; RDATA <= regbank[araddr_q[3:2]];
        end else if (RVALID && RREADY) RVALID <= 1'b0;
    end
endmodule

// ---------------- Testbench: full write, then a single-byte write ----------------
module tb_strb;
    reg         ACLK, ARESETN;
    reg  [3:0]  AWADDR, ARADDR;
    reg  [31:0] WDATA;
    reg  [3:0]  WSTRB;
    reg         AWVALID, WVALID, BREADY, ARVALID, RREADY;
    wire        AWREADY, WREADY, BVALID, ARREADY, RVALID;
    wire [1:0]  BRESP, RRESP;
    wire [31:0] RDATA;

    axi_lite_slave_strb dut (
        .ACLK(ACLK), .ARESETN(ARESETN),
        .AWADDR(AWADDR), .AWVALID(AWVALID), .AWREADY(AWREADY),
        .WDATA(WDATA), .WSTRB(WSTRB), .WVALID(WVALID), .WREADY(WREADY),
        .BRESP(BRESP), .BVALID(BVALID), .BREADY(BREADY),
        .ARADDR(ARADDR), .ARVALID(ARVALID), .ARREADY(ARREADY),
        .RDATA(RDATA), .RRESP(RRESP), .RVALID(RVALID), .RREADY(RREADY)
    );

    always #5 ACLK = ~ACLK;

    // Simple blocking-task write/read helpers
    task axi_write(input [3:0] addr, input [31:0] data, input [3:0] strb);
        begin
            AWADDR=addr; WDATA=data; WSTRB=strb;
            AWVALID=1; WVALID=1; BREADY=1;
            wait (AWREADY && WREADY); @(posedge ACLK);
            AWVALID=0; WVALID=0;
            wait (BVALID); @(posedge ACLK);
            BREADY=0; @(posedge ACLK);
        end
    endtask

    task axi_read(input [3:0] addr);
        begin
            ARADDR=addr; ARVALID=1; RREADY=1;
            wait (ARREADY); @(posedge ACLK);
            ARVALID=0;
            wait (RVALID);
            $display("[%0t] READ  addr=0x%0h data=0x%08h", $time, addr, RDATA);
            @(posedge ACLK); RREADY=0; @(posedge ACLK);
        end
    endtask

    initial begin
        ACLK=0; ARESETN=0;
        AWADDR=0; WDATA=0; WSTRB=4'hF;
        AWVALID=0; WVALID=0; BREADY=0; ARADDR=0; ARVALID=0; RREADY=0;
        repeat (2) @(posedge ACLK);
        ARESETN=1; @(posedge ACLK);

        // Full 32-bit write of 0x11223344 to register 0
        axi_write(4'h0, 32'h1122_3344, 4'b1111);
        axi_read (4'h0);

        // Write only the least-significant byte (strobe 0001) with 0xFF
        axi_write(4'h0, 32'h0000_00FF, 4'b0001);
        axi_read (4'h0);

        if (RDATA === 32'h1122_33FF)
            $display("RESULT: PASS - only byte 0 changed (0x112233FF)");
        else
            $display("RESULT: FAIL - got 0x%08h", RDATA);
        $finish;
    end
endmodule
[85] READ  addr=0x0 data=0x11223344
[175] READ  addr=0x0 data=0x112233ff
RESULT: PASS - only byte 0 changed (0x112233FF)
/var/folders/l0/gs5hnkm56g54bmxg32r6nnwr0000gn/T/tmpwvla6if5/axi_lite_slave_strb.v:137: $finish called at 195 (1s)

Common mistakes

  • Combinational handshake loops. Driving one side’s READY directly from the other side’s VALID through combinational logic can create loops and timing hazards. Keep AWREADY, WREADY, and RVALID registered.
  • Sampling the master’s ARVALID to produce read data. Drive RVALID and RDATA from the slave’s own accept (its ARREADY pulse) with a latched address, so a master that drops ARVALID immediately cannot cause lost or missed reads.
  • Ignoring WSTRB. Writing all 32 bits on every transfer corrupts the bytes the master did not intend to touch. Honour the strobe lanes.
  • Forgetting the B channel. A master that waits on BVALID will hang forever if the slave never issues a write response.
  • Addressing confusion. With byte addresses and 32-bit words, the word index is ADDR[3:2], not ADDR[1:0].
  • Dropping RVALID before RREADY. Payload and VALID must stay stable until the destination accepts; retracting early loses data.

Quick check

On an AXI channel, when does a data transfer actually occur?

On a rising clock edge where both VALID and READY are high in the same cycle. Either side may assert first, but the transfer only commits when they overlap.

Which channel carries the write acknowledgement, and what does BRESP = 2’b00 mean?

The write response (B) channel carries it. BRESP = 2'b00 is the OKAY status, meaning the write completed without error.

Why drive RVALID from the slave’s ARREADY pulse with a latched address instead of the master’s ARVALID?

It guarantees the read address is already captured and makes the read-data logic depend only on the slave’s own state. That removes a race in which the master lowers ARVALID on the exact edge the slave samples it, which would otherwise stall the read.

A master writes 0x000000FF with WSTRB = 4’b0001 to a register currently holding 0x11223344. What is the new value?

0x112233FF. Only strobe bit 0 is set, so only byte lane 0 is updated; the upper three bytes keep their previous contents.

Have questions about this topic?
Share your question in comments or talk to our mentor team for batch guidance.

Learn Verilog with mentors, labs and placement support

This free series is maintained by the ChipXpert training team in Hyderabad & Bengaluru. Go deeper with instructor-led courses on real EDA tools.

Explore VLSI Courses
Next batch starts 18th November 2026
Start your VLSI career with ChipXpert

Live online and classroom batches in Hyderabad & Bengaluru. Fill in your details and a counsellor will call you back.

  • Real EDA tools in your browser: industry-standard EDAReal EDA tools in your browser
  • Recorded sessions on the elearn portalRecorded sessions
  • Placement assistance: resume, mock interviewsPlacement assistance
  • Merit scholarship up to 60% · EMI optionsScholarship up to 60%
Hi! Ask me about courses, fees, batches or discounts. ×
BENGALURU
HYDERABAD