Skip to content
VLSI Mentor

C Part II · Module 15 · Bit-Level Programming

Setting, Clearing, Toggling and Testing Bits

Four operations, one line each, and they are the whole of register programming. What makes them worth a chapter is the promise they carry: every bit you did not name comes through untouched, which is the only reason two drivers can share a register. This chapter shows why that promise holds, measures it holding, and then measures the one expression that breaks it so thoroughly that a 64-bit register loses its entire upper half — with no diagnostic from any flag, including -Weverything. It ends on the clause people skip: the result of a mask is a value, not a boolean, and testing it against 1 works for exactly one bit.

Intermediate21 min readCPart IIHardwareBit Manipulation

Part II · Module 15 · Chapter 15.3 · Setting, Clearing, Toggling and Testing Bits

1. The Four Operations

Given a mask M built the way Chapter 15.2 described, there are four things you can want.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
reg |=  M;                      /* set    — those bits become 1 */
reg &= ~M;                      /* clear  — those bits become 0 */
reg ^=  M;                      /* toggle — those bits flip     */
if ((reg & M) != 0u) { ... }    /* test   — nothing changes     */

That is the complete vocabulary. Everything else in driver code is these four applied to masks you built from a datasheet.

What they have in common is the property that makes them usable at all, and it is worth stating as a promise rather than a side effect:

Every bit outside M is unchanged.

A control register is shared. The enable bit belongs to the driver's open routine, the interrupt mask to its interrupt setup, the divisor to its baud-rate code — and each of those runs at a different time without knowing what the others have configured. A read-modify-write that preserves everything it did not name is the only reason that works.

2. Why Each One Works

Chapter 15.2 §3 gave the per-bit table; these four operations are that table used deliberately.

Set: reg |= M. x | 0 is x and x | 1 is 1. So zeros in the mask preserve and ones force on.

Clear: reg &= ~M. x & 1 is x and x & 0 is 0. The preserving value for & is 1, so the mask has to be inverted first: ~M has ones everywhere you want to preserve and zeros exactly where M had ones.

Toggle: reg ^= M. x ^ 0 is x and x ^ 1 is ~x. Zeros preserve, ones flip.

Test: reg & M. Same identity as clear, used to discard rather than to keep: everything outside the mask becomes 0, so what is left is the field in its original position and nothing else.

The four fundamental bit operations compared across five columns: the operation, the C expression, what it does to the bit you name, what it does to every other bit, and the identity that makes it work. Setting a bit uses OR-equals with the mask: the named bit becomes 1, every other bit is unchanged, because x OR 0 equals x. Clearing uses AND-equals with the complement of the mask: the named bit becomes 0, others unchanged, because x AND 1 equals x. Toggling uses XOR-equals with the mask: the named bit flips, others unchanged, because x XOR 0 equals x. Testing uses AND with the mask compared against zero: nothing is changed at all, and the result is a value rather than a boolean, which is why it must be compared against zero and never against one.OperationCThe named bitEvery otherbitWhySetORreg |= Mbecomes 1bit setunchangednothing elsemoves0 is neutralfor ORx | 0 = xClearAND NOTreg &= ~Mbecomes 0bit clearedunchangednothing elsemoves1 is neutralfor ANDx & 1 = xToggleXORreg ^= Mflips1 to 0, 0 to 1unchangednothing elsemoves0 is neutralfor XORx ^ 0 = xTestAND, compare(reg & M) != 0not changeda read, not awritenot changeda read, not awritea value, nota flagnever == 112
Figure 1 — the four operations, the identity that makes each one work, and what each does to the bit you name and to every other bit. The fourth column is the whole reason these are the operations and not some others: in all four rows it reads unchanged, which is the promise that lets separate pieces of code configure separate fields of one shared register. The last row is the one that behaves differently and is treated separately in section 5 — a test changes nothing, and its result is the masked value rather than a true-or-false, so it must be compared against zero and never against one.

3. The Promise, Measured

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

#define ENABLE  (UINT32_C(1) << 0)
#define IRQ_EN  (UINT32_C(1) << 5)

