Skip to content
VLSI Mentor

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

Read-Modify-Write

Read the interrupt-pending register, set the bit you want to acknowledge, write it back: a textbook read-modify-write, and it silently discards every other interrupt that was pending. Measured here with no concurrency at all — single-threaded, no interrupt handler, nothing racing — because the sequence is not merely unsafe on that register, it is wrong in principle. This chapter separates those two failures, which get conflated constantly: the ones caused by what happens between the read and the write, and the ones that would happen on a machine with one instruction and all the time in the world.

Advanced22 min readCPart IIHardwareRegisters

Part II · Module 16 · Chapter 16.5 · Read-Modify-Write

1. The Sequence

To change one field of a register you cannot modify it in place. You fetch the word, alter the bits you own, and send it back:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
uint32_t ctrl = reg_read32(base, UART_CTRL);            /* read   */
ctrl &= ~FIELD_MASK(CTRL_DIVISOR_POS, CTRL_DIVISOR_W);  /* modify */
ctrl |=  PUT(divisor, CTRL_DIVISOR_POS, CTRL_DIVISOR_W);
reg_write32(base, UART_CTRL, ctrl);                     /* write  */

Chapter 16.4 measured this working: 0x00081a3d became 0x00083e8d, one field changed, every other field and the reserved bit intact, one read and one write.

That is the sequence working, on the register it was designed for. The rest of the chapter is the three ways it does not.

The crucial distinction, which gets lost constantly:

  • Unsafe means something else changes the register between your read and your write. This is a concurrency problem and it goes away if nothing else touches the register.
  • Wrong means the sequence does not do what you want even on a machine running one instruction at a time with nothing else in the system. This is a property of the register, and no amount of locking fixes it.

The second kind is much less well known and is where this chapter starts.

2. Simply Wrong, Case One: Write-One-to-Clear

An interrupt-pending register records which interrupt sources are asserted. Acknowledging one is the most routine operation in a driver. Here is the textbook read-modify-write, run single-threaded with no interrupt handler and nothing else in the system:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
dev_raise_irq(0x5u);                        /* bits 0 and 2 become pending */

uint32_t v = reg_read32(base, UART_IRQ);    /* read   */
v |= 0x1u;                                  /* modify: mark bit 0 to clear */
reg_write32(base, UART_IRQ, v);             /* write  */
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
pending before      = 0x00000005
intent: clear bit 0 only, leave bit 2 pending
pending after RMW   = 0x00000000   <- bit 2 is GONE too

Bit 2 was a pending interrupt belonging to some other part of the driver. It has been acknowledged without ever being handled, and the event it represented is lost permanently.

The mechanism is simple once stated. UART_IRQ is write-one-to-clear: writing a 1 to a bit clears it; writing a 0 leaves it alone. So the register does not store what you write, and writing back what you read means "clear every bit that was set" — which is the exact opposite of preserving them.

The read was not just unnecessary, it was actively harmful: it supplied the value that destroyed the other bits.

The correct form writes only the bit being acknowledged, and does not read at all:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
reg_write32(base, UART_IRQ, 0x1u);          /* write ONLY the bit to clear */
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
pending after W1C   = 0x00000004   <- bit 2 survived

Counted on the model, the two forms are:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
RMW ack : 1 read + 1 write = 2 transactions -> pending 0x00000000
W1C ack : 0 read + 1 write = 1 transaction  -> pending 0x00000004

Half the bus traffic, and correct.

3. Simply Wrong, Case Two: A Read With a Side Effect

The second failure is at the other end of the sequence.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
uint32_t d = reg_read32(base, UART_DATA);   /* read   */
reg_write32(base, UART_DATA, d);            /* write back */
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
fifo level before   = 3
read 0x41 then wrote it back
fifo level after    = 2   <- a byte was consumed

UART_DATA is read-to-clear: reading it pops a byte off the receive FIFO. The read step of the read-modify-write consumed a byte of received data, and the write-back did nothing at all because the register ignores writes.

Note what makes this worse than §2: the damage was done by the read, which means it happens even if you decide not to write, even if you abandon the operation, and even if the whole thing is wrapped in the most careful locking imaginable. There is no sequence of operations that includes reading this register and does not consume data.

Chapter 16.2 §3 measured this happening entirely by accident, from a four-byte address error. Here it is happening on purpose, from a habit.

Both of these registers are in a perfectly ordinary peripheral, and this is the point: W1C and read-to-clear are not exotic. An interrupt-pending register and a receive-data register are in essentially every device you will program.

Chapter 16.6 is the taxonomy — what the access types are, how to read the column in a datasheet, and what each one means for your code. It comes after this chapter deliberately: the failures are what make the taxonomy worth reading.

