C Refresher

Bit Manipulation in C

If you want to program microcontrollers, you have to learn how to speak their language. And their language is Binary.

In this lesson, we are going to learn what bits are, how to manipulate them, and how changing a single 1 to a 0 inside the code can physically turn on an LED in the real world.


1. The Problem: Why do we need this?

Imagine you have an 8-bit microcontroller. It has a physical piece of hardware called "Port A", which is directly connected to 8 physical pins on the outside of the chip.

Inside the microcontroller's memory, "Port A" is controlled by a single 8-bit variable (a register). Let's say we have an LED connected to Pin 3, and a motor connected to Pin 7.

Currently, the motor is running, but the LED is off. The memory looks like this: 10000000 (The 7th bit is 1, the 3rd bit is 0).

Your task: Turn on the LED (make Pin 3 a 1).

If you just say PortA = 8; (which is 00001000 in binary), the LED will turn on... but the motor will turn off! You accidentally erased the 1 in the 7th bit!

We need a way to say: "Turn on Pin 3, but leave everything else exactly as it is." That is what Bit Manipulation does.


2. The Basics: Binary and Hexadecimal

Binary (Base-2)

Humans count in Base-10 (0 to 9). Computers count in Base-2 (0 and 1). Each 0 or 1 is called a Bit. 8 Bits together make a Byte.

Let's look at an 8-bit number. Each position has a specific value, doubling as we move to the left (starting from the rightmost bit, which we call "Bit 0").

Bit 7Bit 6Bit 5Bit 4Bit 3Bit 2Bit 1Bit 0
1286432168421

If we have the binary number 00001001, its value is 8 + 1 = 9.

In C, we can write binary numbers using the 0b prefix:

c
1uint8_t my_number = 0b00001001; // This is the number 9

Hexadecimal (Base-16)

Writing out 0b11010110 is annoying and hard to read. Programmers use Hexadecimal (Base-16) as a shortcut. Every 4 bits (a "nibble") perfectly translates to one Hex character.

We count: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A, B, C, D, E, F

  • 0000 = 0
  • 1010 = A (10)
  • 1111 = F (15)

In C, we write hex numbers using the 0x prefix:

c
1uint8_t my_number = 0x09; // This is the number 9
2uint8_t max_value = 0xFF; // This is 11111111 (255)

3. The Bitwise Operators

C provides four mathematical operators that work directly on the individual bits of a number.

1. Bitwise AND (&)

The AND operator compares two bits. The result is 1 ONLY if BOTH bits are 1. Otherwise, it's 0.

text
  1100
& 1010
  ----
  1000

Think of it as a Filter: Only things that match survive.

2. Bitwise OR (|)

The OR operator compares two bits. The result is 1 if EITHER bit is 1.

text
  1100
| 1010
  ----
  1110

Think of it as a Merger: Any 1s from either side get combined.

3. Bitwise XOR (^)

The XOR (Exclusive OR) operator compares two bits. The result is 1 if the bits are DIFFERENT. If they are the same, the result is 0.

text
  1100
^ 1010
  ----
  0110

Think of it as a Flipper: It flips bits where there is a 1.

4. Bitwise NOT (~)

The NOT operator only takes one number. It simply flips every single bit. 0 becomes 1, and 1 becomes 0.

text
~ 1100
  ----
  0011

4. The Magic Mask: Bit Shifting

Before we can use AND and OR to control our microcontroller, we need to learn how to create a "Mask".

A mask is simply a binary number that has a 1 in the exact spot (or spots) we care about, and 0s everywhere else. We create masks using the Left Shift (<<) and Right Shift (>>) operators.

The Left Shift operator takes a number and physically pushes all its bits to the left by a certain amount. The spaces left behind on the right are filled with 0s.

c
1// Take the number 1 (00000001) and shift it left by 3 spaces
2uint8_t mask = (1 << 3);

Let's look at what (1 << 3) actually does in slow motion:

  1. Start with the number 1: 00000001
  2. Shift left 1 space: 00000010 (This is now the number 2)
  3. Shift left 2 spaces: 00000100 (This is now the number 4)
  4. Shift left 3 spaces: 00001000 (This is now the number 8)

Boom! We just dynamically created a mask targeting exactly Bit 3.

Why do we use (1 << 3) instead of just typing 8?

