Skip to content
VLSI Mentor

C Part II · Module 15 · Bit-Level Programming

Extracting and Building Bit Fields

Read every field out of a register, put every field straight back in, and compare: the value has changed. Nothing was modified in between, the macros are correct, and one bit has vanished — because the datasheet's field map does not name it, and a rebuild can only write the bits it knows about. This chapter builds the extract-and-insert pair as one skill rather than two, measures the trap that the insert direction has and the extract direction does not, and measures what a full rebuild costs against the read-modify-write that does not have the problem at all.

Intermediate21 min readCPart IIHardwareBit Manipulation

Part II · Module 15 · Chapter 15.4 · Extracting and Building Bit Fields

1. One Skill, Two Directions

A field has a position and a width. Getting its value out of a register and putting a new value in are the same two operations — a shift and a mask — applied in opposite orders:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
extract:   shift the field DOWN to bit 0,  then mask off what is above it
insert:    mask the value to the field's width,  then shift it UP to position

They are presented together here deliberately. Learned separately, the near-symmetry is exactly what makes people shift the wrong way: "the field is at bit 4, so I shift by 4" is true in both directions and says nothing about which way. Learned as a pair, the rule is one sentence — a register value is always shifted down, a field value is always shifted up — and the direction stops being a thing you have to recall.

The mask is on the other side of the shift in each case, and that asymmetry is not arbitrary either. Extracting, you shift first so the mask is always the same simple run of ones at the bottom. Inserting, you mask first so that a value too large for the field cannot reach the bits above it. §5 measures what happens if you do not.

2. The Pair

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#include <stdint.h>

/* a mask of `width` ones, at the bottom */
#define FIELD_ONES(width)        ((UINT32_C(1) << (width)) - 1u)

/* read:  register -> field value */
#define GET(reg, pos, width)     (((reg) >> (pos)) & FIELD_ONES(width))

/* write: field value -> register bits */
#define PUT(val, pos, width)     (((uint32_t)(val) & FIELD_ONES(width)) << (pos))

Three lines, and every field accessor in a driver is one of them applied to two named constants. Combined with Chapter 15.1 §7's transcription of the field map:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#define UART_MODE_POS      1u
#define UART_MODE_W        2u
#define UART_DIVISOR_POS   4u
#define UART_DIVISOR_W    12u
#define UART_THRESH_POS   16u
#define UART_THRESH_W      8u

#define UART_GET_DIVISOR(r)    GET((r), UART_DIVISOR_POS, UART_DIVISOR_W)
#define UART_PUT_DIVISOR(v)    PUT((v), UART_DIVISOR_POS, UART_DIVISOR_W)

Each of the datasheet's two numbers now appears exactly once, and no accessor can disagree with the map.

Measured on the running register:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
extracted: enable=1 mode=2 div=419 thr=8

3. Why the Shifts Go the Way They Do

Worth one worked example in each direction, because the arithmetic is the whole content.

Extracting DIVISOR (position 4, width 12) from 0x00081A3D:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
reg            0000 0000 0000 1000 0001 1010 0011 1101
reg >> 4       0000 0000 0000 0000 1000 0001 1010 0011     the field is now at the bottom
& 0xFFF        0000 0000 0000 0000 0000 0001 1010 0011     everything above it discarded
                                             = 0x1A3 = 419

After the shift, the field occupies bits 11:0 whatever its original position was — which is why the mask is just width ones and never has to account for the position. That is the reason to shift first.

Inserting the value 419 back:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
419            0000 0000 0000 0000 0000 0001 1010 0011
& 0xFFF        0000 0000 0000 0000 0000 0001 1010 0011     already fits: unchanged
<< 4           0000 0000 0000 0000 0001 1010 0011 0000     moved to position 4
                                             = 0x1A30

The mask did nothing here because 419 fits in 12 bits. §5 is about the case where it does not.

Chapter 15.2 §6's distinction appears again in the middle line of the extract: reg & DIVISOR_MASK would give 0x1A30, which is the field isolated in place. It is >> 4 that turns it into 419, the field's value. The two are different numbers and only one of them is what the hardware means.