int main(void) {
    uint32_t reg = 0x0000000Cu;             /* bits 2 and 3 already set */
    printf("start        0x%08" PRIx32 "\n", reg);
    reg |=  ENABLE;  printf("set  ENABLE  0x%08" PRIx32 "\n", reg);
    reg |=  IRQ_EN;  printf("set  IRQ_EN  0x%08" PRIx32 "\n", reg);
    reg ^=  IRQ_EN;  printf("tog  IRQ_EN  0x%08" PRIx32 "\n", reg);
    reg &= ~ENABLE;  printf("clr  ENABLE  0x%08" PRIx32 "\n", reg);
    printf("bits 2,3 untouched throughout: %s\n", (reg & 0xCu) == 0xCu ? "yes" : "NO");
    return 0;
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
start        0x0000000c
set  ENABLE  0x0000000d
set  IRQ_EN  0x0000002d
tog  IRQ_EN  0x0000000d
clr  ENABLE  0x0000000c
bits 2,3 untouched throughout: yes

Four operations, and bits 2 and 3 — which nothing in the program named — came through every one of them. The register returned to its starting value because set and clear are inverses, and toggle applied twice would have been too.

Note that ^= applied once to a set bit cleared it. Toggle is not "set"; it is the only operation of the four whose result depends on the previous value, which makes it the right tool for a blinking output and the wrong tool for anything you want to be in a known state.

4. The Trap: A Mask Narrower Than the Register

reg &= ~M is the only one of the four that applies an operator to the mask before using it, and ~ is where the width of M stops being a detail.

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

int main(void) {
    uint64_t reg  = 0xFFFFFFFFFFFFFFFFull;   /* every bit set    */
    uint32_t mask = 0x80000000u;             /* clear bit 31 only */

    uint64_t bad  = reg & ~mask;             /* ~mask is uint32_t */
    uint64_t good = reg & ~(uint64_t)mask;   /* widen BEFORE ~    */

    printf("~mask      = 0x%08"  PRIx32 "   (still 32 bits)\n", (uint32_t)~mask);
    printf("reg & ~mask            = 0x%016" PRIx64 "\n", bad);
    printf("reg & ~(uint64_t)mask  = 0x%016" PRIx64 "\n", good);
    return 0;
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
~mask      = 0x7fffffff   (still 32 bits)

reg & ~mask            = 0x000000007fffffff   <- upper 32 bits GONE
reg & ~(uint64_t)mask  = 0xffffffff7fffffff   <- only bit 31 cleared

The intent was to clear one bit. What happened was that thirty-three bits were cleared — bit 31 and the entire upper half.

The mechanism is ordinary and that is what makes it dangerous. mask is a uint32_t, so ~mask is computed in 32 bits and is 0x7FFFFFFF — a 32-bit value whose top bit is 0 and which has no upper half at all. Converting it to uint64_t for the & zero-extends it, producing 0x000000007FFFFFFF. Every bit above 31 in that value is a zero, and & with a zero clears.

Widening the mask to the register's type before complementing gives 0xFFFFFFFF7FFFFFFF, which preserves everything except bit 31.

The opposite case is worth a sentence because people conflate them. A mask wider than the register is harmless: uint8_t r &= ~B7 with B7 = 0x80 computes ~B7 as an int — 0xFFFFFF7F — and assigning the result back into a uint8_t truncates to 0x7F, which is the intended mask. Measured, that produces the correct answer and no warning. The dangerous direction is specifically mask narrower than register.

5. Why a Mask Result Is a Value, Not a Boolean

This is the clause people skip, and it produces a bug that passes testing.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
uint32_t r2 = IRQ_EN;      /* only bit 5 set */
printf("r2 & IRQ_EN          = %" PRIu32 "\n", r2 & IRQ_EN);
printf("(r2 & IRQ_EN) == 1   = %s\n", (r2 & IRQ_EN) == 1u ? "true" : "false");
printf("(r2 & IRQ_EN) != 0   = %s\n", (r2 & IRQ_EN) != 0u ? "true" : "false");
printf("!!(r2 & IRQ_EN)      = %d\n", !!(r2 & IRQ_EN));
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
r2 & IRQ_EN          = 32   (0x20)
(r2 & IRQ_EN) == 1   = false   <- wrong test
(r2 & IRQ_EN) != 0   = true    <- right test
!!(r2 & IRQ_EN)      = 1

The bit is set and (r2 & IRQ_EN) == 1 is false.

& is an arithmetic operator producing an integer, not a predicate producing a truth value. Chapter 15.2 §6 made the related point for multi-bit fields: masking isolates the bits and leaves them at their original positions. For a single bit at position n, "isolated and left in place" means the result is either 0 or 2ⁿ — here 32, not 1.

So the correct tests are != 0, or a plain if (reg & M) which uses C's own "non-zero is true" rule, or !!(reg & M) when you genuinely need a 0-or-1 integer to store.

The reason this bug survives is worth noticing, because it explains a whole category of near-misses: it works for bit 0. ENABLE is bit 0, (reg & ENABLE) == 1 is correct, and anyone who tested their macro on the enable bit saw it pass. The macro then fails for every other bit in the register, silently, by reporting that a set bit is clear.

6. Writing a Whole Field

The four operations name bits. A multi-bit field needs both a clear and a set, and the two must happen as one expression or the register passes through a state neither value calls for:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
/* set MODE (bits 2:1) to `m`, leaving every other field alone */
reg = (reg & ~MODE_MASK) | ((m << MODE_POS) & MODE_MASK);

Three things are going on and all three are necessary.

reg & ~MODE_MASK clears the field — §2's clear, applied to a two-bit mask rather than one bit.

m << MODE_POS moves the new value into position.

& MODE_MASK after the shift is the one people leave out. If m is larger than the field can hold, the shifted value has bits above the field, and | would set them — corrupting whichever neighbouring field they land in. Chapter 15.4 measures exactly that, and builds this expression into an accessor so it cannot be written any other way.

The single = matters too: writing the clear and the set as two statements means the register briefly holds the field as zero. For a plain memory variable that is invisible. For a hardware register, a zero in a mode field may be a valid and different mode that the device will act on. Chapter 16.5 is about read-modify-write on real registers, where this stops being hypothetical.

7. What You Should Be Able to Say Now

  1. Four operations: |= set, &= ~ clear, ^= toggle, & … != 0 test.
  2. Every bit outside the mask is unchanged — measured across all four, with untouched neighbours — and that promise is why a shared register works at all.
  3. Each follows from an identity: x|0=x, x&1=x, x^0=x. Clear needs ~ because the preserving value for & is 1.
  4. Toggle is the only one whose result depends on the previous value, so it is wrong for anything that must end in a known state.
  5. A mask narrower than the register destroys the upper bits. Measured: uint64_t & ~uint32_t_mask cleared 33 bits, not 1. No diagnostic from any flag, including -Weverything.
  6. Build the mask in the register's own width — UINT64_C(1) << 31 — rather than remembering to cast at each use.
  7. A mask result is a value, not a boolean. For bit 5 it is 32. == 1 is false for every bit except bit 0, which is why the bug passes testing.
  8. Use != 0, a bare if, or !! to store a flag. Never == 1.
  9. A multi-bit field needs clear-and-set in one expression, with the new value masked after shifting.

8. Exercises

  1. Reproduce §3 and add a second toggle of IRQ_EN. Confirm the register returns to 0x0000000d and explain why in terms of §2's identity.
  2. Write set_bit, clear_bit, toggle_bit and test_bit as static inline functions taking a uint32_t * and a bit number. Decide whether test_bit returns int or bool, and justify it from §5.
  3. Reproduce §4 and confirm both outputs. Then write the version with UINT64_C and confirm no cast is needed at the point of use.
  4. Build §5's wrong test as a macro IS_SET(reg, m) ((reg & m) == 1) and write two test cases: one that passes and one that fails. Say which bit each uses.
  5. Write the clear-and-set expression for THRESH (bits 23:16) and verify on 0x00081A3D that setting it to 0x5A leaves DIVISOR, MODE, ENABLE and bit 3 unchanged.
  6. Split §6's single expression into two statements and name a device for which the intermediate state would be a real problem.
  7. Take the ~ trap from §4 and construct the mirror case — a mask wider than the register. Confirm the measured claim that it is harmless, and say precisely why the two directions differ.

9. Summary

  • |= set · &= ~ clear · ^= toggle · (reg & M) != 0 test. That is the vocabulary.
  • Bits outside the mask are untouched — measured — which is the entire basis of sharing a register between unrelated code.
  • The identities: x|0=x, x&1=x, x^0=x. Clear inverts the mask because & preserves on 1.
  • Toggle alone depends on the prior value.
  • A mask narrower than the register is the trap: uint64_t & ~uint32_t cleared 33 bits, undiagnosed by -Weverything. Build masks in the register's width.
  • A mask wider than the register is harmless — measured — so the hazard has a direction.
  • The masked result is a value: 32 for bit 5. == 1 is right only for bit 0, which is why it passes testing and fails in the field.
  • A field write is one expression: clear with ~mask, shift the value in, mask it again after shifting.

That last expression has three parts and the measured consequence of omitting the final mask has not yet been shown. It is also the first time in this module that a value is going into a register rather than coming out, and the two directions turn out to be one skill with the steps reversed — which is the only way to learn them without constantly getting the order backwards.

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.