C Part II · Module 16 · Registers and Memory-Mapped I/O
Register Fields, Masks and Shifts
Module 15 built field accessors against a plain variable and every one of them still works. What changes when the word is a device is not the arithmetic — it is that the value has to be fetched before it can be modified, that the fetch is an access with its own cost and possibly its own side effect, and that the bits you did not name belong to somebody else's code. This chapter applies Module 15 to a real register description, measures a field update preserving a reserved bit that a careless rebuild destroys, and ends at the gap between the read and the write.
Advanced21 min readCPart IIHardwareRegisters
Part II · Module 16 · Chapter 16.4 · Register Fields, Masks and Shifts
1. Nothing About the Arithmetic Changes
A register's field description is the same shape as Chapter 15.1's, because Chapter 15.1's was taken from one:
CTRL (offset 0x04, RW, 32-bit access)
bit 0 ENABLE
bits 2:1 MODE 0 = 8N1, 1 = 8E1, 2 = 8O1, 3 = reserved
bit 3 reserved — preserve the value read
bits 15:4 DIVISOR 12 bits
bits 23:16 THRESH 8 bits
bits 31:24 reservedEvery tool from Module 15 applies unchanged. The mask for DIVISOR is still ((1 << 12) - 1) << 4. Extraction is still shift-down-then-mask. Insertion is still mask-then-shift-up. The UINT32_C(1) discipline from 15.2 §4 still matters and for the same reason.
So this chapter is not about new arithmetic. It is about the four things that change when the uint32_t is a device rather than a variable, and the first of them is the one that generates the rest.
2. What Actually Changes
The value must be fetched, and the fetch is an access. In Module 15, reg was a variable: reading it was free, repeatable, and had no consequences. Here, every read of reg is a bus transaction — Chapter 16.1's six stages — with a cost, a width, and possibly a side effect. An accessor that reads the register twice performs two transactions, and on some registers that is a bug rather than an inefficiency.
The write-back is a second access, with a gap. A field update is read, modify, write — three steps, of which two are I/O. Between them is a window in which the register may change. On a variable there was no window. Chapter 16.5 is entirely about that window.
The bits you did not name belong to someone else. Module 15 §15.3 established that masked operations preserve everything outside the mask, and framed it as a convenience. Here it is a contract: the interrupt-enable bit belongs to the interrupt setup code, the divisor to the baud-rate code, and a reserved bit may belong to the hardware itself. Getting it wrong corrupts a subsystem you were not working on.
The register has an identity. A field accessor for a variable takes a uint32_t. A field accessor for a register should take the thing that says which register — a base and an offset — because a bare uint32_t parameter could be any word in the program, and the compiler cannot tell you that you passed the wrong one.
3. Reading Fields From a Live Register
Using the accessors from Chapter 16.3 §6 and the field macros from Chapter 15.4:
#define FIELD_ONES(w) ((UINT32_C(1) << (w)) - 1u)
#define FIELD_MASK(p, w) (FIELD_ONES(w) << (p))
#define GET(reg, p, w) (((reg) >> (p)) & FIELD_ONES(w))
#define PUT(val, p, w) (((uint32_t)(val) & FIELD_ONES(w)) << (p))
#define CTRL_ENABLE_POS 0u
#define CTRL_ENABLE_W 1u
#define CTRL_MODE_POS 1u
#define CTRL_MODE_W 2u
#define CTRL_DIVISOR_POS 4u
#define CTRL_DIVISOR_W 12u
#define CTRL_THRESH_POS 16u
#define CTRL_THRESH_W 8u
uint32_t uart_divisor(uintptr_t base) {
uint32_t ctrl = reg_read32(base, UART_CTRL); /* ONE read */
return GET(ctrl, CTRL_DIVISOR_POS, CTRL_DIVISOR_W);
}Measured against the model, after writing 0x00081A3D:
CTRL = 0x00081a3d
enable=1 mode=2 divisor=419 thresh=8The same four values Chapter 15.1 extracted from the same word, now fetched from a device.
Note the local ctrl. It is not a style preference — it is the point of §2's first item. Writing the obvious thing instead:
/* two reads, two bus transactions */
uint32_t d = GET(reg_read32(base, UART_CTRL), CTRL_DIVISOR_POS, CTRL_DIVISOR_W);
uint32_t m = GET(reg_read32(base, UART_CTRL), CTRL_MODE_POS, CTRL_MODE_W);issues two transactions where one would do, and the two may disagree if the hardware changed the register in between. Read once into a local, then extract every field you need from that copy. On CTRL the cost is a wasted transaction; on a register whose read has a side effect it is Chapter 16.9's "data goes missing under load".
4. Writing One Field
The expression is Chapter 15.4 §4's, wrapped so that the register's identity is in the signature:
static void uart_set_divisor(uintptr_t base, uint32_t divisor) {
assert(divisor <= FIELD_ONES(CTRL_DIVISOR_W)); /* 13.8 */
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 */
}Measured, starting from 0x00081A3D and setting the divisor to 1000:
before 0x00081a3d enable=1 mode=2 divisor=419 thresh=8 bit3=1
after 0x00083e8d enable=1 mode=2 divisor=1000 thresh=8 bit3=1One field changed, everything else — including the reserved bit the datasheet says to preserve — came through untouched.
Three details in that function are each worth a sentence.
The assert is Chapter 15.4 §5's decision. PUT masks the value so an over-large divisor cannot reach THRESH; the assertion is what tells you it was over-large. Keep both: the mask is the containment and survives NDEBUG, the assertion is the diagnosis and does not.
The read and the write are separate statements. They have to be — there is no way to modify a device's register in place — and making them visible is honest about there being two accesses rather than one.
The order of the three steps is fixed. Chapter 15.3 §6 warned against splitting the clear and the set into two writes; here they are two operations on a local and one write, which is the only form that does not expose an intermediate value to the hardware.
5. Writing Several Fields at Once
Configuring a device usually means setting four or five fields together, and doing it with four calls to §4's function means four read-modify-write cycles and four write transactions. Fold them into one:
static void uart_configure(uintptr_t base, uint32_t mode, uint32_t divisor, uint32_t thresh) {
assert(mode <= FIELD_ONES(CTRL_MODE_W));
assert(divisor <= FIELD_ONES(CTRL_DIVISOR_W));
assert(thresh <= FIELD_ONES(CTRL_THRESH_W));
uint32_t ctrl = reg_read32(base, UART_CTRL); /* one read */
ctrl &= ~(FIELD_MASK(CTRL_MODE_POS, CTRL_MODE_W)
| FIELD_MASK(CTRL_DIVISOR_POS, CTRL_DIVISOR_W)
| FIELD_MASK(CTRL_THRESH_POS, CTRL_THRESH_W));
ctrl |= PUT(mode, CTRL_MODE_POS, CTRL_MODE_W)
| PUT(divisor, CTRL_DIVISOR_POS, CTRL_DIVISOR_W)
| PUT(thresh, CTRL_THRESH_POS, CTRL_THRESH_W);
reg_write32(base, UART_CTRL, ctrl); /* one write */
}Measured: 0x00081A3D configured to mode 1, divisor 1000, threshold 0x20:
after = 0x00203e8b enable=1 mode=1 divisor=1000 thresh=32 bit3=1
transactions: 1 read, 1 writeTwo transactions instead of eight, and — more importantly — the device never sees a partially configured state. Four separate updates would briefly present three combinations of settings that nobody asked for, and a device that acts on CTRL writes would act on each of them.
The clear-mask is built as one | of the three field masks, which is the general form: clear everything you are about to set, in one step; set it, in one step; write once. The reserved bits appear in neither mask and are carried through by the read.
6. Making the Accessors Hard to Misuse
Four properties separate an accessor you can trust from one that merely works today.
Take the register's identity, not a word. uart_set_divisor(uintptr_t base, uint32_t divisor) cannot be handed the wrong word; set_divisor(uint32_t *word, uint32_t divisor) can be handed any word in the program.
Name the register in the function name. uart_set_divisor, not set_divisor. A register map has sixty fields and several of them are called MODE.
Return the field's value, not the masked word. Chapter 15.2 §6 measured the difference: reg & DIVISOR_MASK is 0x1A30, the field's value is 419. An accessor should return 419.
Assert the field bounds and the alignment. Chapter 13.8's tool, applied to the two things the caller can get wrong.
And one thing to avoid: do not write a uart_get_ctrl() that returns the raw word and let callers mask it themselves. That is how the same field ends up extracted with three different shift constants in three files, and how two of them end up wrong.
7. The Misconception: It Is the Same Function as Before
"A field accessor is a field accessor — the register version is the same code with a different source for the word."
The arithmetic is identical and the function is not, because a register accessor performs I/O and that changes what the function is allowed to be.
It cannot be pure. uart_divisor(base) called twice may legitimately return different values, because the hardware may have changed the register. A compiler cannot cache it, and neither can you.
It cannot be called speculatively. Calling a getter "just to have the value" is free for a variable and is a bus transaction here — and on a read-to-clear register it consumes data. Chapter 16.2 §3 measured exactly that happening by accident.
Its cost is not the arithmetic. The masks and shifts are a handful of instructions; the access is a bus transaction that may take tens or hundreds of cycles. This is why §3 and §5 are built around read once, extract many and modify many, write once — the optimisation that matters is the number of transactions, and it is one the compiler cannot make for you because volatile forbids it.
The practical form of the correction is the design rule the whole chapter has been building toward:
Fetch the word once into a local. Do all the field arithmetic on the local. Write it back once.
Everything Module 15 taught applies to the middle step, unchanged. The first and last steps are the new part, and the window between them is the next chapter.
8. What You Should Be Able to Say Now
- The arithmetic is unchanged. Masks, shift-down-then-mask, mask-then-shift-up,
UINT32_C(1)— all of Module 15 applies as written. - Four things change: the value must be fetched by an access; the write-back is a second access with a gap; the unnamed bits belong to other code; the register has an identity that belongs in the signature.
- Read once into a local, then extract every field from that copy. Two reads are two transactions that may disagree — and on a read-to-clear register, the second one loses data.
- A field write is read, clear one mask, set one field, write. Measured: divisor 419 → 1000 gave
0x00083e8dwith the reserved bit and every other field intact. - Keep both the mask and the assertion — the mask contains an over-large value, the assertion diagnoses it and is removed by
NDEBUG. - Never rebuild the word from named fields. Measured in 15.4 and restated here against a device whose datasheet says preserve the value read for a bit that is set at reset.
- Set several fields in one cycle: one clear-mask, one set, one write. Two transactions instead of eight, and no partially configured state on the wire.
- Accessors take base and offset, name their register, return the field's value, and assert their bounds.
- A register accessor is not pure, must not be called speculatively, and is dominated by transaction cost — which is why read once, write once is the shape.
9. Exercises
- Write
uart_divisor,uart_modeanduart_threshagainst the model, each reading once. Confirm 419, 2 and 8 on0x00081A3D. - Rewrite the three as a single
uart_read_config(base, struct uart_config *out)that reads once. Count transactions in both versions. - Implement
uart_set_divisorand verify that bit 3 and all other fields survive. Then deliberately omit the& ~FIELD_MASKstep and say what the result is for divisor 1000 — predict before running. - Implement §5's
uart_configureand confirm the measured result. Then write the four-separate-calls version and list the intermediate states the device would see. - Write the accessor that returns
reg & DIVISOR_MASKinstead of the field's value. Give the number it returns for0x00081A3Dand say which chapter measured that distinction. - Add
_Static_assertlines ensuring everyCTRLfield's position plus width is at most 32, and that no two field masks overlap. The second is harder — say why. - A colleague proposes
uint32_t uart_get_ctrl(uintptr_t base)returning the raw word, with callers masking. Give two concrete failure modes, using material from this module.
10. Summary
- Module 15's arithmetic applies unchanged. This chapter changes the source of the word, not the maths.
- Four changes: the read is an access · the write-back is a second access with a gap · unnamed bits belong to other code · the register has an identity.
- Read once into a local; extract every field from the copy. Two reads are two transactions, and on an RC register the second loses data.
- Field write = read, clear the mask, set the field, write. Measured
0x00081a3d→0x00083e8d, reserved bit 3 and every neighbour intact. - Mask contains,
assertdiagnoses — keep both. - Never rebuild from named fields: the datasheet says preserve the value read for bit 3, and it is set at reset.
- Several fields in one cycle: one clear-mask, one set, one write — and no partially configured state reaches the device.
- Accessors take base + offset, name their register, return field values, assert bounds.
- Not pure, not speculative, dominated by transaction cost.
The pattern throughout has been read, modify, write — three steps, two of them accesses, with something in between. That shape is so standard it has a name, and it has three separate ways of being wrong that have nothing to do with the arithmetic in the middle: one at the read, one at the write, and one in the gap.
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.