4. Writing One Field

Chapter 15.3 §6 gave the expression; here it is with the accessors, and it is worth naming its three parts because leaving any one out is a distinct bug:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
#define FIELD_MASK(pos, width)   (FIELD_ONES(width) << (pos))

/* set one field of `reg` to `val`, leaving every other bit alone */
#define WITH_FIELD(reg, pos, width, val) \
    (((reg) & ~FIELD_MASK(pos, width)) | PUT((val), (pos), (width)))

reg = WITH_FIELD(reg, UART_MODE_POS, UART_MODE_W, 1u);

& ~FIELD_MASK clears the field. PUT puts the new value in position, already masked. | combines them. Measured on the running register, setting THRESH to 0x5A:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
before 0x00081a3d  after 0x005a1a3d
THRESH 8 -> 90
DIVISOR same 1 · MODE same 1 · EN same 1 · bit3 same 1

Every other field unchanged, including bit 3 — which is about to matter.

5. The Trap the Insert Direction Has

GET cannot produce a wrong-sized answer: it masks last, so the result is always within the field's width. PUT can be handed a value too large for the field, and what happens then depends entirely on whether the mask is there.

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

MODE is two bits. Writing 4095u << MODE_POS without the mask produces 0x1FFE — twelve ones starting at bit 1, running straight through DIVISOR and into bit 12. Combined with | into a register, it would set eleven bits belonging to a field nobody was trying to change, and the symptom would appear as a wrong baud rate rather than as a wrong mode.

With the mask, 4095 & 0x3 is 3, shifted to 0x6. The value is wrong — 4095 was never a legal mode — but the damage is confined to the field the programmer named.

6. The Measurement: A Rebuild Loses What It Cannot Name

Here is the natural-looking thing to do when several fields change at once — read them all, modify the ones you want, write them all back:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
uint32_t reg = 0x00081A3Du;

uint32_t rebuilt = PUT(GET(reg, 0, 1),   0,  1)
                 | PUT(GET(reg, 1, 2),   1,  2)
                 | PUT(GET(reg, 4, 12),  4, 12)
                 | PUT(GET(reg, 16, 8), 16,  8);

Nothing is modified. Every field is read out and put straight back. The result should be the register.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
original  = 0x00081a3d
rebuilt   = 0x00081a35
lost bits = 0x00000008   (bit 3)

It is not. One bit is gone, and the macros are not at fault — the field map is.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
fields cover = 0x00fffff7
gaps in map  = 0xff000008
reg bits NOT covered by any field = 0x00000008

The four fields cover 0x00FFFFF7. Bit 3 is a gap — Chapter 15.1's register map lists it as reserved, and reserved bits are not in the map because they have no name. The rebuild wrote every bit the map describes and zero into every bit it does not, which is what | of four masked values must do.

And bit 3 was set in the register that was read.

Read-modify-write of only the field being changed has no such problem, because it never touches anything outside one mask:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
RMW set MODE=1 : 0x00081a3b   bit3 preserved

The rule that follows: change one field at a time with §4's WITH_FIELD, chaining if several change. A full rebuild is correct only when you can state that the field map covers every bit of the register, and for a register with reserved bits — which is most of them — you cannot.

7. What Form to Put in a Header

Macros are used above because they show the arithmetic. For a real register map there are two better options and the choice follows Chapter 15.2 §7's pattern.

static inline functions, when the position and width are compile-time constants for each field — which is the normal case:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
static inline uint32_t uart_get_divisor(uint32_t reg) {
    return (reg >> UART_DIVISOR_POS) & FIELD_ONES(UART_DIVISOR_W);
}
static inline uint32_t uart_with_divisor(uint32_t reg, uint32_t val) {
    assert(val <= FIELD_ONES(UART_DIVISOR_W));
    return (reg & ~FIELD_MASK(UART_DIVISOR_POS, UART_DIVISOR_W))
         | ((val & FIELD_ONES(UART_DIVISOR_W)) << UART_DIVISOR_POS);
}