4. Unsafe: The Window

The third failure is the one people do know about, and it is the only one that is genuinely a concurrency problem.

Between the read and the write there is a gap. If anything changes the register in that gap, your write destroys the change:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
your code                     the other writer
---------                     ----------------
read CTRL  -> 0x00081A3D
                              sets ENABLE: CTRL becomes 0x00081A3D | 1
modify divisor in a local
write CTRL <- 0x00083E8C      <- ENABLE is back to 0

The other writer can be an interrupt handler, another core, a DMA engine, or the hardware itself updating a status bit inside the same register. Your write is based on a snapshot that is no longer true, and the update that happened in the window is simply overwritten.

This one is not measured here, and that is deliberate. Demonstrating it requires a real interrupt arriving between two instructions, which single-threaded C on this machine cannot produce. Chapter 13.2 established what this curriculum does with behaviour it cannot observe: it states the reasoning and declines to fabricate the trace. The sequence above is a description of the mechanism, not a transcript of a run.

What can be said precisely is which mechanisms do and do not help:

volatile does not help. It guarantees that both accesses happen, at the right width, in the order written. It says nothing about what happens between them. Chapter 16.3 §7 stated this and Chapter 17.5 is where it is explained properly — it is the single most common misunderstanding about volatile, and believing it here produces code that is confidently wrong.

Hardware support helps, where it exists. W1C is one form. Many devices also provide set and clear registers — write a 1 to CTRL_SET to set those bits, to CTRL_CLR to clear them — which turn a read-modify-write into a single write with no window at all. If your device has them, use them; that is what they are for.

Disabling interrupts helps, for the interrupt case only. It closes the window against a handler on the same core and does nothing about another core or a DMA engine.

Making the register single-owner helps most. If exactly one piece of code ever writes CTRL, there is no window to race over. This is Chapter 13.11's ownership habit — one owner per resource — applied to a register instead of an allocation, and it is the cheapest of the four because it costs nothing at runtime.

Four stages of a read-modify-write with the hazard attached to each. Stage one, read the register: this is already a hazard if the read has a side effect, because on a read-to-clear register the read alone consumes data — measured, a FIFO level fell from three to two. Stage two, modify one field: safe arithmetic on a local copy, and the only stage with no hardware hazard. Stage three, write it back: this is where write-one-to-clear registers break, because writing back the bits you read clears every pending bit — measured, pending went from 0x5 to 0x0 when only bit 0 was meant to be cleared. Stage four, the window between read and write: anything that changes the register in that gap is lost, whether an interrupt handler, another core, or the hardware itself. The conclusion is that read-modify-write is correct only on a read-write register whose read has no side effect and which nothing else writes concurrently.Read-modify-write, and the three places it can be wrong01READ THE REGISTERhazard: on an RC register the read itself consumes data — measured 3 to 202MODIFY ONE FIELD IN A LOCAL COPYclear with ~mask, shift the value in, combine — the only safe stage03WRITE IT BACKhazard: on a W1C register this clears every pending bit — measured 0x5 to 0x004THE WINDOW BETWEEN READ AND WRITEhazard: an interrupt, another core, or the hardware writes in the gap and islost
Figure 1 — the sequence with the hazard attached to each stage, which is the useful way to hold it because the three failures attach at three different places. Stage 1 and stage 3 are properties of the register and fail with no concurrency at all: the read consumes data on a read-to-clear register, and the write-back clears everything pending on a write-one-to-clear register, both measured. Stage 4 is the concurrency hazard and is the only one people usually name. Stage 2 is the only step with no hardware hazard whatsoever, and is also the only one that Module 15 was about — which is a fair summary of how much of this module is not arithmetic.

5. When Read-Modify-Write Is Right

Having spent four sections on failures, the positive statement matters:

Read-modify-write is correct on a read-write register whose read has no side effect, which nothing else writes concurrently, and whose unnamed bits must be preserved.

That describes most control and configuration registers, which is why the technique is standard. CTRL in the model is exactly such a register, and Chapter 16.4's measured update — 0x00081a3d to 0x00083e8d with the reserved bit intact — is the technique doing its job.

The checklist before using it on a register you have not used before is four questions, all answerable from the datasheet's access-type column:

  1. Is it RW? If it is RO, the write does nothing. If it is WO, the read gives you nothing to modify.
  2. Does the read have a side effect? If yes, you cannot read it as part of an update.
  3. Does a write mean "store this"? If it means "clear these" or "do this", writing back what you read is a command you did not intend to issue.
  4. Does anything else write it? An interrupt handler, another core, a DMA engine, or the hardware.

