Skip to content
VLSI Mentor

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:

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

Chapter 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:

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

Three 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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));
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 measured read_plain emitting zero loads — so a FIFO drain loop written to discard values compiles to nothing.

4. The Types

Six register access types against four questions: does a write store the value, does a read return what was written, does a read have a side effect, and is read-modify-write safe on it. RW, read-write: a write stores, a read returns it, a read has no side effect, and read-modify-write is safe. RO, read-only: a write is ignored, a read returns hardware state, no side effect, and read-modify-write is pointless because the write does nothing. WO, write-only: a write stores, a read returns zero rather than the value, no side effect, and read-modify-write is broken because the read gives you nothing to modify. W1C, write-one-to-clear: writing a one clears that bit, a read returns which bits are pending, no side effect on read, and read-modify-write is actively destructive because writing back the bits you read clears every pending bit. RC, read-to-clear: the read itself clears or pops, so a read has a side effect and read-modify-write loses data on the read alone. Reserved: write what the datasheet says, usually zero or the value read, and never assume.TypeWrite stores?Read returnsit?Read has aneffect?Is RMW safe?RWread-writeyesyesnosafeROread-onlyignoredhardwarestatenopointlessthe write doesnothingWOwrite-onlyyesreads 0measurednobrokennothing to modifyW1Cwrite 1 to clear1 clears0 leaves alonewhat ispendingnodestructiveclears everypending bitRCread to clearignoredpops a valueyesthe read IS theactionloses dataon the read aloneReservedas specifiedas specifiedas specifiedread thesentencenever assume12
Figure 1 — six access types against the four questions that decide what your code may do with each. The rightmost column is the one to read first: read-modify-write, which is the default habit for touching a register, is safe on exactly one of the six. The second and third columns together are the two assumptions section 2 and section 3 measured false — a write that does not store, and a read that is not free. The bottom row is different in kind from the others: reserved is not a behaviour but an instruction addressed to you, and section 5 shows that the three instructions datasheets actually give are mutually incompatible, so there is no habit that satisfies all of them.

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
CTRL reset value = 0x00000008

What 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 saysWhat 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:

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

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

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

Four 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

  1. "Write then read back" is not verification. Measured: TXD read 0x00000000 after writing 0x5A; STATUS ignored a write of all ones; IRQ turned a write of 0x1 into 0x4.
  2. A read is not free. Measured: two reads of DATA returned 0x41 then 0x42, and STATUS changed from 0x211 to 0x10 as a consequence, with nothing written.
  3. Read once into a local (16.4 §3) — a second read is a second transaction and on an RC register it is lost data.
  4. Without volatile a discarded read is deleted (16.3 §3), so a drain loop can compile to nothing.
  5. RMW is safe on RW only. Pointless on RO, broken on WO, destructive on W1C, data-losing on RC.
  6. W0C exists and inverts W1C's convention — a reliable bug when porting a driver between devices.
  7. 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.
  8. Record the instruction in the header next to the register, with the datasheet section number.
  9. The access column is part of the instruction set, not reference material. Measured: the same str stores, is ignored, clears bits, or transmits, depending only on the address.

8. Exercises

  1. Reproduce §2's three round-trip failures against the model. For each, say what the write actually accomplished, if anything.
  2. Reproduce §3. Then write a FIFO drain loop and count its transactions for three queued bytes. Say which register each transaction targets.
  3. 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.
  4. 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?
  5. 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.
  6. The model's CTRL has 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?
  7. 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: TXD reads 0x00000000 after a write of 0x5A; STATUS ignores writes; IRQ turns a written 0x1 into 0x4.
  • A read has effects: DATA gave 0x41 then 0x42 and 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.