Chapter 11.6 §8 and Chapter 11.4 §8 between them make the case: a function has types, evaluates each argument once, is visible to the debugger of Chapter 13.9, and static inline in a header costs nothing at any optimisation level above -O0. The macro versions evaluate reg twice in WITH_FIELD, which is Chapter 11.6 §4's double-evaluation trap waiting for someone to write WITH_FIELD(*p++, …).

Generic macros, when the width is genuinely a parameter — a generated map, a sweep over field widths in a test. Then use the guarded FIELD_ONES32 and accept the macro.

Either way, two things belong next to the definitions:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
_Static_assert(UART_DIVISOR_POS + UART_DIVISOR_W <= 32, "field runs off the end");

and a comment naming the datasheet table the numbers came from. The assertion catches a transposed position or width at build time, which Chapter 13.3 §7 named as the way to rely on something deliberately.

8. What You Should Be Able to Say Now

  1. One skill, two directions. Extract: shift down, then mask. Insert: mask, then shift up. A register value is always shifted down; a field value is always shifted up.
  2. Shift first when extracting so the mask is always a plain run of width ones, independent of position.
  3. Mask first when inserting so an over-large value cannot reach the bits above the field. Measured: 4095 << 1 unmasked gives 0x1FFE and runs through DIVISOR; PUT gives 0x6.
  4. A field write has three parts — clear with ~mask, shift the masked value up, combine with |. Measured preserving all other fields.
  5. Masking in PUT is containment, not validation. Add an assert for the diagnosis; keep the mask, because NDEBUG removes the assertion.
  6. A full rebuild drops every bit the field map does not name. Measured: read all four fields and put them straight back, and 0x00081A3D became 0x00081A35 — bit 3, reserved, silently cleared.
  7. Read-modify-write one field at a time preserves it. Measured: 0x00081A3B, bit 3 intact.
  8. Reserved bits often carry a documented requirement — preserve, or write zero, or maintain the reset value — and a rebuild can only honour one of them.
  9. Prefer static inline accessors to macros when position and width are constants; the macro form evaluates reg twice.

9. Exercises

  1. Implement GET and PUT and extract all four fields of 0x00081A3D. Confirm enable=1 mode=2 div=419 thr=8.
  2. Reproduce §6's rebuild and confirm the lost bit. Then add a fifth term covering bit 3 and confirm the round trip succeeds. Say why adding that term is not a general fix.
  3. Write WITH_FIELD and use it to set MODE to 1 on the running register. Verify every other field — including bit 3 — is unchanged.
  4. Call PUT(4095, 1, 2) and 4095u << 1 and print both. Identify which neighbouring field the second one corrupts and by how many bits.
  5. Rewrite WITH_FIELD as a static inline function. Then construct the call that breaks the macro version but not the function, using Chapter 11.6 §4's trap.
  6. Add _Static_assert(pos + width <= 32, …) for every field in the running register. Then change one width to make it fail, and read the message.
  7. A datasheet says of bits 31:24: "Reserved. Software must preserve the value read." Write the function that changes THRESH and honours that instruction, and say what a full rebuild would have done instead.

10. Summary

  • Extract: shift down, then mask. Insert: mask, then shift up. One skill; the direction follows from which side the value is on.
  • Shift-first on extract keeps the mask position-independent; mask-first on insert confines an over-large value.
  • A field write is clear, shift-in, combine — measured preserving every neighbour.
  • PUT's mask is containment; assert is validation. Keep both: NDEBUG removes only the assertion.
  • A full rebuild silently drops unmapped bits. Measured: 0x00081A3D → 0x00081A35, bit 3 gone, because the field map covers 0x00FFFFF7.
  • Read-modify-write one field preserves it — measured 0x00081A3B.
  • Reserved bits carry real instructions — preserve / write zero / maintain reset value — which a rebuild cannot all honour.
  • static inline over macros when position and width are constants; the macro evaluates reg twice.

Every expression in this chapter had a uint32_t on the left of the shift and a small constant on the right, and that is why all of it behaved. Both of those assumptions fail routinely in real register code — the register is 16 bits, or the shift count is computed, or the result is assigned somewhere narrower — and the failures that follow look like three unrelated bugs. They are not three.

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.