Skip to content

AMBA APB · Module 12

Read-Only Registers

Read-only registers as a map contract — software observes, hardware owns the value — and the write-to-RO policy decision: silently drop the write (lenient) or return PSLVERR (strict). Constant/ID registers versus live hardware-driven status, and the always-defined read value.

A read-only (RO) register is a one-directional contract: software may read the value, but the value is owned and driven by hardware, and a software write to it does not change the stored value. Everything in this chapter follows from that single asymmetry. The previous module built RO as a bank kind in RTL — writes ignored — and this chapter steps up to the map view: what RO promises software, the two flavours it comes in (a fixed constant/ID versus a live hardware status), why the read value must always be defined, and the one genuine design decision RO forces — the write-to-RO policy: silently drop the write (lenient, common) or return PSLVERR (strict). RO is the access type where the hardware is the author and software is only ever the reader.

1. Problem statement

The problem is specifying a register whose value software can observe but never author — and pinning down exactly what happens when software nonetheless writes to it.

Read-only is the access type for everything software needs to see but must not set: a peripheral's live status (busy, FIFO level, error flags driven by the datapath), and fixed facts about the IP (an identification register, a version/revision number, a capability bitmap). For all of these, the value's source of truth is hardware, not the last bus write. That single decision — hardware owns the value — creates three specification questions that the map must answer before any RTL exists:

  • What does a read return, and is it always defined? A read of an RO register must yield a meaningful, defined value at every instant — the live hardware state, or the hardwired constant. There is no "RO read is undefined" case; an undefined read value is a broken contract, because software branches on what it reads.
  • What does a write do? Software will write to RO addresses — by accident (a stray store), by a buggy driver, or because a generic register-walk test hits every address. The map must declare the policy: does the write silently complete with no effect on the value (drop), or does the slave flag it as an error (PSLVERR)? This is the one real RO design decision.
  • What is the reset/default value? Software reads RO registers at boot — to discover the IP version, read initial status, or check capabilities — often before anything else happens. So even an RO register needs a defined value out of reset (the constant, or the hardware's reset state), because firmware acts on it immediately.

So the job is not "make a register the bus can't write" — it is to author a contract where the read value is always defined and hardware-owned, and where the write policy (drop versus error) is a deliberate, documented choice.

2. Why previous knowledge is insufficient

Chapter 12.1 established that access types are a contract — a documented per-field promise the driver binds to, not an implementation detail. RO is the first access type we now design as a full contract, and the prior chapters give us pieces but not the whole:

  • Module 11.1 (register-bank-design) built RO as a bank kind in RTL — writes ignored. That chapter answered "how do I make the flop bank ignore a write to this slot?" This chapter answers the questions upstream of that: what RO promises software, whether the value is a constant or live hardware, and what the write policy should be. The RTL technique from 11.1 is the tool; this chapter decides what to build with it. We apply 11.1, we do not re-teach it.
  • The write policy is a map decision the bank chapter never made. "Writes ignored" silently assumes the drop policy. But dropping is only one of two legal choices — the strict alternative is to return PSLVERR on a write to RO. That is a contract decision (lenient versus strict), made in the map, that the bank RTL alone does not surface.
  • Module 11.6 (pslverr-generation) taught how to produce PSLVERR. If you choose the strict write policy, the mechanism is exactly what 11.6 covered — you fold "write to an RO address" into the error term. This chapter decides whether to error and why; it does not re-derive how the error signal is generated. We apply 11.6.

So the model to add is the RO contract: read value always defined and hardware-owned, the constant-versus-live distinction, a defined reset, and the explicit write-policy decision — built on top of the bank RTL (11.1) and, if strict, the error mechanism (11.6). This sets up the mirror-image access types coming next: 12.5 write-only registers (software authors, read is the question) and 12.6 status registers (the RO-live flavour taken to its full depth).

3. Mental model

The model: a read-only register is a gauge, not a dial. A dial (RW) is something software turns and the hardware obeys — software is the author. A gauge (RO) is something software looks at — the needle is driven by the machine, and pressing on the glass does nothing to the reading. Software is purely an observer. The whole of RO is contained in that asymmetry: the value flows hardware → software, never the other way.

Three refinements make it precise:

  • The needle has two kinds of source. A live gauge (RO-hardware-driven) reflects something that changes — busy, FIFO occupancy, an error flag the datapath raised; its value is wired from hardware logic onto the read path and changes as the hardware does. A fixed plate (RO-constant) is engraved once — an ID register, a version number, a capability bitmap; its value is a hardwired constant on the read path that never changes. Both are RO — software only reads — but one is live and one is constant, and that difference matters for verification and for what software does with the value.
  • The needle always reads something. Whether live or constant, a read returns a defined value at every instant — there is no moment where reading the gauge yields garbage. Hardware owning the value means hardware is responsible for it being defined, including out of reset.
  • Pressing the glass is the design decision. What happens when software writes to the gauge? The lenient answer: nothing — the write completes (PREADY, no error) and the needle is unmoved. The strict answer: the slave flags the attempt with PSLVERR — the needle still does not move, but the access is reported as an error. Choosing drop-versus-error is the single real RO decision; the value's immunity to the write is constant across both.
Two read-only registers side by side: on the left a hardware status source drives a live value onto the read mux while a software write is dropped at a storage flop that does not exist; on the right a hardcoded ID constant is wired to the read mux and a write is likewise dropped; a footnote contrasts the lenient drop policy against the strict PSLVERR policy, both of which leave the value unchanged.
Figure 1 — the two flavours of read-only register and the write-policy decision. On the left, an RO hardware-live register: a hardware status source drives a live value straight onto the read mux, so a bus read returns the current hardware state (blue read path); a software write is dropped — the write decode never enables a storage flop (amber dropped-write path ending in nothing). On the right, an RO constant register: a hardcoded ID/version value is wired directly to the read mux, so the bus always reads the same fixed constant; a write is likewise dropped. The footnote shows the one design decision — the lenient policy silently completes the write (PREADY, no PSLVERR), while the strict policy asserts PSLVERR on a write to RO; in both cases the value is never changed. The figure frames RO as a contract where the read value is always defined and hardware-owned, and the only choice is the write policy.

4. Real SoC implementation

RO is implemented by making the read mux return a hardware-owned value for the address and by never enabling a storage flop on a write to that address. A live RO drives logic onto the read path; a constant RO wires a literal onto it. The lenient policy needs nothing extra — the write simply lands nowhere. The strict policy folds "write to an RO address" into the [error term (11.6)].

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------------
//  Read-only registers as a MAP CONTRACT applied onto the bank RTL (11.1).
//  Map:
//    0x10  ID       [31:0]  RO  constant  reset = 32'hDEAD_0001  (version/ID)
//    0x14  STATUS   BUSY[0], LEVEL[7:4]   RO  live hardware      (driven by datapath)
//  The contract: software READS the value; the value is HARDWARE-OWNED;
//  a software WRITE never changes it. The only choice is the write policy.
// ---------------------------------------------------------------------------
 
localparam ID = 4'h0, STATUS = 4'h4;   // low nibble of paddr (window-relative)
 
// --- RO-constant (ID / version) -------------------------------------------
// No flop. The value is a hardwired literal placed on the read path.
// Defined out of reset by construction; a write has nothing to land in.
localparam logic [31:0] ID_VALUE = 32'hDEAD_0001;
 
// --- RO-hardware-live (STATUS) --------------------------------------------
// The value is DRIVEN BY HARDWARE onto the read path, in the exact field
// positions the map declares. No storage flop -> writes cannot change it.
logic [31:0] status_rd;
assign status_rd = {24'h0, hw_level /*[7:4]*/, 3'h0, hw_busy /*[0]*/};
 
// --- Read mux: RO values appear here; writes never reach a flop -----------
always_comb begin
  prdata = 32'h0;                                   // defined default
  unique case (paddr[7:4])
    ID:     prdata = ID_VALUE;                       // RO-constant: always the same
    STATUS: prdata = status_rd;                      // RO-live: current hardware state
    default: prdata = 32'h0;                         // unmapped reads as defined 0
  endcase
end
 
// --- Write side: there is simply NO write enable for ID or STATUS ---------
// Other (RW) registers have wr_en_<reg> terms; RO registers deliberately
// have none, so a write to 0x10 / 0x14 is silently DROPPED (lenient policy).
// (RW example, for contrast — RO has no equivalent line:)
//   if (wr_en_ctrl) ctrl <= pwdata;
 
// ===========================================================================
//  OPTIONAL STRICT VARIANT: return PSLVERR on a write to an RO address.
//  We do NOT re-derive PSLVERR here — we APPLY the mechanism from 11.6 by
//  contributing one term: "a write whose address decodes to an RO register".
// ===========================================================================
wire access      = psel & penable;
wire is_ro_addr  = (paddr[7:4] == ID) | (paddr[7:4] == STATUS);
wire write_to_ro = access & pwrite & is_ro_addr;    // the offending access
 
// Folded into the slave's existing error term (see 11.6 for how pslverr is
// driven/timed). The stored value is STILL never changed — strict only adds
// the report; it does not make RO writable.
assign pslverr = write_to_ro /* | other_error_terms... */;

Two facts make this contract design and not just coding. First, the value is hardware-owned by construction: the constant is a literal and the live status is assign-driven from datapath logic — neither has a write-enable, so the RO promise ("a write never changes the value") is structural, not something the write logic has to remember to enforce. Second, drop-versus-strict is a one-line policy switch, not a re-architecture: the lenient design simply omits the write-enable; the strict design adds one term (write_to_ro) to the error mechanism from 11.6. In both cases the stored value is identically immune — strictness changes only whether the bus reports the attempt, never whether the value moves.

5. Engineering tradeoffs

The RO design choices split along two axes: what drives the value (constant versus live hardware) and what the write policy is (drop versus error). Each combination has a place.

RO design choiceBehaviour on read / writeStrictnessWhen to use it
Write-drop (lenient)Read returns the hardware value; a write completes normally (PREADY, no PSLVERR) with no effectLenient — the default in most SoCsWhen you want maximum software robustness: register-walk tests, generic init code, and stray writes never break; the common, expected choice
Write-error (PSLVERR)Read returns the hardware value; a write asserts PSLVERR — but the value is still unchangedStrictWhen the integration policy wants illegal accesses reported (safety/security IP, debug builds, contract-enforcement) so a buggy driver is caught, not silently tolerated
RO-constantRead always returns the same hardwired literal; write dropped/errored per policyn/a (value is fixed)ID, version/revision, capability bitmaps — facts about the IP that never change; lets software identify the block and gate features at runtime
RO-hardware-liveRead returns the current hardware state in the mapped field positions; write dropped/errored per policyn/a (value changes)Status: busy, FIFO level, error/condition flags driven by the datapath — software polls the live machine state

The throughline: the value's immunity to writes is constant; only two things vary — whether the value is a fixed plate or a live needle, and whether a write is silently absorbed or loudly rejected. Default to drop for software robustness; choose strict PSLVERR only when your integration contract wants illegal writes surfaced. Pick constant for facts about the IP and live for state of the IP.

6. Common RTL mistakes

7. Debugging scenario

The signature RO bug is a read-only status accidentally implemented as read-write: the status field is a writable flop instead of a wire from hardware, so a stray software write latches a stale value over the live hardware status — masking a real condition.

  • Observed symptom: an overflow (or error) condition that the hardware genuinely raised does not show up when software polls the status register — the bit reads 0 even though the datapath asserted it. It is intermittent and correlated with other software activity: the masking happens only after some unrelated driver code runs, which makes it look like a phantom hardware glitch.

  • Waveform clue: on the bus, just before the status bit goes "stuck," there is a write transaction to the status register's address (PWRITE=1, PSEL/PENABLE asserted) — often a full-word write of 0 from a generic clear-the-registers init or a stray store. After that write, the read-back of the status no longer tracks the live hardware signal: the internal status_q flop holds the written value, while the true hardware condition hw_ovf is high and ignored. The tell is that a write to a register that should be RO is changing what a later read returns.

  • Root cause: the status field was declared RO in the map but built as a writable flop in the RTL — it has a write-enable on the status address. So software can write it, and a stray write stamps a stale value into the flop, which the read mux then returns instead of the live hardware status. The live hw_ovf is never actually wired onto the read path; the flop is. The RO contract ("hardware owns the value, software writes never change it") was violated in the RTL.

  • Correct RTL: make the status structurally hardware-owned — drive it onto the read path directly from hardware with no storage flop and no write-enable, exactly as in Beat 4 (assign status_rd = {…, hw_ovf, …};). A write to the status address then has nowhere to land and is dropped (lenient) — or, under the strict policy, contributes write_to_ro to the [error term (11.6)]. Either way the read always returns the live hardware condition, and no software write can mask it.

  • Verification assertion: prove that a write to an RO address never changes what is observed there. For a live RO, assert the read tracks hardware regardless of writes; for any RO, assert a write does not alter the stored/observed value:

    Azvya Education Pvt. Ltd.VLSI Mentor
    Snippet
    // A write to the RO STATUS address must NOT change the observed value:
    // the read of STATUS always equals the live hardware status, write or not.
    property ro_status_is_hw_owned;
      @(posedge pclk) disable iff (!presetn)
        (psel && penable && paddr[7:4] == STATUS)
          |-> (prdata == {24'h0, hw_level, 3'h0, hw_busy});
    endproperty
    assert property (ro_status_is_hw_owned);
     
    // General RO-immutability: a write to an RO address leaves its read value stable.
    property ro_write_has_no_effect;
      logic [31:0] seen;
      @(posedge pclk) disable iff (!presetn)
        (psel && penable && pwrite && is_ro_addr, seen = ro_read_value)
          |=> (ro_read_value == seen) or $stable(hw_owned_source);
    endproperty
    assert property (ro_write_has_no_effect);
  • Debug habit: when a status bit "goes stale" or "gets stuck," do not chase the hardware that drives it first — check whether a write transaction to that register precedes the stickiness. If a write to a supposedly read-only register changes a later read, the RO field was built as a writable flop. The fix is structural: RO status must be a wire from hardware, never a software-writable flop.

Two cases of a status register: the top buggy case in red shows a writable flop where a stray software write of zero overrides a live hardware OVF=1 so the read returns a stale zero; the bottom correct case in green shows the status wired combinationally from hardware to the read mux with the software write dropped, so the read returns the true live OVF=1.
Figure 2 — the RO-as-RW bug, shown as two cases. Top (bug, red): the status is a writable flop instead of a wire from hardware. The hardware reports OVF=1 (active), but a stray software write of 0 latches into the flop, and the read mux returns the stale written 0 — the real overflow condition is masked and the bug is hidden. Bottom (correct, green): the same status is a combinational wire driven straight from hardware onto the read mux, with no storage flop; the software write is dropped (amber) because there is nowhere for it to land, so the read always returns the true live status OVF=1 and the condition is never masked. The figure teaches that an RO status must be structurally hardware-owned, not a software-writable flop — the symptom of the bug being a status bit a stray write can overwrite.

8. Verification perspective

An RO register is verified as a contract: the value is hardware-owned, software writes never change it, the constant reads the constant, the live RO tracks hardware, and the chosen write policy (drop versus error) is exercised and covered.

  • Write-to-RO leaves the value unchanged. The core RO check: issue a write to every RO address with a value different from what is there, then read back and assert the value is unchanged (the live hardware state or the constant — not the written data). This is the directed proof that the RO contract holds, and it is where an RO-built-as-RW bug (Beat 7) is caught: a writable-flop RO will read back the written value and fail the check.
  • RO reflects hardware; the constant reads the constant. For a live RO, drive the hardware source to several distinct states and assert each read returns the current hardware value in the mapped field positions — including transitions while no bus access is happening — proving the read path is wired to hardware, not to a flop. For an RO-constant, assert the read returns the exact specified literal out of reset and after arbitrary traffic, confirming it is truly fixed (a common bug is an "ID register" that is accidentally a 0 flop). A UVM RAL model built from the map (12.1) gives the RO predict/mirror behaviour for free: RAL knows an RO field is not updated by a write and is back-door comparable to the hardware value.
  • Cover the write policy — drop and error. Functional coverage must record a write to each RO address and the bus response under the chosen policy: under drop, that the access completed with PSLVERR low; under strict, that the access asserted PSLVERR. Cover both a write and a read to each RO register, the live RO observed in multiple hardware states, and the constant RO read at reset. An address-only "every RO offset accessed" model misses that the write response and the value immutability are the actual RO contract.

The point: verify RO as a promise about ownership — software writes never move the value, the value comes from hardware (constant or live), and the write-policy response is whatever the map declared — not merely that the offset is reachable.

9. Interview discussion

"What is a read-only register, and what happens if software writes to it?" looks trivial but separates a memorizer from a designer: the weak answer is "you can read it but not write it," while the strong answer frames RO as a contract and surfaces the write-policy decision.

Lead with the framing: a read-only register is a contract where software may only observe the value, the value is owned and driven by hardware, and a software write does not change the stored value. Then add the two distinctions that show depth. First, RO has two flavours: an RO-constant (ID, version, capability bits — a hardwired literal software reads to identify and feature-gate the IP) and an RO-hardware-live (status: busy, FIFO level, error flags — driven by the datapath onto the read path). Note that a live RO is often a wire, not a flop with its write-enable removed. Second — the part that signals real experience — the write-to-RO policy is the one genuine design decision: software will write RO addresses (stray stores, register-walk tests, buggy drivers), and you must choose between silently dropping the write (lenient, the common default, keeps generic software robust) and returning PSLVERR (strict, reports the illegal access for safety/security/debug). Crucially, both leave the value unchanged — strict only adds the report. Close with the integration point: "I apply the bank RTL from the register-bank module to make the value hardware-owned, and if I choose strict I fold 'write to an RO address' into the existing PSLVERR term — RO is never a writable flop, and an RO read value is always defined." That arc — contract, two flavours, the write-policy decision, and the structural guarantee — is a complete senior answer.

10. Practice

  1. State the contract. For an RO register, write down precisely what a read returns and what a write does — for both the lenient and the strict policy — and note what is constant across both policies.
  2. Classify constant versus live. Given a busy flag, a FIFO occupancy count, an IP version number, and a capability bitmap, label each RO-constant or RO-hardware-live and justify it by where its value comes from.
  3. Write the RTL. Implement an RO-constant ID register and an RO-live status register so neither has a write-enable, the read mux returns the hardware-owned value, and a write is dropped. Then add the strict variant by contributing one term to the error logic.
  4. Choose the write policy. For (a) a general-purpose timer IP and (b) a security/safety block, decide drop versus PSLVERR-on-write-to-RO and justify each by the integration goal.
  5. Find the bug. Given a status register that "goes stale" after unrelated driver activity, identify the RTL mistake (RO built as a writable flop), the waveform tell (a write to the RO address precedes the stickiness), and the structural fix.

11. Q&A

12. Key takeaways

  • A read-only register is a one-directional contract: software may only observe the value, the value is owned and driven by hardware, and a software write never changes the stored value.
  • RO comes in two flavours: an RO-constant (ID, version, capability bits — a hardwired literal software reads to identify and feature-gate the IP) and an RO-hardware-live (status driven by the datapath onto the read path). A live RO is often a wire, not a flop with its write-enable removed.
  • The read value is always defined — including out of reset — because software reads RO registers (often at boot) and branches on what it reads; an undefined RO read is a broken contract.
  • The one real design decision is the write-to-RO policy: silently drop the write (lenient, the common default, keeps generic software robust) or return PSLVERR (strict, reports the illegal access). Both leave the value unchanged — strict only adds the report.
  • RO is built on the bank RTL (11.1) and, if strict, the error mechanism (11.6) — you apply them, not re-teach them: make the value hardware-owned by construction, and contribute one "write to an RO address" term to the existing PSLVERR logic.
  • Verify RO as ownership: writes never move the value, the constant reads the constant, the live RO tracks hardware, and the write-policy response (drop versus error) is exercised and covered.