Skip to content
VLSI Mentor

C Part II · Module 16 · Registers and Memory-Mapped I/O

Register Programming Bugs

Every other chapter in this module went from a cause to its effect. On a bench you work the other way: a device ignores a setting, an interrupt goes missing once an hour, a character is dropped under load — and you have to get from there to the line. This chapter inverts the module into a diagnostic table, with every symptom backed by a measurement made earlier rather than by a plausible story, and it corrects the instinct that makes these bugs take days: reaching for a debugger, which reads registers that may be consumed by being read, in a build where the most common of these failures does not occur.

Expert23 min readCPart IIHardwareRegisters

Part II · Module 16 · Chapter 16.9 · Register Programming Bugs

1. Working Backwards

Everything so far has run forwards: here is a mistake, here is what it does. On a bench you start at the other end. Something is wrong with a device, and the available evidence is a symptom — a setting that does not take, an interrupt that is occasionally missed, a character lost under load.

The useful property of register bugs is that there are not many of them. The same handful recur across every device and every company, and each has a signature specific enough to identify it from the symptom alone. This chapter is that table.

Every entry is backed by a measurement from earlier in this module, not by a plausible-sounding story. That matters: a diagnostic table built from folklore sends you to the wrong place with confidence.

Six recurring register bugs, each given by the symptom you observe first, the cause, and the check that confirms it. The device ignores the setting entirely: the offset is wrong so you configured a different register; check the computed address against the datasheet. Only part of the value takes effect: the access width is wrong, so an 8-bit write updated one byte of a 32-bit register; check the pointer type. One field is right and a neighbour was destroyed: the value was not masked before shifting into position; check for a mask after the shift. An interrupt is lost occasionally: read-modify-write on a write-one-to-clear register; write only the bit to clear. Data goes missing under load: a read with a side effect was performed twice, once for a condition and once for the value; read once into a variable. A polling loop never exits: volatile was omitted, so the load was hoisted out of the loop; check the generated code for the load inside the loop.SymptomCauseThe checkThe setting doesnothingwrong offsetyou configured anotherregisterprint the computedaddresscompare with the datasheetOnly part of it takeseffectwrong access widthan 8-bit write to a 32-bitregistercheck the pointer typeit decides the instructionA neighbouring field iswreckedvalue not masked beforeshiftingmask after the shift15.4 measured the spillAn interrupt is lostnow and thenread-modify-write onW1Cwrite only the bit toclearData goes missing underloada side-effecting readdone twiceread once into avariableThe poll never exitsvolatile omittedthe load was hoisted outread the generated codeis the load inside the loop?12
Figure 1 — six recurring register bugs indexed by what you observe first, which is the only index that is useful at a bench. Read the middle column as a hypothesis and the right-hand column as the thing that confirms or kills it in minutes. Notice how many of the checks are static: printing a computed address, looking at a pointer type, reading generated code. Only two of the six require the device to be running at all, which is the opposite of how these are usually investigated, and is the reason section 7 argues against reaching for a debugger first.

2. The Setting Does Nothing

Symptom. You configure the device and nothing changes. Reading the register back gives the reset value, or a value you did not write.

Cause: a wrong offset. Chapter 16.2 §3 measured the canonical form — a byte offset added to a typed pointer:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
right = 0x40021004      (CTRL)
wrong = 0x40021010      (DATA)

The four-byte offset was scaled by four. The write went to DATA, which ignores writes, and CTRL was never touched:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
CTRL = 0x00000008   <- never written, still the reset value

