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

OffsetNameAccess
0x00CTRLRead/write
0x04DATARead/write, byte writes through WSTRB
0x08COUNTRead-only: number of writes to the register block that got OKAY
0x0CIDRead-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:

Output
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;
Output
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 RDATA and BRESP stable while their VALID waits for READY.
  • Use WSTRB for byte writes, and answer SLVERR for addresses with no register.
  • Register slaves trade speed for simplicity: one read every two cycles is plenty.
Did you find what you were looking for?

Quiz

No quiz questions available for this article.