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

FeatureChoice in this slave
Data bus, IDs, memory32 bits, 4-bit IDs, 4 KB (one 4 KB region, so no burst can cross a boundary inside it)
BurstsFIXED, INCR and WRAP, any length, byte to word size
WritesOne write transaction at a time
ReadsUp to four addresses queued, answered in order, with a fixed latency
ErrorsSLVERR for any access at 0xF00 and above
Exclusive accessOne 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
Output
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 BREADY does 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 WLAST against AWLEN and 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;
Output
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.
Did you find what you were looking for?

Quiz

No quiz questions available for this article.