AMBA AXI · Module 15
AXI4-Lite Register Bank
Generalize the hand-coded AXI4-Lite slave into a parameterised, table-driven register bank — a register description table that captures address, access type, and reset, with generic decode, WSTRB-aware write, read mux, and per-field access-type logic (RW/RO/W1C/W1S) that scales to hundreds of registers.
In Chapter 15.1 we built a slave with two hand-coded registers. That doesn't scale: a real peripheral has dozens or hundreds of control/status registers, and hand-writing a case arm per register is error-prone and unmaintainable. This chapter turns the slave into a parameterised register bank — a single, reusable block whose register map is described by a table (address, access type, reset value, fields), with generic decode, write, and read logic that loops over that table. The AXI4-Lite handshake engines from 15.1 stay exactly the same; what changes is that the register file becomes data-driven. This is how production CSR blocks are actually built, and it's the structure register generators (IP-XACT/SystemRDL → RTL) emit.
1. From Hand-Coded Registers to a Description Table
The key shift is separating what the register map is (a table of descriptors) from how it's accessed (generic logic that reads the table). Each register is described once — its byte offset, access type, reset value, and which bits are writable — and the decode/write/read logic is written once, parameterised over the number of registers.
The table is a parameter — an array of descriptors. In SystemVerilog you can express it with a packed struct and a parameter array:
typedef enum logic [1:0] { RW, RO, W1C, W1S } acc_e; // access types
typedef struct packed {
logic [ADDR_W-1:0] offset; // byte offset (word-aligned)
acc_e access; // access type
logic [DATA_W-1:0] reset; // reset/default value
logic [DATA_W-1:0] wmask; // writable-bit mask (reserved bits = 0)
} reg_desc_t;
parameter int NREG = 4;
parameter reg_desc_t MAP [NREG] = '{
'{ offset: 'h00, access: RW, reset: 32'h0000_0000, wmask: 32'hFFFF_FFFF }, // CTRL
'{ offset: 'h04, access: RO, reset: 32'h0000_0000, wmask: 32'h0000_0000 }, // STATUS
'{ offset: 'h08, access: W1C, reset: 32'h0000_0000, wmask: 32'h0000_00FF }, // IRQ_STATUS
'{ offset: 'h0C, access: RW, reset: 32'h0000_00FF, wmask: 32'h0000_00FF } // IRQ_ENABLE
};2. Generic Address Decode
Decode becomes a loop over the table instead of a hand-written case. It compares the incoming word offset against every descriptor's offset, producing a one-hot select (or an index) and a "hit" flag; no hit means DECERR. Because it's generated from the table, adding a register is a one-line table edit — the logic never changes.
function automatic logic [$clog2(NREG)-1:0] decode_index
(input logic [ADDR_W-1:0] addr, output logic hit);
hit = 1'b0;
decode_index = '0;
for (int i = 0; i < NREG; i++) begin
if (addr[ADDR_W-1:2] == MAP[i].offset[ADDR_W-1:2]) begin
decode_index = i[$clog2(NREG)-1:0];
hit = 1'b1;
end
end
endfunction3. Generic Write with Access-Type and WSTRB Logic
The write datapath now applies two masks per byte: the WSTRB byte enable (which bytes the manager wants to write) and the descriptor's wmask (which bits are writable, so reserved/RO bits are protected). On top of that, the access type decides the update rule — RW writes the masked value, W1C clears bits where a 1 is written, W1S sets them, RO ignores writes entirely.
// Inside the write-commit state, idx = decoded register index
always_ff @(posedge aclk) begin
if (!aresetn) begin
for (int i = 0; i < NREG; i++) reg_q[i] <= MAP[i].reset;
end else if (do_write && hit_w) begin
for (int b = 0; b < DATA_W/8; b++) begin
if (wstrb_q[b]) begin
logic [7:0] wb = wdata_q[b*8 +: 8];
logic [7:0] mask = MAP[idx_w].wmask[b*8 +: 8]; // writable bits
unique case (MAP[idx_w].access)
RW : reg_q[idx_w][b*8 +: 8] <= (reg_q[idx_w][b*8 +: 8] & ~mask)
| (wb & mask);
W1C: reg_q[idx_w][b*8 +: 8] <= reg_q[idx_w][b*8 +: 8] & ~(wb & mask);
W1S: reg_q[idx_w][b*8 +: 8] <= reg_q[idx_w][b*8 +: 8] | (wb & mask);
RO : ; // ignore write data
endcase
end
end
end
endHardware updates (e.g. a status bit a peripheral sets, or a W1C event bit) are merged separately — typically a hardware-set path ORs into W1C/RO bits — but the software-side rules above are uniform across the whole map.
4. Generic Read Mux and Reset
Reads become a single indexed mux over the register array — no per-register case. Reset initializes every register to its descriptor's reset value in one loop (shown above). For registers with read side effects (RC — read-clears), the read state also triggers the clear; for plain RW/RO/W1C the read is a pure mux.
// Read datapath: idx_r = decoded read index, hit_r from decode
always_ff @(posedge aclk) begin
if (aresetn && do_read) begin
if (hit_r) begin
s_rdata <= reg_q[idx_r];
s_rresp <= 2'b00; // OKAY
end else begin
s_rdata <= '0;
s_rresp <= 2'b11; // DECERR (unmapped)
end
end
endThe whole bank is now defined by the MAP table plus this fixed datapath. Adding a register — say an IRQ_RAW W1C status at 0x10 — is a single new row in MAP and a bump to NREG; no decode, write, or read code changes. That's the payoff of the table-driven structure, and it's exactly what register generators automate.
5. Common Misconceptions
6. Debugging Insight
7. Verification Insight
8. Interview Questions
9. Summary
A register bank generalizes the hand-coded slave by separating the register map (a description table of offset, access type, reset value, and writable-bit mask) from the access logic (generic decode, write, and read that loop over the table). The AXI4-Lite handshake FSMs from Chapter 15.1 are reused unchanged. Decode compares the incoming word address against every table entry to produce an index and a hit (miss → DECERR). Write gates each byte by WSTRB (manager intent) and wmask (writable bits), then applies the descriptor's access-type rule — RW substitutes, W1C clears written-ones, W1S sets them, RO ignores. Read is a single indexed mux, and reset loads every register from its descriptor. Reserved bits (wmask = 0) stay write-protected for forward compatibility.
The payoff is that adding or changing a register is a one-row table edit with no datapath change — the same structure register generators emit from SystemRDL/IP-XACT — and the same table can drive the UVM register model, so RTL and verification share one source of truth. Verification proves the shared generic logic deeply on per-type representatives and sweeps the whole map model-driven, making even hundred-register banks tractable. Next, we zoom back into the engines themselves and design the write-side FSM rigorously.
10. What Comes Next
You've made the register file scalable; next we return to the handshake engines and design the write FSM rigorously:
- 15.3 — AXI Write FSM (coming next) — the write-side state machine in depth: AW/W sequencing, response generation, and the corner cases a robust write engine must handle.
Previous: 15.1 — A Simple AXI4-Lite Slave. Related: 10.4 — Common CSR Design Patterns for the access-type semantics this bank implements, 6.7 — Write Strobes (WSTRB) for byte enables, and 10.5 — AXI4-Lite Verification Checklist for the register-model verification strategy.