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.
| Channel | Direction | Key signals | Purpose |
|---|---|---|---|
| Write address (AW) | Master to slave | AWADDR, AWVALID, AWREADY | Where to write |
| Write data (W) | Master to slave | WDATA, WSTRB, WVALID, WREADY | What to write |
| Write response (B) | Slave to master | BRESP, BVALID, BREADY | Acknowledge the write |
| Read address (AR) | Master to slave | ARADDR, ARVALID, ARREADY | Where to read |
| Read data (R) | Slave to master | RDATA, RRESP, RVALID, RREADY | Return 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
READYdirectly from the other side’sVALIDthrough combinational logic can create loops and timing hazards. KeepAWREADY,WREADY, andRVALIDregistered. - Sampling the master’s
ARVALIDto produce read data. DriveRVALIDandRDATAfrom the slave’s own accept (itsARREADYpulse) with a latched address, so a master that dropsARVALIDimmediately 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
BVALIDwill 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], notADDR[1:0]. - Dropping
RVALIDbeforeRREADY. Payload andVALIDmust 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.
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