UNIT 02 · LESSON 1 OF 6

Binary, Hexadecimal, and Bit Operations

Why does hardware documentation prefer the strange middle spelling, and how does one line of C turn on bit 3 of a register without disturbing the seven bits that some other part of the program relies on?

INTERACTIVEOne byte, three ways of reading it
A byte shown as eight bits with an operation applied to itvalue07128166405321416138024112001AND00001111result000010100A= 0x0A = 10
A byte shown as eight bits with an operation applied to itvalue07128166405321416138024112001AND00001111result000010100A= 0x0A = 10

Try this

0x5A & 0x0F = 0x0A (binary 00001010, decimal 10). 2 bits changed. Each hexadecimal digit stands for exactly four bits.

Eight bits hold one value that can be written as binary, as two hexadecimal digits (one per group of four bits) or as a decimal number. Choose an operation and a second operand to see how AND clears, OR sets, XOR toggles and shifts move bits. Results are truncated to 8 bits, as they would be when stored back into a uint8_t.

What you will be able to do
  • Convert between binary, hexadecimal and decimal for 8- and 16-bit values without a calculator, using the four-bits-per-hex-digit rule.
  • Predict the result of AND, OR, XOR, NOT and shifts on a byte, bit by bit.
  • Write the standard C idioms to set, clear, toggle and test bit n of a register.
  • Build a mask for a multi-bit field from its width and position, and extract that field.
  • Explain why a shifted 1 must be written 1u rather than 1 when the target is the top bit of a 32-bit register.
Before you start
  • Basic C syntax: variables, assignment, integer literals.
  • Reading a small table.
Steps in this lesson
  1. A byte is eight switches
  2. The bitwise operators
  3. Set, clear, toggle, test
  4. Fields wider than one bit
  5. Worked example: decode a status register
  6. Common misconceptions

The puzzle

The same eight wires inside a chip are, in one line of a datasheet, 0b01011010; in the next, 0x5A; in your debugger, 90. Nothing about the wires changed. Why does hardware documentation prefer the strange middle spelling, and how does one line of C turn on bit 3 of a register without disturbing the seven bits that some other part of the program relies on?

STEP 1

A byte is eight switches

A bit is one binary digit: 0 or 1. Physically it is one storage cell, or one wire held low or high (unit 1 covered how a voltage becomes one of those two states). Eight bits make a byte, and a byte is the smallest unit most processors can address individually.

Writing a byte as a number means assigning a weight to each bit. Bit 0, the rightmost, weighs 20=12^0 = 1; bit 7, the leftmost, weighs 27=1282^7 = 128. The value is the sum of the weights of the bits that are 1:

0b01011010=64+16+8+2=900\text{b}01011010 = 64 + 16 + 8 + 2 = 90

↑ This step uses the figure at the top of the page.

The figure shows the same byte three ways, and the important one for hardware is the middle row: two hexadecimal digits. Hex uses sixteen symbols, 0–9 then A–F for ten to fifteen, so one hex digit holds exactly four bits, a nibble. That is the whole reason hardware people use it. A 32-bit register is eight hex digits; each digit maps to four adjacent bits with no arithmetic. Decimal has no such property: 90 tells you nothing about which bits are set until you divide by powers of two.

HexBinaryHexBinary
0000081000
1000191001
20010A1010
30011B1011
40100C1100
50101D1101
60110E1110
70111F1111

Memorise this table and you can read any register value directly. 0x5A is 0101 1010; 0xF0 has the top four bits set; 0x80 is only bit 7. In C, 0x prefixes a hex literal and (since C23, and as a common extension before it) 0b prefixes binary. Datasheets usually give you hex.

STEP 2

The bitwise operators

C has operators that work on each bit position independently, with no carries between positions:

