The other articles in this chapter each used one part of the same AXI4 memory slave. This article puts the whole design together: a 4 KB memory with an independent write engine and read engine, a read address queue for outstanding transactions, burst address generation, byte strobes, error responses and an exclusive access monitor. Along the way it points out the design choices a real slave has to make.
10 min read | Intermediate
What You'll Learn
- Structure an AXI4 slave as separate write and read engines
- Generate READY and VALID without breaking the handshake rules
- Queue read addresses to support outstanding transactions
- Decide what a simple slave may leave out
The Plan
| Feature | Choice in this slave |
|---|---|
| Data bus, IDs, memory | 32 bits, 4-bit IDs, 4 KB (one 4 KB region, so no burst can cross a boundary inside it) |
| Bursts | FIXED, INCR and WRAP, any length, byte to word size |
| Writes | One write transaction at a time |
| Reads | Up to four addresses queued, answered in order, with a fixed latency |
| Errors | SLVERR for any access at 0xF00 and above |
| Exclusive access | One monitor |
Ports and Parameters
module axi_mem #(
parameter READ_LAT = 2,
parameter ERR_BASE = 12'hF00,
parameter TRACE = 0
) (
input ACLK,
input ARESETn,
// write address channel
input [3:0] AWID,
input [11:0] AWADDR,
input [7:0] AWLEN,
input [2:0] AWSIZE,
input [1:0] AWBURST,
input AWLOCK,
input AWVALID,
output AWREADY,
// write data channel
input [31:0] WDATA,
input [3:0] WSTRB,
input WLAST,
input WVALID,
output WREADY,
// write response channel
output reg [3:0] BID,
output reg [1:0] BRESP,
output reg BVALID,
input BREADY,
// read address channel
input [3:0] ARID,
input [11:0] ARADDR,
input [7:0] ARLEN,
input [2:0] ARSIZE,
input [1:0] ARBURST,
input ARLOCK,
input ARVALID,
output ARREADY,
// read data channel
output reg [3:0] RID,
output reg [31:0] RDATA,
output reg [1:0] RRESP,
output reg RLAST,
output reg RVALID,
input RREADY
);
The interface leaves out the optional signals the slave does not use: AxCACHE, AxPROT, AxQOS, AxREGION and the user signals. A slave may ignore those inputs; in a real design they would still be connected, and a slave that enforces security would check AxPROT.
The Burst Address
// Address of the next beat of a burst
function [11:0] next_addr(input [11:0] a, input [2:0] size, input [1:0] burst, input [7:0] len);
reg [11:0] bytes;
reg [11:0] incr;
reg [11:0] wrap_bytes;
begin
bytes = 12'd1 << size;
incr = (a & ~(bytes - 12'd1)) + bytes; // aligned address + size
if (burst == FIXED) begin
next_addr = a;
end
else if (burst == WRAP) begin
wrap_bytes = bytes * (len + 12'd1); // the wrap boundary block
next_addr = (a & ~(wrap_bytes - 12'd1)) | (incr & (wrap_bytes - 12'd1));
end
else begin
next_addr = incr;
end
end
endfunction
One function serves both engines. It increments from the aligned address, so unaligned bursts work, and wraps inside the block of beats times size for WRAP bursts.
The Write Engine
// ---- Write side: AW, then W beats, then B ----
reg w_active; // an address was accepted, data beats are due
reg [3:0] w_id;
reg [11:0] w_addr;
reg [7:0] w_len;
reg [2:0] w_size;
reg [1:0] w_burst;
reg w_excl; // exclusive write
reg w_pass; // the exclusive write may update memory
reg w_err;
assign AWREADY = !w_active && !BVALID;
assign WREADY = w_active;
always @(posedge ACLK) begin
if (!ARESETn) begin
w_active <= 1'b0;
BVALID <= 1'b0;
end
else begin
if (BVALID && BREADY)
BVALID <= 1'b0;
if (AWVALID && AWREADY) begin
w_active <= 1'b1;
w_id <= AWID;
w_addr <= AWADDR;
w_len <= AWLEN;
w_size <= AWSIZE;
w_burst <= AWBURST;
w_excl <= AWLOCK;
w_pass <= !AWLOCK || (mon_valid && mon_id == AWID && mon_addr == AWADDR);
w_err <= 1'b0;
end
if (WVALID && WREADY) begin
if (TRACE)
$display("%0t slave: write beat at 0x%h, WSTRB %b", $time, w_addr, WSTRB);
if (w_addr >= ERR_BASE) begin
w_err <= 1'b1;
end
else if (w_pass) begin
if (WSTRB[0])
mem[w_addr[11:2]][7:0] <= WDATA[7:0];
if (WSTRB[1])
mem[w_addr[11:2]][15:8] <= WDATA[15:8];
if (WSTRB[2])
mem[w_addr[11:2]][23:16] <= WDATA[23:16];
if (WSTRB[3])
mem[w_addr[11:2]][31:24] <= WDATA[31:24];
end
w_addr <= next_addr(w_addr, w_size, w_burst, w_len);
if (WLAST) begin
w_active <= 1'b0;
BVALID <= 1'b1;
BID <= w_id;
if (w_err || w_addr >= ERR_BASE)
BRESP <= SLVERR;
else if (w_excl && w_pass)
BRESP <= EXOKAY;
else
BRESP <= OKAY;
end
end
end
end
The write side is a small state machine with one state bit, w_active. AWREADY is high when no write is in progress and no response is waiting, so a new address is accepted in a single cycle, as the specification recommends. WREADY is high while a burst is active; the slave waits for the address before taking data, which is allowed. The response is raised on the last beat, so it always follows both the address and all the data, as AXI4 requires.
Errors are tracked per beat but reported once, on B, as the protocol demands. Beats in the disabled region are not written. An exclusive write that fails the monitor check (w_pass = 0) writes nothing and gets OKAY.
Quick Check
Q: AWREADY depends on BVALID, and BVALID only falls when the master raises BREADY. Is that a forbidden dependency?
Show Answer
No. The rule forbids a VALID that waits for its own READY. A READY that waits for something else, here the previous response being taken, is allowed. The cost is performance: a master slow to accept responses slows down this slave's writes.
The Read Engine
// ---- Read side: a queue of 4 addresses, answered in order ----
reg [3:0] q_id [0:3];
reg [11:0] q_addr [0:3];
reg [7:0] q_len [0:3];
reg [2:0] q_size [0:3];
reg [1:0] q_burst [0:3];
reg q_lock [0:3];
reg [15:0] q_time [0:3]; // first cycle the data may be sent
reg [1:0] q_head;
reg [1:0] q_tail;
reg [2:0] q_count;
reg [15:0] cycle;
reg r_active; // sending the beats of one burst
reg [3:0] r_id;
reg [11:0] r_addr;
reg [7:0] r_len;
reg [7:0] r_left; // beats still to send after this one
reg [2:0] r_size;
reg [1:0] r_burst;
reg r_lock;
wire ar_hs = ARVALID && ARREADY;
wire present = r_active && (!RVALID || RREADY); // a new beat can go out this cycle
wire can_load = q_count != 3'd0 && cycle >= q_time[q_head] &&
(!r_active || (present && r_left == 8'd0));
assign ARREADY = (q_count < 3'd4);
always @(posedge ACLK) begin
if (!ARESETn) begin
q_head <= 2'd0;
q_tail <= 2'd0;
q_count <= 3'd0;
cycle <= 16'd0;
r_active <= 1'b0;
RVALID <= 1'b0;
end
else begin
cycle <= cycle + 16'd1;
if (ar_hs) begin
q_id[q_tail] <= ARID;
q_addr[q_tail] <= ARADDR;
q_len[q_tail] <= ARLEN;
q_size[q_tail] <= ARSIZE;
q_burst[q_tail] <= ARBURST;
q_lock[q_tail] <= ARLOCK;
q_time[q_tail] <= cycle + READ_LAT - 2;
q_tail <= q_tail + 2'd1;
end
q_count <= q_count + (ar_hs ? 3'd1 : 3'd0) - (can_load ? 3'd1 : 3'd0);
if (RVALID && RREADY)
RVALID <= 1'b0;
if (present) begin
if (TRACE)
$display("%0t slave: read beat at 0x%h", $time, r_addr);
RVALID <= 1'b1;
RID <= r_id;
RLAST <= (r_left == 8'd0);
if (r_addr >= ERR_BASE) begin
RDATA <= 32'h0;
RRESP <= SLVERR;
end
else begin
RDATA <= mem[r_addr[11:2]];
RRESP <= r_lock ? EXOKAY : OKAY;
end
r_addr <= next_addr(r_addr, r_size, r_burst, r_len);
if (r_left == 8'd0)
r_active <= 1'b0;
else
r_left <= r_left - 8'd1;
end
if (can_load) begin
r_active <= 1'b1;
r_id <= q_id[q_head];
r_addr <= q_addr[q_head];
r_len <= q_len[q_head];
r_left <= q_len[q_head];
r_size <= q_size[q_head];
r_burst <= q_burst[q_head];
r_lock <= q_lock[q_head];
q_head <= q_head + 2'd1;
end
end
end
Every accepted address goes into a four-entry queue with the cycle when its data may be sent. ARREADY stays high while the queue has room, so the master can have four reads in flight. The engine takes the head of the queue when its time has come, either when idle or while sending the last beat of the previous burst, so consecutive bursts follow without a gap.
The beat logic uses the standard AXI sender condition, !RVALID || RREADY: a new beat goes onto the channel only when the channel is empty or its current beat is being accepted. That keeps RDATA stable while the master stalls, and RVALID never depends on RREADY.
The Exclusive Monitor
// ---- Exclusive monitor updates ----
always @(posedge ACLK) begin
if (!ARESETn) begin
mon_valid <= 1'b0;
end
else begin
if (can_load && q_lock[q_head]) begin
mon_valid <= 1'b1; // an exclusive read starts monitoring
mon_id <= q_id[q_head];
mon_addr <= q_addr[q_head];
end
else if (WVALID && WREADY && w_pass && |WSTRB && w_addr[11:2] == mon_addr[11:2]) begin
mon_valid <= 1'b0; // any write to the location ends it
end
end
end
Quick Check
Q: The read engine loads the next burst while sending the last beat of the current one. What would change if it waited until the channel was idle?
Show Answer
Every burst would be followed by one empty cycle on R, even when the next burst's data was ready. Loading during the last beat keeps back-to-back bursts gap-free.
The Simulation
The testbench writes a burst, overwrites one byte, reads the burst back while a failing write runs on the write channels, and finishes with an exclusive pair:
write(1, 12'h010, 3, WORD, INCR, 32'h11, 0, 0, 0); // INCR, 4 words
write(1, 12'h015, 0, BYTE, INCR, 32'hAB, 0, 0, 0); // one byte at 0x015
after_b = 2;
read(2, 12'h010, 3, WORD, INCR, 0, 0); // 0x014 now holds 0x0000AB12
write(3, 12'hF00, 0, WORD, INCR, 32'hEE, 0, 0, 0); // SLVERR
after_b = 3;
read(4, 12'h040, 0, WORD, INCR, 1, 0); // exclusive read
after_r = 2;
write(4, 12'h040, 0, WORD, INCR, 32'h44, 0, 1, 0); // exclusive write: EXOKAY
35 AW id=1 addr=0x010 len=3 size=4 INCR 45 W data=0x00000011 strb=1111 55 W data=0x00000012 strb=1111 65 W data=0x00000013 strb=1111 75 W data=0x00000014 strb=1111 last 85 B id=1 OKAY 95 AW id=1 addr=0x015 len=0 size=1 INCR 105 W data=0x0000ab00 strb=0010 last 115 B id=1 OKAY 125 AW id=3 addr=0xf00 len=0 size=4 INCR 125 AR id=2 addr=0x010 len=3 size=4 INCR 135 W data=0x000000ee strb=1111 last 145 B id=3 SLVERR 155 AR id=4 addr=0x040 len=0 size=4 INCR exclusive 155 R id=2 data=0x00000011 OKAY 165 R id=2 data=0x0000ab12 OKAY 175 R id=2 data=0x00000013 OKAY 185 R id=2 data=0x00000014 OKAY last 195 R id=4 data=0x00000000 EXOKAY last 205 AW id=4 addr=0x040 len=0 size=4 INCR exclusive 215 W data=0x00000044 strb=1111 last 225 B id=4 EXOKAY
The byte write replaced only byte 1 of the word at 0x014, which reads back as 0x0000AB12. The read of ID 2 and the failing write of ID 3 run at the same time on their own channels, and the second read address is accepted while the first burst is still returning data.
What a Real Slave Would Add
- Several writes in flight: an address queue on AW and a response queue on B, so that a slow
BREADYdoes not block new writes. - Out-of-order reads: several read engines and ID tracking, as in Transaction IDs and Ordering.
- One exclusive monitor per ID, as the specification recommends.
- Register slices on the channels to meet timing, which AXI allows on any channel.
- Checks on
WLASTagainstAWLENand on unsupported sizes, answered with SLVERR.
Running It in ChipVerify Lab
The example contains this slave and the testbench above. Change READ_LAT and watch the read data move.
Common Mistakes
Mistake #1: Accepting a new address while still busy
A slave that raises AWREADY while it is still receiving the data of the previous burst must store that address somewhere. Without a queue, it overwrites the burst in progress.
Mistake #2: Updating RDATA under a stall
Present a new beat only when !RVALID || RREADY. Anything else can lose or duplicate beats when the master applies backpressure.
Mistake #3: A write response before the address
An AXI3-style slave that accepts all data and responds before taking the address is not AXI4-compliant. Raise BVALID only after both the address and the last data beat.
Practice Exercise
Challenge: A Read-Only Region
Add a parameter RO_BASE = 0xE00. Writes from RO_BASE up to 0xEFF must not change memory and must get SLVERR; reads there stay normal. Test it with a two-beat write starting at 0xDFC and a read of the same two words.
Hint
Only the write engine changes: treat a beat at or above RO_BASE like a beat in the disabled region.
Solution
if (w_addr >= ERR_BASE || w_addr >= RO_BASE) begin
w_err <= 1'b1;
end
// ... and the same condition when BRESP is chosen on the last beat:
if (w_err || w_addr >= ERR_BASE || w_addr >= RO_BASE)
BRESP <= SLVERR;
35 AW id=1 addr=0xdfc len=1 size=4 INCR 45 W data=0x000000d0 strb=1111 55 W data=0x000000d1 strb=1111 last 65 B id=1 SLVERR 75 AR id=1 addr=0xdfc len=1 size=4 INCR 105 R id=1 data=0x000000d0 OKAY 115 R id=1 data=0x00000000 OKAY last
The burst gets SLVERR, but its first beat, at the writable 0xDFC, was written: the read returns 0xD0 there and the unchanged 0 at 0xE00. The specification allows this: a write that receives an error may have partly updated the slave.
Open the ready-made example in the lab and run the simulation.
Summary
- An AXI4 slave splits naturally into a write engine (AW, W, B) and a read engine (AR, R) around shared memory.
- One next-address function handles FIXED, INCR, WRAP and unaligned bursts.
- A read address queue with timestamps gives outstanding transactions and hides latency.
- Present a new beat only when the channel is empty or being emptied:
!VALID || READY. - Errors are reported per beat on R and once per burst on B; failed exclusive writes change nothing.