A yes to 1 and no to 2, 3 and 4 means read-modify-write is the right tool. Any other combination means stop and find the mechanism the hardware provides — a set/clear register pair, a W1C write, a single-owner rule — because there almost always is one.

6. The Misconception: RMW Is the Safe Way

"Always read-modify-write a register, so you do not clobber the other bits."

It is excellent advice for one kind of register and actively destructive on others, and the advice is usually given without the qualifier.

The belief comes from a real and correct observation — Chapter 15.3 established that masked operations preserve what they do not name, and Chapter 15.4 measured a full rebuild destroying a reserved bit that a read-modify-write preserved. Both are true, and both are about a register that stores what you write.

The failure is in the generalisation. "Preserve the other bits" assumes those bits can be preserved by writing them back, and that assumption is false for W1C (writing them back clears them) and vacuous for RC (the damage is done before you write anything). Measured, with no concurrency involved: 0x5 became 0x0, and a FIFO lost a byte.

The corrected rule is one sentence longer and worth the extra clause:

Read-modify-write preserves the bits you did not name, on a register that stores what you write. Check that it does before you use it.

And the operational version: the access-type column is not documentation, it is part of the instruction set of the device. Reading it is Chapter 16.6.

7. What You Should Be Able to Say Now

  1. Read, modify, write — three steps, two of them accesses, with a gap between.
  2. "Unsafe" and "wrong" are different failures. Unsafe is concurrency and goes away if nothing else touches the register. Wrong is a property of the register and no locking fixes it.
  3. RMW on a W1C register destroys unrelated pending bits. Measured single-threaded: 0x5 → 0x0 when only bit 0 was to be cleared. The correct form writes only that bit: 0x5 → 0x4, one transaction.
  4. W1C exists to make acknowledgement a single safe transaction. Read-modify-write reintroduces the problem the hardware solved.
  5. RMW on a read-to-clear register loses data at the read. Measured: FIFO 3 → 2. No sequence that reads that register avoids it, and no locking helps.
  6. The window is real and is the only concurrency failure. It is reasoned here rather than measured, because a fabricated race trace would be worth nothing.
  7. volatile does not close the window. It preserves the accesses; it says nothing about what happens between them (17.5).
  8. What does help: hardware set/clear registers, a W1C write, disabling interrupts for the handler case only, and a single-owner rule — which costs nothing.
  9. The four-question checklist: RW? read side-effect-free? does a write mean store? does anything else write it?

8. Exercises

  1. Reproduce §2 against the model. Then write the correct single-write acknowledgement and confirm bit 2 survives. Count the transactions in both.
  2. Reproduce §3 and confirm the FIFO level falls. Then write a loop that drains the FIFO correctly, and say how many transactions it performs for three queued bytes.
  3. For each of the model's six registers, answer §5's four questions from the comments in the header. Which ones is read-modify-write correct on?
  4. A device provides CTRL, CTRL_SET and CTRL_CLR. Write enable() and disable() using the set/clear pair, and say what window each avoids.
  5. Write out the interleaving in §4 for two writers that both use read-modify-write on CTRL to set different bits. How many ways can it go wrong, and what is the final state in each?
  6. Someone proposes wrapping the read-modify-write of §2 in an interrupt-disable. Explain precisely why that does not fix it, using §1's distinction.
  7. Take a driver function you have written or read that touches a register. Identify its sequence, and answer §5's four questions for the register it touches. If you cannot answer question 3, say what you would need.

9. Summary

  • Read, modify, write: two accesses with a gap.
  • Two distinct failures: "unsafe" (concurrency) and "wrong" (the register itself). Locking fixes only the first.
  • W1C, measured with no concurrency: 0x5 → 0x0, an unrelated interrupt lost. Correct form writes only the bit — 0x5 → 0x4, one transaction.
  • W1C exists to remove the window; using RMW on it puts the window back and adds a bug.
  • Read-to-clear, measured: the read consumed a byte, FIFO 3 → 2. Nothing downstream can undo it.
  • The window is reasoned, not fabricated — a race trace this machine cannot produce would be worthless.
  • volatile does not close it (17.5).
  • What helps: set/clear register pairs · a single W1C write · interrupt disable (handler case only) · one owner per register, which is free.
  • Four questions before using RMW: RW? side-effect-free read? does a write mean store? sole writer?

Every failure in this chapter came from the same place: a column in the datasheet, two or three letters wide, that says what a read and a write actually do to that particular register. It is the most information-dense part of any register description and the part most often skimmed, and the next chapter is how to read it.

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.