OperatorNameBit ruleTypical use
a & bAND1 only if both are 1clear bits, extract a field, test a bit
a | bOR1 if either is 1set bits
a ^ bXOR1 if exactly one is 1toggle bits
~aNOTflips every bitbuild an inverted mask
a << nshift leftmove every bit n places up, zeros enter at the bottombuild a mask, multiply by 2n2^n
a >> nshift rightmove every bit n places downextract a field, divide by 2n2^n

Do not confuse & with && or | with ||. The doubled forms are logical operators that treat the whole value as true or false and produce 0 or 1. 0x04 && 0x02 is 1 (both non-zero); 0x04 & 0x02 is 0 (no bit in common).

The second operand of AND, OR and XOR is usually called a mask: a value whose set bits mark the positions you want to affect. Go back to the figure and try AND with mask 0x0F: the low nibble survives, the high nibble is cleared. OR with 0xF0 forces the high nibble to all ones. XOR with 0xFF inverts everything, the same as NOT.

STEP 3

Set, clear, toggle, test

Every peripheral driver is built from four idioms. All start from a mask with a single bit set, 1u << n:

reg |=  (1u << n);          /* set bit n */
reg &= ~(1u << n);          /* clear bit n */
reg ^=  (1u << n);          /* toggle bit n */
if (reg & (1u << n)) { … }  /* test bit n */
INTERACTIVEThe four idioms every driver uses
Set, clear, toggle and test idioms applied to one bit of a bytev1706150403120110mask 1u << n000010000x08set: v | mask101011010xADclear: v & ~mask101001010xA5toggle: v ^ mask101011010xADtest: (v & mask) != 000000000false
Set, clear, toggle and test idioms applied to one bit of a bytev1706150403120110mask 1u << n000010000x08set: v | mask101011010xADclear: v & ~mask101001010xA5toggle: v ^ mask101011010xADtest: (v & mask) != 000000000false
Bit 3: mask = 0x08. 0xA5 | 0x08 = 0xAD; 0xA5 & ~0x08 = 0xA5; 0xA5 ^ 0x08 = 0xAD; bit 3 is clear.

Setting, clearing, toggling and testing one bit each have a standard C form built from a shifted 1. The mask 1u << n has only bit n set; its complement ~(1u << n) has every bit set except n. Pick a bit position to see the four expressions applied to the same starting byte.

Why 1u and not 1? The literal 1 is a signed int. Shifting it left by 31 tries to produce 2312^{31}, which does not fit in a 32-bit signed int, and C11 §6.5.7 says that result is undefined. 1u is an unsigned int, whose shifts are defined all the way up to bit 31. It costs nothing and removes a class of bugs that only appears when someone touches the top bit of a register. Lesson 6 returns to this.

Note also that the clear idiom uses &= ~mask, an AND with the inverted mask: every bit except n is 1, so every other bit passes through unchanged. Writing reg &= mask would keep only bit n and clear the rest, which is almost never what you want.

STEP 4

Fields wider than one bit

Registers pack multi-bit fields: a 4-bit prescaler in bits 7:4, a 2-bit mode in bits 2:1. The mask for a field of width ww at position pp is

mask=(2w−1)≪p\text{mask} = (2^{w} - 1) \ll p

For width 4 at position 4: (24−1)≪4=15≪4=0xF0(2^4 - 1) \ll 4 = 15 \ll 4 = 0\text{xF0}. To read the field, AND with the mask and shift back down; to write it, clear the field, then OR in the new value shifted up:

#define PRESC_Pos  4u
#define PRESC_Msk  (0xFu << PRESC_Pos)

uint32_t presc = (reg & PRESC_Msk) >> PRESC_Pos;             /* read  */
reg = (reg & ~PRESC_Msk) | ((value << PRESC_Pos) & PRESC_Msk); /* write */

The _Pos/_Msk pair is the naming convention CMSIS uses for Cortex-M devices, and most vendor headers follow it. The final & PRESC_Msk in the write protects the neighbouring fields if value is ever larger than the field can hold.

STEP 5

Worked example: decode a status register

