Skip to content

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. PPROT is 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: PPROT can 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 PRESETn reopens 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.
A CFG register protected by a commit gate that requires LOCK equal to zero and a matched key window; the write-once lock-until-reset LOCK bit and an unlock FSM feed the gate, a green path shows a committed write, and a red dashed path shows a stray write dropped because LOCK was set.
Figure 1 — a configuration register guarded by a lock bit and a key-unlock sequence. An APB write to the CFG register flows into a commit gate that fires only when both conditions hold: the LOCK bit is zero, and the magic KEY value (0xC0DE) was written to the KEY register within the unlock window. The LOCK bit is write-once and lock-until-reset: once software sets it, it stays set until PRESETn and every further CFG write is dropped; software cannot clear it. The KEY register runs a small unlock FSM that arms a short window after the magic value, so a stray write that lacks the key sequence is rejected. The green path shows a legal committed write (cfg <= PWDATA); the amber blocks show the locked and unarmed states that reject; the red path shows a stray write from runaway firmware being silently dropped because LOCK was already set. The figure frames configuration protection as a register-level guard on the write itself, distinct from bus-level PPROT access checks.

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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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;
endcase

Three facts make this configuration design and not just a flop. First, the guard is the designcfg_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 purposelocked 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.

MechanismStrengthWhat it defends againstWhen to use
Lock bit (lock-until-reset)Strong, simple, irreversible until resetAny runtime write after init — stray, buggy, or malicious — because the gate is closed and software cannot reopen itConfig set once at boot and never changed for the run (clock dividers, security mode, calibration); the default config guard
Write-once fieldStrong for a single field, no extra registerA 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 accessStray/random writes from runaway code; requires a deliberate value+sequence a wild write won't reproduceConfig that is changed occasionally at runtime but must never change by accident (PLL retune, watchdog disable)
PPROT bus-access protectionStrong on origin, not on intent — a different layerAn unauthorised requester (wrong privilege/security level) reaching the register at all — checked at the bus-access layer, Module 9.4Isolating 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 CFG address (PSEL, PENABLE, PWRITE high, PADDR = CFG) carrying a PWDATA that 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. The cfg register flips from its init value 0x08 to 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 CFG address), 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 locked latch 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 sets LOCK, the identical stray write now hits a closed gate and is silently rejected — cfg holds 0x08 until PRESETn. (For config that must occasionally change, use a key window instead of a permanent lock.)

  • Verification assertion: prove the lock actually holds. Once locked is set, the value must be stable until reset, and a write without the key/unlock must not commit:

    Azvya Education Pvt. Ltd.VLSI Mentor
    Snippet
    // 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.

Two cases of a stray write to a clock-divider CFG register: the top buggy case has no protection so the write commits and corrupts the value, glitching the clock in red; the bottom correct case has a LOCK bit, so the commit gate sees LOCK equals one and drops the write, holding the value stable in green.
Figure 2 — the same stray write hitting two register designs. Top (bug, red): the clock-divider CFG register has no protection, so a stray write from buggy firmware commits, overwrites the init value 0x08 with garbage 0xFF, and glitches the system clock at runtime — silent corruption of persistent critical state. Bottom (correct, green): the identical CFG register is guarded by a LOCK bit that firmware set after init; the same stray write reaches the commit gate, the gate sees LOCK equals one, and the write is dropped, so the divider holds 0x08 stable until reset and the clock is undisturbed. The figure shows the defect is the absence of a register-level lock on set-once config, and the fix is a lock-until-reset gate that rejects writes after the lock rather than trusting firmware never to misbehave.

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 locked is asserted, the protected value is $stable for every cycle until PRESETn. Pair it with a negative check — a write to the register address while locked must not change the value — and a check that locked itself 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 controlPPROT 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

  1. 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 LOCK freezes it until reset, and confirm software cannot clear the lock.
  2. Build a key window. Add a KEY register and an unlock FSM so a CFG write commits only if 0xC0DE was written to KEY within the last four cycles; trace what happens for a wrong key and for a write that arrives one cycle too late.
  3. 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.
  4. 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.
  5. 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 PRESETn reopens 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 PPROT bus-access control: PPROT rejects 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.