An AXI4-Lite register slave is the most common AXI interface anyone writes: almost every IP block has one for its control and status registers. It looks simple, but it has one classic trap: the write address and the write data arrive on separate channels, in either order, and a slave that assumes they come together will hang or lose writes. This article builds a register slave that handles every order correctly.
9 min read | Intermediate
What You'll Learn
- Accept AW and W independently and combine them into one write
- Generate BVALID and RVALID without breaking the handshake rules
- Implement read/write, byte-writable and read-only registers
- Report SLVERR for addresses with no register
The Register Map
| Offset | Name | Access |
|---|---|---|
| 0x00 | CTRL | Read/write |
| 0x04 | DATA | Read/write, byte writes through WSTRB |
| 0x08 | COUNT | Read-only: number of writes to the register block that got OKAY |
| 0x0C | ID | Read-only: 0x41584C31 |
| other | - | SLVERR |
Writes to the read-only registers are ignored with an OKAY response, a common and harmless choice.
Ports
module axil_regs (
input ACLK,
input ARESETn,
// write address channel
input [11:0] AWADDR,
input [2:0] AWPROT,
input AWVALID,
output AWREADY,
// write data channel
input [31:0] WDATA,
input [3:0] WSTRB,
input WVALID,
output WREADY,
// write response channel
output reg [1:0] BRESP,
output reg BVALID,
input BREADY,
// read address channel
input [11:0] ARADDR,
input [2:0] ARPROT,
input ARVALID,
output ARREADY,
// read data channel
output reg [31:0] RDATA,
output reg [1:0] RRESP,
output reg RVALID,
input RREADY
);
AWPROT and ARPROT are inputs because every AXI4-Lite interface has them, but this slave does not use them. A slave with secure registers would check bit 1 (Non-secure) and answer SLVERR.
Write Address and Data in Any Order
The specification allows the write data to arrive before, with or after its address. A slave may wait for one before accepting the other, but the safest and fastest approach is to accept each one as soon as it arrives and hold it until its partner comes:
reg [31:0] ctrl;
reg [31:0] data;
reg [31:0] count;
// Holding registers: an address or data that arrived before its partner
reg aw_full;
reg [11:0] aw_addr;
reg w_full;
reg [31:0] w_data;
reg [3:0] w_strb;
assign AWREADY = !aw_full;
assign WREADY = !w_full;
// The write happens once both halves are here and the response channel is free
wire do_write = aw_full && w_full && (!BVALID || BREADY);
always @(posedge ACLK) begin
if (!ARESETn) begin
aw_full <= 1'b0;
w_full <= 1'b0;
BVALID <= 1'b0;
ctrl <= 32'h0;
data <= 32'h0;
count <= 32'h0;
end
else begin
if (AWVALID && AWREADY) begin
aw_full <= 1'b1;
aw_addr <= AWADDR;
end
if (WVALID && WREADY) begin
w_full <= 1'b1;
w_data <= WDATA;
w_strb <= WSTRB;
end
if (BVALID && BREADY)
BVALID <= 1'b0;
if (do_write) begin
aw_full <= 1'b0;
w_full <= 1'b0;
BVALID <= 1'b1;
BRESP <= (aw_addr[11:4] == 8'h00) ? OKAY : SLVERR;
if (aw_addr[11:4] == 8'h00)
count <= count + 32'd1;
if (aw_addr == 12'h000)
ctrl <= w_data;
if (aw_addr == 12'h004) begin
if (w_strb[0])
data[7:0] <= w_data[7:0];
if (w_strb[1])
data[15:8] <= w_data[15:8];
if (w_strb[2])
data[23:16] <= w_data[23:16];
if (w_strb[3])
data[31:24] <= w_data[31:24];
end
end
end
end
AWREADY is high whenever no address is being held, and WREADY whenever no data is being held. The write itself happens in the cycle when both holding registers are full and the response channel is free (!BVALID || BREADY). That same cycle empties both holding registers and raises BVALID, so the response always follows both the address and the data, as AXI4 requires.
Quick Check
Q: A slave sets WREADY = AWVALID, so it only takes data in the cycle the address arrives. What goes wrong?
Show Answer
Its WREADY now depends on the master's AWVALID. That is legal, but if the master sends data first and the address much later, the data waits the whole time. And if the slave also made AWREADY depend on WVALID, a master that sends one before the other would wait forever. Accepting each channel independently avoids both problems.
Reads
// Reads: one at a time, the address is accepted when the read data channel is free
assign ARREADY = !RVALID;
always @(posedge ACLK) begin
if (!ARESETn) begin
RVALID <= 1'b0;
end
else begin
if (RVALID && RREADY)
RVALID <= 1'b0;
if (ARVALID && ARREADY) begin
RVALID <= 1'b1;
RRESP <= (ARADDR[11:4] == 8'h00) ? OKAY : SLVERR;
case (ARADDR)
12'h000: RDATA <= ctrl;
12'h004: RDATA <= data;
12'h008: RDATA <= count;
12'h00C: RDATA <= 32'h41584C31;
default: RDATA <= 32'h0;
endcase
end
end
end
The read side accepts an address when the read data channel is empty, and puts the data on R in the next cycle. Because ARREADY is !RVALID, this slave handles one read every two cycles at most. That is plenty for registers; a faster slave would also accept an address in the cycle the previous data is taken, using !RVALID || RREADY.
The Simulation
The testbench sends five writes. The first has address and data together; the second sends its data three cycles before the address; the third sends its address eight cycles before its data, which writes only the top byte. The last two go to the read-only ID and to an address with no register. Then it reads everything back:
35 AW id=0 addr=0x000 len=0 size=4 INCR 35 W data=0x000000c1 strb=1111 last 55 W data=0x11223344 strb=1111 last 55 B id=0 OKAY 75 AW id=0 addr=0x004 len=0 size=4 INCR 95 AW id=0 addr=0x004 len=0 size=4 INCR 95 B id=0 OKAY 145 W data=0xaa000000 strb=1000 last 165 AW id=0 addr=0x00c len=0 size=4 INCR 165 W data=0xffffffff strb=1111 last 165 B id=0 OKAY 185 AW id=0 addr=0x040 len=0 size=4 INCR 185 W data=0x12345678 strb=1111 last 185 B id=0 OKAY 205 B id=0 SLVERR 215 AR id=0 addr=0x000 len=0 size=4 INCR 225 R id=0 data=0x000000c1 OKAY last 235 AR id=0 addr=0x004 len=0 size=4 INCR 245 R id=0 data=0xaa223344 OKAY last 255 AR id=0 addr=0x008 len=0 size=4 INCR 265 R id=0 data=0x00000004 OKAY last 275 AR id=0 addr=0x00c len=0 size=4 INCR 285 R id=0 data=0x41584c31 OKAY last 295 AR id=0 addr=0x040 len=0 size=4 INCR 305 R id=0 data=0x00000000 SLVERR last
In the waveform, the data of the second write is accepted at cycle 3 and waits in the holding register until its address arrives at cycle 5. For the third write it is the other way round: the address is held from cycle 7 until the data arrives at cycle 12. In both cases there is exactly one write and one response. COUNT reads 4: the four writes that landed in the register block, including the ignored write to ID.
Quick Check
Q: While the third write's address waits in the holding register, the master sends the address of the fourth write. Can the slave accept it?
Show Answer
No. AWREADY is low while an address is held, so the fourth address waits with AWVALID high, as the waveform shows from cycle 8. It is accepted after the third write completes.
Running It in ChipVerify Lab
The example contains this slave and the testbench. Move the write data earlier and later and check that every write still happens exactly once.
Common Mistakes
Mistake #1: Assuming AW and W arrive together
A slave that writes only when AWVALID and WVALID are high in the same cycle works with some masters and hangs with others. Hold each half until the other arrives.
Mistake #2: A new response before the old one is taken
Raising BVALID for a second write while the first response is still waiting overwrites it. Only complete a write when !BVALID || BREADY.
Mistake #3: Read data that changes under a stall
Once RVALID is high, RDATA must stay put until RREADY. Reading a counter register combinationally from the current state lets the value move while the master waits.
Practice Exercise
Challenge: Reject Read-Only Writes
Change the slave so that a write to COUNT or ID gets SLVERR instead of being silently ignored, and is not counted. What do the write responses and COUNT show now?
Hint
Only CTRL (0x000) and DATA (0x004) are writable. Base both BRESP and the COUNT increment on that.
Solution
BRESP <= (aw_addr == 12'h000 || aw_addr == 12'h004) ? OKAY : SLVERR;
if (aw_addr == 12'h000 || aw_addr == 12'h004)
count <= count + 32'd1;
185 B id=0 SLVERR 205 B id=0 SLVERR ... 265 R id=0 data=0x00000003 OKAY last
The write to ID now gets SLVERR, as does the write to 0x040, and COUNT reads 3. Whether to reject read-only writes is a design choice: rejecting them catches software bugs early, ignoring them is more forgiving. Document whichever you choose.
Open the ready-made example in the lab and run the simulation.
Summary
- Accept the write address and the write data independently, holding each until its partner arrives.
- Complete a write only when both halves are present and the response channel is free.
- Keep
RDATAandBRESPstable while their VALID waits for READY. - Use
WSTRBfor byte writes, and answer SLVERR for addresses with no register. - Register slaves trade speed for simplicity: one read every two cycles is plenty.