If you are reading a datasheet, it will tell you "Enable the timer by setting Bit 5." If you try to do the math in your head to figure out what 2 to the power of 5 is (it's 32), and type Port = 32;, your code becomes completely unreadable to the next person. When you write Port |= (1 << 5);, anyone reading your code immediately knows exactly what you are doing: "Ah, they are setting Bit 5."

Advanced: Multi-Bit Masks

What if a hardware register needs you to set multiple bits at once? For example, setting a 2-bit clock prescaler on Bits 4 and 5. You don't have to do it one by one! You can shift a larger number.

c
1// We want to set Bits 4 and 5 to "11" (which is the number 3 in binary)
2// The number 3 in binary is 00000011.
3// Let's shift the whole block of 1s over by 4 spaces!
4
5uint8_t mask = (3 << 4);
6
7/* What the computer does:
8 Start with 3: 00000011
9 Shift left by 4: 00110000
10*/

Now you have a mask that targets both Bit 4 and Bit 5 simultaneously!


5. The Golden Macros (How to actually control pins)

Now we combine our Operators (AND, OR, XOR) with our Masks (Bit Shifting) to solve the problem we introduced at the start of this lesson.

Here are the 4 fundamental rules of embedded programming. Memorize these.

1. SETTING a Bit (Turning a pin ON)

To turn a specific bit to 1 without changing any other bits, we use OR (|).

c
1// We want to turn on Pin 3.
2// PortA currently equals 10000000 (Motor is on)
3
4PortA = PortA | (1 << 3);
5
6/* What the computer does:
7 10000000 (PortA)
8 | 00001000 (Our Mask: 1 << 3)
9 --------
10 10001000 (New PortA: Motor is STILL on, LED is NOW on!)
11*/
12
13// Shorthand version:
14PortA |= (1 << 3);

2. CLEARING a Bit (Turning a pin OFF)

To force a specific bit to 0 without changing others, we use AND (&) combined with NOT (~).

c
1// We want to turn OFF Pin 7 (the motor), but leave the LED on.
2// PortA currently equals 10001000
3
4PortA = PortA & ~(1 << 7);
5
6/* What the computer does:
7 Step 1: Create mask (1 << 7) -> 10000000
8 Step 2: NOT the mask ~(mask) -> 01111111
9 Step 3: AND with PortA
10
11 10001000 (PortA)
12 & 01111111 (Our inverted mask)
13 --------
14 00001000 (New PortA: Motor is OFF, LED is STILL on!)
15*/
16
17// Shorthand version:
18PortA &= ~(1 << 7);

3. TOGGLING a Bit (Blinking a pin)

If you want to turn a pin ON if it's OFF, and OFF if it's ON, we use XOR (^).

c
1// Toggle the LED on Pin 3
2PortA ^= (1 << 3);

4. CHECKING a Bit (Reading a sensor)

If you want to know if a specific button is pressed (is Bit 2 a 1?), we use AND (&).

c
1// Is the button on Pin 2 currently pressed?
2if (PortA & (1 << 2)) {
3 // The result is not 0, meaning the bit was 1. The button is pressed!
4} else {
5 // The result was 0. The button is not pressed.
6}

6. Real-World Example: STM32 Pinmap

Let's look at a real-world example using an STM32 Microcontroller.

On the STM32, there is a hardware register called GPIOA_ODR (General Purpose Input/Output Port A, Output Data Register). This is a 32-bit variable in memory.

  • Bit 5 of this register physically connects to the green LED on the STM32 development board.
  • Bit 6 of this register connects to a buzzer.

If we want to blink the LED without accidentally turning on the buzzer, we use exactly what we just learned!

c
1#include <stdint.h>
2
3// This is the physical memory address where the GPIOA register lives in the STM32 chip
4#define GPIOA_ODR (*(volatile uint32_t *)(0x40020014))
5
6#define LED_PIN 5
7#define BUZZER_PIN 6
8
9int main() {
10
11 // 1. Turn ON the LED
12 GPIOA_ODR |= (1 << LED_PIN);
13
14 // 2. Turn ON the Buzzer
15 GPIOA_ODR |= (1 << BUZZER_PIN);
16
17 // 3. Turn OFF the LED (buzzer stays on)
18 GPIOA_ODR &= ~(1 << LED_PIN);
19
20 while(1) {
21 // 4. Blink the buzzer forever!
22 GPIOA_ODR ^= (1 << BUZZER_PIN);
23 delay(1000);
24 }
25
26 return 0;
27}

That is the absolute core of embedded programming. Every library, from Arduino's digitalWrite() to ESP-IDF's gpio_set_level(), is just doing this exact bit manipulation behind the scenes!

Previous
Operators & Control Flow