AMBA APB · Module 11
Register-Bank Design
The structural heart of an APB slave — the control/status register bank. How to organise the flop arrays, distinguish register kinds (RW, RO, W1C, RO-hardware), separate storage from bus glue, and build the skeleton the decoder, read mux, write logic, and PREADY/PSLVERR generation all plug into.
Everything you have learned about APB so far has been about the bus — phases, handshakes, errors, versions. This module flips to the other side of the wire: building the slave that answers it. And the first thing to build is not the bus interface — it is the register bank, the array of flops the peripheral actually exposes. The single idea to carry: an APB slave is a register bank wrapped in a thin layer of bus glue, and the quality of the whole slave is decided by how cleanly you separate the two. Get the bank's structure right — which registers exist, what kind each is, how the hardware and the bus both reach them — and the decoder, read mux, write logic, and PREADY/PSLVERR generation become small, obvious modules that plug into it. Get it wrong and every later piece fights the storage.
1. Problem statement
The problem is organising a peripheral's software-visible state into an addressable array of registers that two very different masters — the CPU over APB, and the peripheral's own hardware — can both read and write without corrupting each other.
A control/status register (CSR) bank is the contract between firmware and hardware. Software writes control registers to command the peripheral and reads status registers to observe it; the hardware does the reverse, updating status and consuming control. The bank has to make that two-sided access well-defined for every bit, which forces three structural decisions before a single line of bus logic is written:
- What register exists at each offset, and of what kind. A read-write config register, a read-only status register driven by hardware, a write-1-to-clear interrupt flag, and a write-only command strobe are four different circuits, not four addresses of the same circuit. The bank's job is to give each offset the right behaviour on read, on write, and on hardware update.
- Who owns each bit on each edge. For every register you must answer: does software's write win, or hardware's update, or both (and in what priority)? A status bit the hardware sets and software clears (W1C) has a precise arbitration rule; getting it wrong loses interrupts or makes them un-clearable.
- How storage stays separate from the bus. The flops that hold the state must be independent of the APB glue that reaches them, so the same bank can be read by the hardware combinationally, updated on its own conditions, and accessed by the bus — without those three paths tangling.
So the job is not "make some flops at some addresses" — it is to define a typed, two-sided, cleanly-layered register array that the bus interface can then service mechanically.
2. Why previous knowledge is insufficient
You have spent ten modules on the APB protocol — the manager's view, the handshake, errors, byte strobes. That knowledge tells you exactly what arrives at the slave's pins and when, but it says nothing about the thing the pins connect to:
- The protocol defines the access, not the storage.
PSEL/PENABLE/PWRITE/PREADYdescribe a read or write transaction. They do not tell you whether the addressed location is a plain RW flop, a hardware-driven status bit, or a clear-on-write flag — and those behave completely differently for the same bus transaction. The register kind is invisible at the protocol layer and is exactly what this module adds. - You have been treating "the register" as a given. Every prior chapter said "the slave captures the write" or "returns the data" as if the storage were trivial. It is not: a real bank mixes register kinds, has bits owned by hardware, and must resolve simultaneous software-and-hardware updates. That arbitration has no representation in the bus protocol.
- The bus glue is small; the bank is where the design lives. The decoder, read mux, and write logic you will build next are thin and mechanical if the bank is well-structured — and a nightmare if it is not. So the protocol knowledge is necessary but not sufficient: you now need the internal architecture the protocol was always talking to.
So the model to add is the slave's interior: a typed register bank with defined hardware/software ownership, sitting behind — and deliberately separated from — the thin APB interface.
3. Mental model
The model: an APB slave is a filing cabinet (the register bank) with a single clerk at the front desk (the bus interface). The cabinet holds the actual documents — each drawer a register, each with its own rules about who may write it and who may only read it. The clerk does not store anything; the clerk only fetches the right drawer for a bus access and applies its rule. The peripheral's hardware is a second person who reaches into the back of the cabinet directly, updating status drawers and reading control drawers, never going through the clerk. Design the cabinet — the drawers and their rules — first; the clerk's job is then trivial.
Three refinements make it precise:
- Each register is a typed drawer. Name the kinds explicitly: RW (software reads and writes; plain storage), RO (software reads; the value comes from hardware, software writes are ignored), W1C (software reads the flags; writing a 1 clears that bit; hardware sets it), WO/strobe (writing triggers an action; reads return 0 or a defined value). The kind determines three behaviours — read value, write effect, hardware-update effect — and the bank must encode all three per register.
- Storage is owned, not shared by accident. Every bit has a defined owner on every edge. A pure RW bit is software-owned. An RO status bit is hardware-owned (software writes do nothing). A W1C bit is jointly owned with a fixed priority — hardware-set generally beats software-clear on the same cycle, so a freshly-arrived event is not silently dropped. Writing the priority down is part of the bank design, not an afterthought.
- The bank is a layer, separate from the bus. Structure the slave as: (1) the register storage and its hardware-update logic, (2) a thin bus interface that produces, per access, a decoded write-enable and an address for the read mux. The interface hands the bank "write these bytes to this register now"; the bank decides what that means for each kind. This separation is what lets you verify the bank against hardware independently of the bus, and swap the bus version (APB3↔APB4) without touching storage.
4. Real SoC implementation
In RTL, the discipline is to declare the storage and its hardware ownership explicitly, and to accept a decoded write request from the bus glue rather than letting bus signals reach into the flops directly. Here is the bank skeleton the rest of the module plugs into.
// Register bank: the typed storage at the heart of the slave.
// The bus interface (decoder + write logic, built in later chapters) hands
// this bank a per-register write-enable and the write data; the bank applies
// each register's KIND rule. Hardware reaches the bank on its own ports.
module apb_reg_bank (
input pclk, presetn,
// --- decoded bus write request (produced by the bus glue, not raw APB) ---
input wr_en_ctrl, // CONTROL register selected + committing
input wr_en_iflag, // IFLAG (W1C) register selected + committing
input [31:0] wr_data, // already byte-merged via PSTRB upstream
// --- hardware-side ports (the peripheral's own logic) ---
input [31:0] hw_status, // RO: status value owned by hardware
input hw_event, // sets an IFLAG bit (hardware-owned)
// --- read-side exposure (the read mux selects among these) ---
output [31:0] ctrl_q, // RW control, also consumed by hardware
output [31:0] status_q, // RO status (just hw_status, registered)
output [31:0] iflag_q // W1C interrupt flags
);
reg [31:0] ctrl, status, iflag;
// RW control: software-owned plain storage. Hardware reads ctrl_q.
always_ff @(posedge pclk or negedge presetn)
if (!presetn) ctrl <= 32'h0; // defined reset value
else if (wr_en_ctrl) ctrl <= wr_data; // software write commits
// RO status: hardware-owned. Software writes can NEVER reach it.
always_ff @(posedge pclk or negedge presetn)
if (!presetn) status <= 32'h0;
else status <= hw_status; // only hardware drives it
// W1C interrupt flag: JOINT ownership with a fixed priority.
// hardware-set beats software-clear so a new event is never lost.
always_ff @(posedge pclk or negedge presetn)
if (!presetn) iflag <= 32'h0;
else begin
if (wr_en_iflag) iflag <= iflag & ~wr_data; // write-1-to-clear
if (hw_event) iflag[0] <= 1'b1; // hw set wins (after clear)
end
assign ctrl_q = ctrl; assign status_q = status; assign iflag_q = iflag;
endmoduleTwo facts make this the right structure. First, the bank never sees raw APB — it receives a decoded wr_en_* and already-merged wr_data, so the address-decode (next chapter), byte-strobe merge (PSTRB), and commit-timing (write logic) live in the thin glue, not tangled into the storage. Second, each register's always_ff encodes its kind — RW commits on the bus write, RO ignores it entirely, W1C arbitrates hardware-set over software-clear — which is exactly the per-register behaviour the protocol layer cannot express. The read mux (its chapter) then just selects among ctrl_q/status_q/iflag_q. This skeleton is small precisely because the bank's structure carries the design weight.
5. Engineering tradeoffs
How you structure the bank is a sequence of trades between simplicity, area, and how cleanly the rest of the slave plugs in.
| Decision | Option A | Option B | When to choose which |
|---|---|---|---|
| Register declaration | One flat reg [31:0] per CSR, named | A packed reg [31:0] regs [0:N-1] array indexed by offset | Named flops for a small typed bank (clarity, per-kind logic); array for a large uniform RW block |
| Kind handling | Per-register always_ff encoding its kind | A generic field-attribute table / generated logic | Hand-written for a handful of mixed kinds; generated (IP-XACT/RDL) for large maps |
| Storage vs glue | Bank module separate from bus interface | One flat module mixing both | Separate, always — independent verification and version-portability outweigh the extra wiring |
| Hardware ports | Explicit hw_* inputs/outputs per register | Hardware pokes the same flops inline | Explicit ports — makes ownership visible and testable; inline coupling hides races |
| Reset values | Per-register defined reset constant | Bulk zero / no reset | Defined constants for anything software reads at boot (reset chapter) |
The throughline: the bank should be the only place state lives, it should be typed so each register's read/write/hardware behaviour is explicit, and it should be separate from the bus glue. Those three choices cost a little extra structure and buy a slave whose decoder, mux, write logic, and error/ready generation are each small, independent, and verifiable — which is the entire goal of this module.
6. Common RTL mistakes
7. Debugging scenario
The signature register-bank bug is a lost or un-clearable interrupt flag, caused by getting the joint hardware/software ownership of a W1C bit wrong — and it only manifests when a hardware event and a software clear collide on the same cycle.
- Observed symptom: an interrupt is occasionally missed — the handler runs, clears the flag, returns, and the line immediately re-asserts for an event that already happened; or, the mirror case, an interrupt that cannot be cleared — software writes 1 to clear it, but it is set again the same cycle, so the handler spins forever. Both appear only under load, when events are dense.
- Waveform clue: on the bus, a write-1-to-clear to the
IFLAGregister lands on the samePCLKedge that the hardwarehw_eventpulses. In the buggy design the flag ends the cycle clear (the software clear overwrote the hardware set) — so the event is lost; or, in the other buggy variant, the whole register was modelled as plain RW so the hardware set is overwritten by the software-written data every time and the bit tracks the bus, not the event. - Root cause: the W1C register was implemented without a defined priority between the software clear and the hardware set on a coincident cycle. The clear and the set were written in an order (or in a way) that lets the clear win, so a hardware event arriving in the same cycle it is cleared is dropped — a classic read-clear race in the storage, not in the handler.
- Correct RTL: give the bit joint ownership with hardware-set priority: apply the software clear first, then the hardware set, in the same
always_ff, so a coincident event survives the clear —if (wr_en_iflag) iflag <= iflag & ~wr_data; if (hw_event) iflag[0] <= 1'b1;(the second assignment, later in the block, wins for bit 0). The flag ends the cycle set, the handler sees the new event, and no interrupt is lost. - Verification assertion: assert that a hardware event is never lost to a coincident clear —
assert property (@(posedge pclk) disable iff(!presetn) (hw_event) |=> iflag[0]);(after any event, the flag is set next cycle regardless of a simultaneous clear) — plus a check that an RO register never changes on a bus write:assert property (@(posedge pclk) disable iff(!presetn) (wr_en_status_region) |=> $stable(status_q) || status_changed_by_hw);. - Debug habit: when an interrupt is lost or un-clearable, do not start in the ISR — go to the register bank and find the W1C bit's
always_ff. Check the priority between the software clear and the hardware set on a coincident cycle. A huge fraction of "phantom" or "stuck" interrupt bugs are a missing or wrong arbitration rule in the storage, invisible until a hardware event and a clear align — which is exactly why the bank's per-bit ownership must be explicit by design.
8. Verification perspective
A register bank is verified against its kind contract — every register must behave correctly on read, on write, and on hardware update — and the highest-value tests are the ones that collide the two owners.
- Test each register's kind, exhaustively. For every register: an RW must read back what was written; an RO must ignore software writes and reflect hardware; a W1C must clear only the written-1 bits and leave others; a WO/strobe must trigger its action and read its defined value. A register-model-driven test (UVM RAL or equivalent) automates this and is the baseline — but only if the model declares the right kind per register, which is exactly the bank's design intent.
- Collide the owners — this is where the bugs are. The most valuable tests drive a hardware update and a bus access to the same register on the same cycle: a
hw_eventset against a software W1C clear, a hardware status update against a software read, a control write against a hardware consume. Assert the defined priority holds (hardware-set wins the W1C race; software never corrupts an RO). A bank that passes isolated per-register tests can still drop every coincident event. - Cover ownership and reset, not just addresses. Functional coverage must include, per register, that a bus write and a hardware update were seen on the same cycle (the race was actually exercised), and that every register was observed at its reset value out of
PRESETn. An address-only coverage model — "every offset was accessed" — completely misses the ownership races that are the bank's real risk.
The point: the bank's correctness is a per-bit, two-owner property. Verify each register's kind contract, deliberately collide hardware and software on the same bit, and cover the races — not just that each address was touched.
9. Interview discussion
"How would you structure an APB slave's register bank?" is a strong design-judgment question because a weak answer says "an array of registers" while a strong one shows you think in register kinds and ownership.
Lead with the architecture: an APB slave is a typed register bank wrapped in thin bus glue, and you design the bank first. Enumerate the kinds — RW (software storage), RO (hardware-owned, software writes ignored), W1C (write-1-clear, hardware-set, with a defined priority), WO/strobe (write triggers, read defined) — and stress that each is a different circuit, not a different address. Then deliver the depth that separates senior answers: every bit has an owner on every edge, and any bit written by both hardware and the bus needs an explicit priority — the canonical example being a W1C interrupt flag where hardware-set must beat software-clear on a coincident cycle, or events are silently lost. Close with the structural discipline: keep the storage separate from the bus interface, so the bank receives a decoded write-enable and merged data while address decode, strobe merge, and commit timing stay in the glue — which is what lets you verify the bank against hardware independently and reuse it across APB versions. Mentioning that the lost/un-clearable interrupt bug lives in the bank's arbitration, not the ISR, signals you have actually debugged one.
10. Practice
- Type the bank. Given a peripheral with a config word, a status word, an interrupt-flag word, and a "start" command, assign each a register kind (RW/RO/W1C/WO) and state its read, write, and hardware-update behaviour.
- Assign ownership. For each register above, name the owner on every edge, and identify which register needs an explicit hardware-vs-software priority and why.
- Write the W1C. From memory, write the
always_fffor a W1C interrupt flag where hardware-set beats software-clear on a coincident cycle; explain the assignment order. - Draw the layers. Sketch the slave as bank + thin bus glue + hardware ports, and mark which signals cross each boundary (decoded write-enable, read-address,
hw_*). - Find the race. Given an interrupt that re-asserts immediately after the handler clears it, locate the bug in the bank and state the one-line fix.
11. Q&A
12. Key takeaways
- An APB slave is a typed register bank wrapped in thin bus glue — design the bank first; the decoder, read mux, write logic, and
PREADY/PSLVERRgeneration are small modules that plug into a well-structured bank. - Registers come in kinds — RW, RO, W1C, WO/strobe — and each is a different circuit, with its own read value, write effect, and hardware-update behaviour. Treating them uniformly is the root of status-corruption and lost-interrupt bugs.
- Every bit has an owner on every edge. Any bit written by both hardware and the bus needs an explicit priority; the canonical case is W1C, where hardware-set must beat software-clear on a coincident cycle or events are lost.
- Keep storage separate from the bus. The bank should receive a decoded write-enable and merged data, never raw APB — so address decode, strobe merge, and commit timing stay in the glue, and the bank is independently verifiable and version-portable.
- Kind is a property of a field, not just a register. A mixed-kind word must be composed from independently-owned fields, or writing one field corrupts the others.
- Verify the kind contract and collide the owners. Test each register's read/write/hardware behaviour, deliberately drive a hardware update and a bus access to the same bit on the same cycle, and cover the race — not just that each address was accessed.