C Part II · Module 16 · Registers and Memory-Mapped I/O
Register Access Types: RW, RO, W1C and the Rest
Two beliefs survive almost everyone's first year of register programming: that a register reads back what you wrote to it, and that reading one has no consequences. Both are measured false here on three registers of an entirely ordinary peripheral — a write that reads back as zero, a write that is ignored, a write of 1 that clears a bit, and a read that permanently consumes data. The access-type column that tells you which is which is two or three letters wide and is the densest information in any datasheet, and this chapter is how to read it and what each entry obliges your code to do.
Advanced22 min readCPart IIHardwareRegisters
Part II · Module 16 · Chapter 16.6 · Register Access Types: RW, RO, W1C and the Rest
1. The Column
Every register table has a narrow column next to each register, and sometimes next to each field, holding two or three letters:
offset name access description
0x00 ID RO device identity
0x04 CTRL RW control
0x08 STATUS RO live status
0x0C IRQ W1C pending interrupts — write 1 to clear
0x10 DATA RC receive data — reading pops the FIFO
0x14 TXD WO transmit dataChapter 16.5 has already shown what happens when that column is skimmed: a read-modify-write on IRQ destroyed an unrelated pending interrupt, and one on DATA consumed a byte of received data, both with no concurrency involved at all.
This chapter is the column itself. It is organised around the two assumptions it exists to destroy, because an acronym list is what the datasheet already gives you and what nobody reads twice.
2. Assumption One: A Register Reads Back What You Wrote
Measured on three registers of the model, each failing differently:
/* write-only */
reg_write32(base, UART_TXD, 0x5Au);
printf("TXD read back = 0x%08x\n", reg_read32(base, UART_TXD));
/* read-only */
reg_write32(base, UART_STATUS, 0xFFFFFFFFu);
printf("STATUS after writing all ones = 0x%08x\n", reg_read32(base, UART_STATUS));
/* write-one-to-clear, with bits 0 and 2 pending */
reg_write32(base, UART_IRQ, 0x1u);
printf("IRQ after writing 0x1 = 0x%08x\n", reg_read32(base, UART_IRQ));TXD read back = 0x00000000 <- not what was written
STATUS after writing all ones = 0x00000010 <- write ignored entirely
IRQ after writing 0x1 = 0x00000004 <- a 1 CLEARED bit 0Three registers, three distinct failures of the round trip.
TXD is write-only. The write did something useful — it queued a byte for transmission — and the register has no readable storage behind it. Reading returns zero, or whatever the designer chose, and never the value you wrote.
STATUS is read-only. The write was accepted by the bus and discarded by the register logic. No fault, no indication, no effect.
IRQ is write-one-to-clear. The write had an effect and it was not storage. A 1 cleared bit 0; the 0s left bits 1 and 2 alone; the result is 0x4, which is neither what was written nor what was there before.
So "write then read back to confirm" is not a verification technique. It works on CTRL and on nothing else here, and a self-test built on it reports failures on three perfectly healthy registers while reporting success on a CTRL whose write did nothing because a clock was off.
3. Assumption Two: Reading Is Free
dev_push_rx(0x41); dev_push_rx(0x42);
printf("STATUS = 0x%08x (2 queued)\n", reg_read32(base, UART_STATUS));
printf("DATA = 0x%08x\n", reg_read32(base, UART_DATA));
printf("DATA = 0x%08x\n", reg_read32(base, UART_DATA));
printf("STATUS = 0x%08x (fifo empty)\n", reg_read32(base, UART_STATUS));STATUS = 0x00000211 (2 queued)
DATA = 0x00000041
DATA = 0x00000042
STATUS = 0x00000010 (fifo empty)Two reads of the same address returned two different values, and STATUS — a different register — changed as a consequence. Nothing was written at any point.
This is not an exotic design. It is how essentially every receive FIFO works: the read is the pop, because that makes receiving a byte one transaction instead of two and removes a window in which a second reader could take the same byte.
The consequences for your code are specific and all of them have bitten someone:
- A debugger that displays registers can consume your data merely by refreshing its view. Many debuggers know about read-to-clear registers; many do not.
- A read in a logging statement is not free.
printf("got %x\n", reg_read32(base, UART_DATA))and then using the value again reads twice and loses a byte. - Reading a register twice to extract two fields is a bug, not an inefficiency — which is why Chapter 16.4 §3 insisted on reading once into a local.
- Without
volatile, a read whose value is unused is deleted entirely — Chapter 16.3 §3 measuredread_plainemitting zero loads — so a FIFO drain loop written to discard values compiles to nothing.
4. The Types
Several others appear in real datasheets and are variations on the same ideas: W1S (write one to set), RW1C (readable, and writing one clears), WC (any write clears), W0C (write zero to clear, which inverts the W1C convention and is a reliable source of bugs when a driver is ported between devices), and RWO (read-write-once, where the first write latches and later ones are ignored — common for configuration locked at boot).
Learning the list is not the skill. Checking the column before writing the access is the skill, and the four questions in Figure 1's header are what you check it for.
5. Reserved Bits: Three Incompatible Instructions
Chapter 15.4 measured a rebuild destroying bit 3 of CTRL, and Chapter 16.4 measured a read-modify-write preserving it. The model's CTRL comes out of reset with it set:
CTRL reset value = 0x00000008What you are supposed to do about such a bit is written in the datasheet as one sentence, and the three sentences that actually appear are mutually incompatible:
| The datasheet says | What satisfies it |
|---|---|
| "Reserved. Software must preserve the value read." | read-modify-write ✓ · full rebuild ✗ |
| "Reserved. Write zero." | full rebuild ✓ · read-modify-write ✗ |
| "Reserved. Maintain the reset value." | neither, unless you know the reset value |
Read the table carefully, because it is the point of the section: the two habits are each correct for exactly one of the three instructions, and the third requires information neither of them has.
There is no universally safe default. "Always read-modify-write" preserves a bit that a write zero device wanted cleared. "Always write zero to reserved bits" clears a bit that a preserve device needed kept — which is the model's case, and which Chapter 15.4 measured. And a device that says maintain the reset value is telling you that neither the current value nor zero is necessarily right, so the value has to be recorded at reset or hard-coded from the document.
The practical approach has three parts:
Read the sentence, once, when you transcribe the register map, and write the answer into the header next to the register:
#define UART_CTRL 0x04u /* RW. bits 31:24 and bit 3 reserved:
datasheet rev C §12.4 — PRESERVE the value read */Default to read-modify-write, because preserve is the most common instruction and because it is also the right behaviour for every non-reserved field you are not touching.
Pin the reset value when the instruction demands it:
#define UART_CTRL_RESET UINT32_C(0x00000008)
_Static_assert((UART_CTRL_RESET & 0x8u) != 0u, "bit 3 is set at reset — see §12.4");6. The Misconception: The Access Column Is Documentation
"RW, RO, W1C — that is reference material. I will look it up if something goes wrong."
It is not reference material. It is the part of the device's instruction set that tells you what a load and a store mean at that address.
Chapter 16.1 §2 measured one str instruction going out to the bus carrying an address, a width, a direction and data. That instruction is identical for every register in the block. What differs — entirely — is what the register logic at the other end does with it, and the access column is the only place that is written down.
Measured in this chapter, the same str at four addresses of one peripheral:
str to CTRL -> stores the value
str to STATUS -> ignored entirely
str to IRQ -> clears the bits that were 1
str to TXD -> queues a byte for transmissionFour meanings, one instruction. Nothing in the C source distinguishes them, nothing in the compiler's output distinguishes them, and no warning flag, analyser or sanitizer can distinguish them, because the program is correct in every respect that a tool can inspect. The information lives in a document.
So the rule is not "look it up when something goes wrong". It is:
Check the access type before you write the access, and put the answer in the header where the next reader will see it.
The cost is one lookup per register, once, at transcription time. Chapter 16.5 measured the cost of skipping it: a lost interrupt and a lost byte, in code that looked right to everyone who reviewed it.
7. What You Should Be Able to Say Now
- "Write then read back" is not verification. Measured:
TXDread0x00000000after writing0x5A;STATUSignored a write of all ones;IRQturned a write of0x1into0x4. - A read is not free. Measured: two reads of
DATAreturned0x41then0x42, andSTATUSchanged from0x211to0x10as a consequence, with nothing written. - Read once into a local (16.4 §3) — a second read is a second transaction and on an RC register it is lost data.
- Without
volatilea discarded read is deleted (16.3 §3), so a drain loop can compile to nothing. - RMW is safe on RW only. Pointless on RO, broken on WO, destructive on W1C, data-losing on RC.
- W0C exists and inverts W1C's convention — a reliable bug when porting a driver between devices.
- Reserved bits carry three incompatible instructions: preserve the value read (RMW ✓, rebuild ✗), write zero (rebuild ✓, RMW ✗), maintain the reset value (neither, without the reset value). There is no universally safe default.
- Record the instruction in the header next to the register, with the datasheet section number.
- The access column is part of the instruction set, not reference material. Measured: the same
strstores, is ignored, clears bits, or transmits, depending only on the address.
8. Exercises
- Reproduce §2's three round-trip failures against the model. For each, say what the write actually accomplished, if anything.
- Reproduce §3. Then write a FIFO drain loop and count its transactions for three queued bytes. Say which register each transaction targets.
- Write a
register_selftest()that writes a pattern and reads it back for all six of the model's registers. Run it, then explain every "failure" it reports and say what the function is actually worth. - For each of the model's six registers, fill in Figure 1's four columns from the header comments. Which of the six is read-modify-write safe on?
- A driver written for a device with W1C interrupt acknowledgement is ported to one that uses W0C. Describe the symptom precisely, and say whether any compiler diagnostic could catch it.
- The model's
CTRLhas bit 3 reserved and set at reset. Write the field update for each of §5's three datasheet instructions. Which two are the same code? - Find a register description in any datasheet you have access to with a reserved field. Quote the exact sentence, classify it against §5's table, and write the header comment you would add.
9. Summary
- Round trip fails three ways, measured:
TXDreads0x00000000after a write of0x5A;STATUSignores writes;IRQturns a written0x1into0x4. - A read has effects:
DATAgave0x41then0x42and emptied the FIFO, with nothing written. - Read once into a local; a second read is a second transaction and may be lost data.
- RMW is safe on RW alone — pointless on RO, broken on WO, destructive on W1C, data-losing on RC.
- Also real: W1S, RW1C, WC, W0C (inverts W1C — a porting bug), RWO.
- Reserved bits: three mutually incompatible instructions. Preserve suits RMW; write zero suits a rebuild; maintain the reset value suits neither. No universal default exists.
- Write the instruction into the header with its datasheet section.
- The access column is the device's instruction set — one
str, four meanings, no tool can tell them apart.
The column has been read and the register's behaviour is now known. Expressing a whole block of them in C is the remaining structural question, and it has a tempting answer — a structure laid over the address — whose attractions and hazards Part I has already measured in a different context, plus one hazard that is specific to registers and that rules out the construct people reach for first.
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.
