AMBA APB · Module 12
Configuration Registers
Protecting persistent configuration from runtime corruption — lock bits (lock-until-reset), write-once fields, and key-unlock (magic value) sequences that guard a critical config write against stray or runaway writes, and how that register-level protection differs from bus-level PPROT.
Configuration registers are the persistent, rarely-written, must-not-be-disturbed state of a peripheral — a clock divider, a mode select, a calibration threshold, written once at init and then held for the life of the run. The single idea to carry: a configuration register's design is mostly about protection. Unlike a status register that changes constantly, config is set once and must survive everything that happens afterward — including a wild pointer in firmware or runaway code spraying writes across the address map. So the engineering of a config register is the engineering of mechanisms that make it hard to write by accident: a lock bit that freezes it until reset, a key/magic-value unlock that demands a deliberate sequence before a write is accepted, and write-once fields that can be programmed exactly once. The throughline: configuration holds critical persistent state, and protecting it from corruption is the design.
1. Problem statement
The problem is keeping persistent, set-at-init configuration from being corrupted at runtime — by a stray write, a buggy driver, or runaway code — while still letting legitimate initialisation write it once.
A configuration register is unlike the registers of the previous chapters in one decisive way: it is supposed to stop changing. Firmware writes it during boot — a UART's baud divider, a PLL's mode, a DMA's burst size, a watchdog's timeout — and from then on the value must hold. That creates a tension the map has to resolve:
- It must be writable once, then effectively immutable. Init needs to set it; everything after init must not be able to disturb it. A plain RW register satisfies the first requirement and fails the second — anything with bus access can overwrite it at any time.
- The threat is accidental, not just malicious. A wild pointer, a stack overflow, a driver bug that computes the wrong offset, an interrupt that fires mid-update — these stamp garbage onto whatever address they hit. If that address is a clock divider, the system clock changes under the feet of running code. The register's job is to survive the rest of the system misbehaving.
- Some config must never change after first program — even legitimately. A trim value, a security mode, a device ID, a one-time calibration: programmed once at manufacture or boot, then frozen forever. RW is wrong here; the contract is write-once.
So the job is not "store a value" — it is to build a register whose write path is gated: open exactly long enough for legitimate configuration, then closed so that persistent critical state cannot be corrupted by the chaos of runtime.
2. Why previous knowledge is insufficient
The earlier chapters give you a register that stores a value and an RTL that commits writes. None of them give you a register that refuses writes — and refusal is the whole point of configuration protection.
- 12.1 CSR fundamentals typed registers by access, not by durability. It taught RW/RO/W1C/RC/WO — what reading and writing do. Configuration adds an orthogonal axis: when a write is even allowed. A config register may be "RW" in access type yet refuse most writes because it is locked. Access type and write-protection are two different specifications layered on the same register.
- 12.6 status registers are the opposite end of the same spectrum. Status is volatile event state — it changes constantly, hardware owns it, software mostly reads. Configuration is persistent contract state — it changes almost never, software owns it, and protecting it from change is the design. Same register file, opposite lifecycles; comparing them sharpens what "config" means.
- 11.4 write logic commits a write unconditionally. The commit RTL there is
if (wr_en) reg <= pwdata. Configuration protection inserts a condition in front of that commit:if (wr_en && !locked && key_ok) reg <= pwdata. The previous chapter built the commit; this chapter builds the guard on the commit. - Module 9 protection violations is a different layer of protection.
PPROTis a bus-access control — the master declares privileged/non-privileged, secure/non-secure, and the slave can reject a transaction whose origin is wrong. That is access control on the transaction. Configuration locks/keys are register-level controls on the write itself, visible to and operated by software through ordinary registers — they fire even for a perfectly legal, privileged access. The two stack:PPROTcan stop the unauthorised requester; the lock bit can stop the authorised-but-mistaken write.
So the model to add is the write-protection contract: a configuration register carries, on top of its access type, a guard that decides whether a legitimate-looking write is allowed to commit at all — by lock, by key, or by write-once. This sets up 12.8 register-map best practices, where these mechanisms become part of a generated, documented map.
3. Mental model
The model: a configuration register is a safe, and the lock/key mechanisms are how you close it after loading. During init you load the safe — set the divider, the mode, the threshold. Then you shut the door: set the lock bit, and the contents are sealed until the system is reset. A plain RW register is a safe with the door welded open; configuration is the discipline of building, and then closing, the door.
Three refinements make it precise:
- A lock bit is a one-way latch that seals the register until reset. The lock bit is itself a tiny write-once register: software writes it to 1, and from then on the protected register (or block) is read-only, and the lock bit itself cannot be cleared by software. Only
PRESETnreopens it. This is lock-until-reset (a.k.a. write-once-lock). The contract is deliberate: protection you can turn off by writing one bit is protection a wild write can also turn off. - A key is a combination you must dial before the door opens. A key/magic-value scheme requires software to write a specific value (say
0xC0DE) to an unlock register, which arms a short window; only a write that lands while the window is open commits. A stray write almost never carries the right value to the right address at the right time, so the key defends against accidental corruption — the random-write probability of hitting the exact magic value first, then the real write within N cycles, is vanishingly small. - Write-once is a fuse: programmable once, then blown. A write-once field accepts exactly one write after reset and ignores every subsequent write until the next reset. It is the cleanest "program at init, freeze forever" primitive — no separate lock bit to manage, the first write is the lock.
4. Real SoC implementation
Here is a configuration register guarded by both mechanisms — a lock-until-reset LOCK bit and a key-unlock window — so a write to CFG commits only if it is unlocked and the magic key was written first. This is the idiomatic shape you see in real PLL, clock, and security blocks.
// Configuration register CFG, protected by a lock bit AND a key-unlock window.
//
// Offset Register Field Bits Reset Access Notes
// 0x00 CFG DIVIDER [7:0] 8'h08 RW* *guarded: commits only if unlocked + keyed
// 0x00 CFG MODE [9:8] 2'h0 RW*
// 0x04 KEY KEY [15:0] 0 WO write 0xC0DE to arm the unlock window
// 0x08 LOCK LOCK [0] 0 W1S-once write 1 -> locks until reset (cannot clear)
localparam [15:0] MAGIC = 16'hC0DE; // the agreed unlock key
localparam [2:0] WINDOW = 3'd4; // key arms a 4-cycle window
logic [9:0] cfg; // the protected configuration value
logic locked; // lock-until-reset latch
logic [2:0] unlock_cnt; // counts down the key-unlock window (0 = closed)
wire wr_cfg = wr_en && (paddr[3:0] == 4'h0);
wire wr_key = wr_en && (paddr[3:0] == 4'h4);
wire wr_lock = wr_en && (paddr[3:0] == 4'h8);
wire key_window_open = (unlock_cnt != 0);
// A CFG write commits ONLY when not locked AND the key window is currently open.
wire cfg_commit = wr_cfg && !locked && key_window_open;
always_ff @(posedge pclk or negedge presetn) begin
if (!presetn) begin
cfg <= 10'h008; // reset value IS the contract: divider 8, mode 0
locked <= 1'b0;
unlock_cnt <= 3'd0;
end else begin
// --- LOCK: write-once, lock-until-reset. Once set, software cannot clear it. ---
if (wr_lock && wdata[0] && !locked)
locked <= 1'b1; // one-way latch; only PRESETn reopens
// --- KEY: writing the magic value arms the window; anything else does not ---
if (wr_key && (wdata[15:0] == MAGIC) && !locked)
unlock_cnt <= WINDOW; // arm: open the window for WINDOW cycles
else if (unlock_cnt != 0)
unlock_cnt <= unlock_cnt - 3'd1; // window ticks closed; a wrong key never arms
// --- CFG: commits only through the guard ---
if (cfg_commit)
cfg <= wdata[9:0]; // legitimate, keyed, unlocked write
// else: write is silently dropped — cfg holds its set-at-init value
end
end
// Reads are unaffected by the lock — config is always readable.
always_comb unique case (paddr[3:0])
4'h0: prdata = {22'h0, cfg};
4'h4: prdata = 32'h0; // KEY reads as 0 (write-only, no key readback)
4'h8: prdata = {31'h0, locked}; // LOCK readable so software can confirm it sealed
default: prdata = 32'h0;
endcaseThree facts make this configuration design and not just a flop. First, the guard is the design — cfg_commit is gated by !locked && key_window_open, so the register physically cannot be written outside a deliberate keyed-and-unlocked sequence; everything else is dropped. Second, lock-until-reset is one-way on purpose — locked only ever goes 0→1 in logic, never 1→0, so once firmware seals the config no software path (including a wild write to the LOCK address) can unseal it; only PRESETn does. Third, the key defends against accidents, not adversaries — it makes random corruption astronomically unlikely (a stray write must carry 0xC0DE to KEY, then a real write to CFG within four cycles), but anything that can run the legitimate sequence can write the register, which is the right threat model for "don't let buggy firmware clobber the clock divider." The lock is the harder wall; the key is the procedural gate. The actual commit machinery underneath is the write logic from 11.4 — this chapter only adds the condition in front of it.
5. Engineering tradeoffs
Configuration protection is a menu of mechanisms with different strengths and threat models. Pick the weakest one that actually covers the risk — over-protecting config makes legitimate reconfiguration painful.
| Mechanism | Strength | What it defends against | When to use |
|---|---|---|---|
| Lock bit (lock-until-reset) | Strong, simple, irreversible until reset | Any runtime write after init — stray, buggy, or malicious — because the gate is closed and software cannot reopen it | Config set once at boot and never changed for the run (clock dividers, security mode, calibration); the default config guard |
| Write-once field | Strong for a single field, no extra register | A field that must be programmed exactly once then frozen forever (trim, device ID, one-time fuse-like config) | Manufacture/boot-time program-once values; cleanest "first write is the lock" primitive |
| Key-unlock (magic value) | Moderate — defends accidents, not a determined attacker with bus access | Stray/random writes from runaway code; requires a deliberate value+sequence a wild write won't reproduce | Config that is changed occasionally at runtime but must never change by accident (PLL retune, watchdog disable) |
| PPROT bus-access protection | Strong on origin, not on intent — a different layer | An unauthorised requester (wrong privilege/security level) reaching the register at all — checked at the bus-access layer, Module 9.4 | Isolating who may touch a config block; stacks under register locks, not instead of them |
The throughline: these mechanisms are layers, not alternatives. PPROT controls who may reach the register (transaction origin); the lock/key/write-once controls whether the write commits (register intent). A robust critical-config block often uses both — PPROT so only privileged secure code can address it, and a lock bit so even that code cannot disturb it after sealing. Choose lock for set-once config, write-once for program-once fields, key for rarely-but-legitimately reconfigured config, and reserve key-only for "guard against accident," never for "guard against an adversary."
6. Common RTL mistakes
7. Debugging scenario
The signature configuration bug is persistent critical state silently corrupted at runtime because the register had no real lock — the value set at init survives for a while, then a stray write from buggy firmware overwrites it and the system degrades with no obvious trigger.
-
Observed symptom: a UART that worked perfectly for minutes suddenly garbles every byte, or a PLL that locked at boot drifts the system clock mid-run, with no driver call that intended to touch the clock divider. Resetting the chip fixes it; it then runs fine again until it doesn't. The fault is intermittent and uncorrelated with any deliberate configuration change.
-
Waveform clue: the APB trace shows a write to the
CFGaddress (PSEL,PENABLE,PWRITEhigh,PADDR = CFG) carrying aPWDATAthat is plausible garbage — and crucially it comes from a code path that has nothing to do with the UART, in the middle of an unrelated ISR. Thecfgregister flips from its init value0x08to that garbage at that cycle, and stays there. There was no lock check in front of the commit, so the write took effect. -
Root cause: the clock-divider configuration was a plain RW register with no write-protection. It was set once at init and then left writable for the entire run, so when a buggy driver computed a wrong offset (a wild pointer landing on the
CFGaddress), the stray write committed and corrupted persistent critical state. The defect is the absence of a protection mechanism on a register that should have been locked after init — the map treated set-once config the same as freely-mutable state. -
Correct RTL: make the register lock-until-reset and have init seal it. Add a one-way
lockedlatch and gate the commit on it, so any post-lock write is dropped:if (wr_cfg && !locked) cfg <= wdata; if (wr_lock && wdata[0]) locked <= 1'b1; // 0->1 only. After boot writes the divider and setsLOCK, the identical stray write now hits a closed gate and is silently rejected —cfgholds0x08untilPRESETn. (For config that must occasionally change, use a key window instead of a permanent lock.) -
Verification assertion: prove the lock actually holds. Once
lockedis set, the value must be stable until reset, and a write without the key/unlock must not commit:Azvya Education Pvt. Ltd.VLSI MentorSnippet// Once locked, the configuration value is frozen until reset. property cfg_frozen_when_locked; @(posedge pclk) disable iff (!presetn) locked |=> $stable(cfg); endproperty assert property (cfg_frozen_when_locked); // A write to CFG while locked must NOT change cfg (stray-write-after-lock rejected). assert property (@(posedge pclk) disable iff (!presetn) (wr_cfg && locked) |=> $stable(cfg)); // A CFG write without an open key window must not commit. assert property (@(posedge pclk) disable iff (!presetn) (wr_cfg && !key_window_open) |=> $stable(cfg)); -
Debug habit: when persistent config is corrupted at runtime with no deliberate writer, do not hunt for "who meant to change it" — hunt for who could change it. Open the map and ask: is this set-once register actually locked after init? A huge fraction of "config mysteriously changed" bugs are critical registers left writable, corrupted by a stray write from elsewhere. The fix is a lock-until-reset gate on the commit, not a hunt for the one bad caller.
8. Verification perspective
Configuration protection is verified by proving the guard holds under every sequence — not just that a legal write works, but that every illegal write is rejected. The high-value checks live on the boundary between locked and unlocked, keyed and unkeyed.
- Lock holds until reset. The core assertion: once
lockedis asserted, the protected value is$stablefor every cycle untilPRESETn. Pair it with a negative check — a write to the register address while locked must not change the value — and a check thatlockeditself never goes 1→0 except through reset. This is the property that fails when someone makes the lock clearable. - Key-sequence coverage, including the wrong key. Cover the full unlock FSM: correct key then write-in-window (commits), correct key then write after the window expires (rejected), wrong key then write (rejected), and write with no key at all (rejected). The wrong-key and expired-window cases are where stray-write protection actually lives, so they must be explicitly covered, not just the happy path.
- Write-once then frozen. For write-once fields, assert the first post-reset write commits and every subsequent write is dropped until reset — drive two writes and confirm the field holds the first value. Cover the corner where the very first write is itself the lock.
- Stray-write-after-lock rejected. Directed and random tests should fire writes at the config address after it is locked (and at the key/lock addresses with garbage) and assert the protected value never moves. A constrained-random sequence that hammers the locked register is the closest analogue to the real runaway-firmware threat, and it is what catches a lock that quietly lets some writes through.
The point: verify the refusal, not just the acceptance. A test that only checks "I can write config during init" passes on a register with a broken lock; the contract is that after the lock, nothing — keyed-wrong, late, or stray — can disturb the value until reset.
9. Interview discussion
"How do you protect a configuration register?" separates engineers who name a lock bit from those who can reason about threat models and layers.
Lead with the framing: configuration registers hold persistent, set-at-init critical state, and their design is about protecting that state from corruption at runtime — by accident or by runaway code. Then name the mechanisms with their strengths: a lock bit is the default — lock-until-reset, set once after init, software cannot clear it, only PRESETn reopens it; write-once is the program-once-then-frozen primitive where the first write is the lock; and a key/magic-value unlock demands a deliberate value-and-sequence (write 0xC0DE to KEY, then the real write within N cycles) for config that is occasionally reconfigured but must never change by accident. Deliver the depth that signals real experience: a key defends against accidents, not adversaries — anyone with bus access who can read the spec can run the sequence, so it is accident protection, not security; a clearable lock is false protection because a wild write can clear it too; and register locks are a different layer from PPROT bus-access control — PPROT rejects a transaction by its origin (privilege/security), the lock rejects a write by register state, and a privileged secure master passes PPROT and is still stopped by a set lock, so robust critical config uses both. Close with the threat model: "the lock isn't there because I distrust my firmware's intent — it's there because firmware has bugs, and a wild write to a clock divider corrupts a running system." That sentence shows you protect config for reliability under failure, not just security.
10. Practice
- Design the guard. Take a plain RW clock-divider register and add a lock-until-reset bit: write the commit gate so init can write it once, then
LOCKfreezes it until reset, and confirm software cannot clear the lock. - Build a key window. Add a
KEYregister and an unlock FSM so aCFGwrite commits only if0xC0DEwas written toKEYwithin the last four cycles; trace what happens for a wrong key and for a write that arrives one cycle too late. - Choose the mechanism. For each of these, pick lock / write-once / key / PPROT and justify: a security mode set once at boot; a manufacturing trim value; a PLL retune done rarely at runtime; isolating a config block so only secure code can address it.
- Find the false lock. Given a lock bit whose RTL allows
locked <= wdata[0](settable and clearable), explain why it provides false protection and fix it to lock-until-reset. - Write the assertions. Write SVA that proves a locked config is stable until reset, that a write without the key does not commit, and that a write-once field rejects its second write.
11. Q&A
12. Key takeaways
- Configuration registers hold persistent, set-at-init critical state (clock dividers, mode selects, thresholds) — written rarely, owned by software, and required not to change at runtime — which is why their design is fundamentally about protection.
- A lock bit is lock-until-reset: set once after init, software cannot clear it, only
PRESETnreopens it. A clearable lock is false protection because a wild write can clear it too. - Write-once fields are the program-once-then-frozen primitive — the first write is the lock — ideal for trim, device ID, and security mode that must never change after boot.
- A key/magic-value unlock defends against accidents, not adversaries — it demands a deliberate value-and-sequence a stray write won't reproduce, but anyone with bus access can run it; it is accident protection, not a security boundary.
- Register locks are a different layer from
PPROTbus-access control:PPROTrejects a transaction by origin, the lock rejects a write by register state, and robust critical config uses both. The commit itself is still the 11.4 write logic — protection only adds the guard in front of it. - Verify the refusal: prove a locked config is stable until reset, that wrong-key/late/stray writes do not commit, and that write-once fields reject their second write — not just that init can write the value.