A UART status register reads 0x00000091. The datasheet says bit 0 is RXNE (receive buffer not empty), bit 4 is TXE (transmit buffer empty), bit 7 is BUSY, and bits 6:5 are a 2-bit error code.

Write the value in binary, four bits per hex digit: 0x91 is 1001 0001. Now read off the fields:

  • Bit 0 = 1: RXNE set, a byte is waiting.
  • Bit 4 = 1: TXE set, you may write the next byte.
  • Bit 7 = 1: BUSY.
  • Bits 6:5 = 00: no error. In C: (status & (0x3u << 5)) >> 5 = (0x91 & 0x60) >> 5 = 0 >> 5 = 0.

To acknowledge the error field by writing zeros to bits 6:5 while leaving the rest of the register alone: status &= ~(0x3u << 5), which is 0x91 & 0xFFFFFF9F = 0x91, unchanged here because the field was already zero.

MYTHS AND FACTS

Common misconceptions

Hex is just another base; decimal would do

Hex digits align with nibbles, so a register value can be read field by field without arithmetic. That alignment is the entire point.

& and && are interchangeable when the values are 0 or 1

Only then. On register values they give different answers: 0x04 && 0x02 is 1, 0x04 & 0x02 is 0.

Shifting left by n multiplies by n

It multiplies by 2n2^n. x << 3 is x * 8.

reg &= mask clears the masked bits

It keeps the masked bits and clears the others. Clearing is reg &= ~mask.

1 << 31 is the top-bit mask

For a 32-bit int that expression is undefined behaviour. Use 1u << 31.

Bits pushed off the end wrap around

They are lost. C shifts are not rotates; a rotate needs either two shifts and an OR or a compiler intrinsic.

Check yourself

Answer in your head, then open the card.

What is 0xB4 in binary and in decimal?

B is 1011 and 4 is 0100, so 1011 0100. Weights: 128 + 32 + 16 + 4 = 180.

A register holds 0x3C. What does reg & ~(1u << 2) give?

0x3C is 0011 1100; bit 2 is set. The inverted mask clears only bit 2: 0011 1000 = 0x38.

Build the mask for a 3-bit field at bits 10:8, and extract the field from 0x0000_0700.

Width 3, position 8: (23−1)≪8=7≪8(2^3-1) \ll 8 = 7 \ll 8 = 0x700. Extract: (0x700 & 0x700) >> 8 = 7. The field holds its maximum value.

Why does x ^ x always give zero, and when is that useful?

XOR gives 1 only where the bits differ, and a value never differs from itself. On some processors XOR-ing a register with itself is the shortest way to zero it, and in checksums the XOR of a value with itself cancelling is what makes the algorithm work.

Sources (4)
  1. ISO/IEC 9899:2011 (C11), committee draft N1570, §6.4.4.1 integer constants, §6.5.7 bitwise shift operators, §6.5.10–6.5.12 bitwise AND, exclusive OR and inclusive OR operators, §7.20 <stdint.h> — the standard defines each operator on the value’s bits; §6.5.7 ¶3–4 are the rules for shift counts and for shifting signed values
  2. Raspberry Pi Ltd, RP2040 Datasheet (build 2025-02-20), §2.1.2 “Atomic Register Access”, p. 18, and §2.3.1.2 GPIO register descriptions — an example of a peripheral documented as named bit fields inside 32-bit registers; the atomic set/clear/xor aliases implement OR, AND-NOT and XOR in hardware
  3. SEI CERT C Coding Standard, INT34-C “Do not shift an expression by a negative number of bits or by greater than or equal to the number of bits that exist in the operand” — explains the undefined shift cases with compliant and non-compliant examples
  4. Arm, CMSIS-Core (Cortex-M) documentation, “Peripheral Access” (register and bit-field definition conventions: _Pos and _Msk macros) — the naming convention FIELD_Pos / FIELD_Msk used by most Cortex-M vendor headers