Other causes with the same symptom. The wrong instance base, so you configured UART1 while watching UART0. A register that needs an enable or a clock ungated first. A write-protected or write-once register (Chapter 16.6's RWO).

The check. Print the computed address and compare it with the datasheet — Chapter 16.2 §6. Two minutes, no hardware, and it distinguishes the first two causes immediately. If the address is right, read the register back: the reset value means the write did not land; a different value means it landed somewhere unexpected.

3. Only Part of It Takes Effect

Symptom. Some of what you wrote is there and some is not — typically the low byte or the low half.

Cause: the wrong access width. Chapter 16.3 §2 measured that the pointer type is the instruction — uint8_t * gives strb, uint16_t * gives strh, uint32_t * gives str — and Chapter 16.6 measured the effect on the model:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
CTRL after 32-bit write of 0x00081A3D = 0x00081a3d
CTRL after  8-bit write of 0xFF       = 0x00081aff   <- only the low byte changed

The check. Look at the pointer type in the access, not at the value being written. This is a static check and takes seconds. A uint8_t * or a memcpy of a non-word size against a register documented as 32-bit is the bug.

The subtle variant. Chapter 16.1 §5 measured the compiler narrowing a 32-bit load to ldrb on its own, because only one bit of the result was used — which happens only when volatile was omitted. If the pointer type is right and the width is still wrong, you are looking at §7's bug instead.

4. A Neighbouring Field Is Wrecked

Symptom. The field you set is correct and an unrelated one in the same register has changed — a wrong baud rate appearing after a change to the mode.

Cause: the value was not masked before being shifted into position. Chapter 15.4 §5 measured it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
unmasked 4095 into a 2-bit field at pos 1 : 0x00001ffe   <- spills
PUT()    4095 into a 2-bit field at pos 1 : 0x00000006   <- truncated

An out-of-range value shifted to a field's position occupies bits above the field, and | sets them in whatever field lives there.

The check. Find the insert expression and confirm the mask is applied after the shift, or that the value is masked to the field's width before it. Chapter 15.4 §2's PUT does this by construction, which is the argument for using an accessor rather than writing the expression inline.

The related variant, worth checking at the same time: a full rebuild of the register from named fields, which Chapter 15.4 §6 measured silently clearing reserved bit 3 — 0x00081A3D becoming 0x00081A35. The symptom there is not a wrecked neighbour but a device that misbehaves after configuration in a way that does not correspond to any field you set.

5. An Interrupt Is Lost Now and Then

Symptom. Occasionally an event is never handled. Rare, load-dependent, and impossible to reproduce on demand — the worst signature a bug can have.

Cause: a read-modify-write on a write-one-to-clear register. Chapter 16.5 §2 measured it with no concurrency at all:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
pending before      = 0x00000005
pending after RMW   = 0x00000000   <- bit 2 is GONE too
pending after W1C   = 0x00000004   <- correct form

Acknowledging your interrupt cleared somebody else's. It only shows up when two sources are pending simultaneously, which is why it is load-dependent and rare.

The check. Look at the acknowledgement. IRQ |= bit or a read followed by a write is the bug; a bare write of the bit is correct:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
reg_write32(base, UART_IRQ, UART_IRQ_RX);   /* correct: one write, one bit */

This is a grep, not a debugging session. Search the driver for |= and &= on any register whose access type is not RW, and check each one against Chapter 16.6's table.

The genuinely concurrent variant. The same symptom arises from Chapter 16.5 §4's window on a plain RW register, when an interrupt handler and the main path both read-modify-write it. That one needs a different fix — hardware set/clear registers, or a single-owner rule — and it is distinguished from the W1C case by the register's access type, which is why §2 of that chapter insisted the two failures are different.

6. Data Goes Missing Under Load

Symptom. Characters or packets are dropped, but only when traffic is heavy.

Cause: a register with a read side effect was read more than once. Chapter 16.6 §3 measured the mechanism — reading DATA pops the FIFO — and Chapter 16.5 §3 measured a read-modify-write consuming a byte:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
fifo level before = 3
fifo level after  = 2   <- a byte was consumed by the read alone

The common shapes are a value read once for a condition and again for its value, a debug printf that reads the register, and a read-modify-write on a read-to-clear register.

The check. Every register whose access type is RC must be read exactly once per item, into a variable:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
uint8_t byte = (uint8_t)reg_read32(base, UART_DATA);   /* ONE read */
if (byte == '\n') { ... }
process(byte);

Then grep for the register's name and count the reads per path. Chapter 16.4 §3 gave the general form of this rule — read once into a local — and here it stops being about efficiency.

Why it is load-dependent: with an empty FIFO the extra read returns zero and is harmless. It only destroys data when there is data to destroy.

7. The Poll Never Exits

Symptom. The device is visibly ready — a logic analyser or a second reader shows the status bit set — and the program is stuck in a wait loop.

Cause: volatile was omitted. Chapter 16.1 §5 measured both halves of this at -O2:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
_wait_ready_plain:
    ldrb  w8, [x8]            <- ONE byte load, BEFORE the loop
    tbnz  w8, #4, LBB3_2
LBB3_1:
    b     LBB3_1              <- the loop body is an infinite branch, no access

The register is read once, before the loop. If the bit is clear at that instant, the program spins forever and never looks again. Chapter 16.3 §3 isolated the two underlying properties: a discarded read was deleted entirely (read_plain: zero loads) and two writes were merged into one (write_twice_plain: one store).

The check. Read the generated code for the loop and look for the load inside it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
cc -std=c17 -O2 -S driver.c -o - | sed -n '/_wait_ready/,/ret/p'

A loop body containing no load is the bug, visible in ten seconds and without any hardware.

The signature that identifies this one before you look: the bug disappears at -O0 and at -O1-with-different-surrounding-code, and it appears or moves when unrelated code changes. Anything whose presence depends on optimisation level is in this family.

8. A Checklist for a Register Access You Did Not Write

Reviewing somebody else's driver, or your own after six months, the six questions are:

  1. Is the address right? Base, offset, instance. Print it.
  2. Is the width right? Look at the pointer type, not the value.
  3. Is volatile on the object, not the pointer? volatile uint32_t *p, not uint32_t *volatile p (16.3 §4).
  4. Is the access type honoured? Any |=, &= or read-then-write on a non-RW register is suspect (16.6).
  5. Is a side-effecting register read exactly once per item? (16.6 §3)
  6. Are inserted field values masked after shifting? (15.4 §5)

Five of the six are answerable by reading the code. The sixth — whether the access type is honoured — requires the datasheet, which is why Chapter 16.8 §7 argued for putting the access type in the header next to the register.

9. Module 16 in One Page

The module traced one store from a C statement to a flip-flop and then spent eight chapters on the gap between a register looks like memory and a register is a device. Every result was executed against a software model of a peripheral whose registers implement their documented access types, because real memory-mapped I/O cannot run on this machine — Chapter 14.6 measured a datasheet address giving exit 139.

ChapterThe measured result
16.1One assignment → str w0, [x8], x8 = 0x40021004. Without volatile, the poll's load was hoisted out of the loop and narrowed to ldrb
16.2(uint32_t *)base + 0x04 reaches 0x40021010 = DATA, which consumed a received byte while CTRL stayed at 0x00000008
16.3strb/strh/str from the pointer type; volatile preserves a discarded read (1 vs 0) and a repeated write (2 vs 1); uint32_t *volatile spills the pointer and protects nothing
16.4Field update 0x00081a3d → 0x00083e8d, reserved bit intact, 1 read 1 write; four fields in one cycle instead of eight transactions
16.5RMW on W1C: pending 0x5 → 0x0, single-threaded. Correct form: 0x4, 1 transaction instead of 2
16.6TXD reads 0x00000000; STATUS ignores a write; IRQ turns 0x1 into 0x4; DATA gives 0x41 then 0x42 and empties the FIFO
16.7Overlay sizeof 0x18, offsets exact; a gapped block needs a hand-computed pad; the bit-field version produced byte-for-byte the right word and is implementation-defined
16.8The datasheet's four categories, mirroring 13.3's
16.9Six symptoms, each traced to a measurement above

Three threads run through all nine.

The instruction is identical; the thing on the other end is not. One str stored a value, was ignored, cleared bits, or transmitted a byte, depending only on the address. No compiler, flag, analyser or sanitizer can distinguish those, because the program is correct in every respect a tool can inspect. The information is in a document.

Almost every failure was silent. Not one of this module's measured bugs produced a diagnostic. The wrong offset, the wrong width, the lost interrupt, the consumed byte, the deleted loop — all compiled clean, most at -Weverything. This is Chapter 13.1's thesis holding at the hardware boundary.

The checks that work are static. Printing an address, reading a pointer type, reading generated code, grepping for |= on a W1C register. The bench instruments come second, and the debugger — the instinctive first move — can consume the data whose disappearance you are investigating.

10. What Comes Next

This module has used volatile as a rule and deferred the explanation five separate times: in 16.1 §5 and §7, in 16.3 §7, in 16.5 §4 and in 16.8 §5. Those debts are all owed to one module.

Module 17, volatile for Hardware Engineers, is short and earns its place by being precise. It opens with the thing this module kept measuring and never explained — why the optimiser is entitled to remove a register access, which is a statement about its model of memory (17.1). It then states what volatile actually means in terms of the C abstract machine (17.2), applies it to register access with the qualifier in the right place (17.3), and extends it to the case this module deliberately avoided: an object that changes outside the current flow of control, where volatile sig_atomic_t turns out to be the only thing C actually guarantees (17.4).

Then the two chapters that matter most for anyone who has read Module 16. 17.5 separates volatile from atomicity, from mutual exclusion, and from any ordering against non-volatile accesses — the three guarantees folklore attaches to it, each of which this module has had to disclaim in passing. And 17.6 distinguishes what the compiler may reorder from what the processor and interconnect may reorder, and names which mechanism enforces each — because volatile enforces neither.

That last clause is the one to carry forward. Chapter 16.5 §4 described a window between a read and a write and said plainly that volatile does not close it. Module 17 is where that stops being a warning and becomes a mechanism.

11. What You Should Be Able to Say Now

  1. Work from the symptom. There are about six recurring register bugs and each has a signature identifiable from what you observe.
  2. Setting does nothing → wrong offset or wrong instance. Measured: the scaling error reaches DATA and leaves CTRL at its reset value. Print the address.
  3. Only part takes effect → wrong access width. Measured: an 8-bit write gave 0x00081aff. Check the pointer type.
  4. A neighbour is wrecked → the value was not masked before the shift. Measured: 0x1ffe against 0x6.
  5. An interrupt is lost occasionally → RMW on W1C. Measured: 0x5 → 0x0. Load-dependent because it needs two sources pending. Grep for |= on non-RW registers.
  6. Data missing under load → a side-effecting register read more than once. Measured: FIFO 3 → 2. Load-dependent because an empty FIFO makes the extra read harmless.
  7. A poll never exits → volatile omitted; the load was hoisted out and the body became b LBB3_1. Anything that depends on optimisation level is in this family.
  8. A debugger is the wrong first tool: it reads registers that may be consumed by reading, and the most common of these bugs does not exist at -O0.
  9. The first three checks are static — address printout, generated code, grep — and catch four of the six.

12. Exercises

  1. For each of Figure 1's six rows, write a short program against the model that exhibits the symptom. Then apply the stated check and confirm it identifies the cause.
  2. Take §2's wrong-offset case and add the address printout from Chapter 16.2 §6. Show that it reveals the bug before any transaction is issued.
  3. Write a driver function with the §5 bug — IRQ |= bit — then write the grep that finds it across a codebase. What false positives does your grep produce, and how would you reduce them?
  4. Reproduce §7 and confirm the loop at -O2 and at -O0. Explain why testing a debug build would have reported the hardware as intermittent.
  5. Apply §8's six-question checklist to a register access in code you did not write. Record which questions you could answer from the code alone.
  6. §5 has two causes with the same symptom — the W1C bug and the concurrent-RMW window. Write the test that distinguishes them, and say which chapter established that they are different kinds of failure.
  7. A colleague reports that a setting works on one board and not another, with identical software. Using Chapter 16.8's categories, list what you would ask them for, in order.

13. Summary

  • Index bugs by symptom, because that is the direction a bench engineer works in. There are about six.
  • Nothing does anything → wrong offset/instance; measured reaching DATA and leaving CTRL at reset. Print the address.
  • Partial effect → wrong width; measured 0x00081aff. Check the pointer type.
  • Neighbour wrecked → unmasked value shifted into place; 0x1ffe against 0x6.
  • Occasional lost interrupt → RMW on W1C; 0x5 → 0x0, load-dependent.
  • Data lost under load → side-effecting read performed twice; FIFO 3 → 2.
  • Poll never exits → volatile omitted; loop body became b LBB3_1. Optimisation-dependent symptoms belong here.
  • The debugger is the wrong first tool — it reads RC registers, and the commonest bug vanishes at -O0.
  • Three static checks catch four of six: address printout · generated code · grep for |= on non-RW registers.

Standards & specifications

Governing standard
ISO/IEC 9899 (the C language standard)(opens ISO/IEC (JTC 1/SC 22/WG 14) in a new tab)

Defines the C language itself — the type system, expression semantics, object model, storage duration and the behaviour categories a conforming implementation must honour. It is a FAMILY of editions (C89/C90, C99, C11, C17, C23) and this curriculum names the edition wherever a claim depends on one. It does not define any foreign-function interface to a hardware description language, and it does not define platform behaviour: what a memory-mapped access does on real silicon is the implementation's and the device's contract, not this standard's. Working drafts published by WG14 are freely available and are what most engineers read day to day; a draft is not the published standard.

This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.

Where this fits

Part of the C Programming curriculum.