# Embedded C From Zero

> Learn embedded C from zero: program a microcontroller and write bare-metal firmware for the AVR ATmega328P (Arduino Uno). Registers, GPIO, volatile, bit manipulation, interrupts, timers, PWM, and serial - read along and run every example in Wokwi, a free in-browser simulator, no hardware needed.


---

# Embedded C From Zero

You already know C. You compiled it, wrangled pointers, and watched `malloc` hand you memory. Embedded C is that same language pointed at a completely different kind of machine: a chip the size of a fingernail, with no operating system under it, a couple of kilobytes of RAM, and no screen to print to. You do not run your program *on* the chip so much as *become* the chip's entire software. There is nothing else running.

That sounds intimidating and turns out to be freeing. On a desktop, a dozen layers sit between your code and the hardware. Here there are none. When you want a pin to go high, you write a number to a memory address and the voltage on a physical leg of the chip changes. No driver, no system call, no permission check. Direct control is the whole reason people love embedded work, and the reason a two-dollar microcontroller can run a coffee machine, a drone, or a pacemaker.

We teach this the way real firmware gets written: read the code, understand the mechanism. But reading is not all you will do. Embedded C targets real chips, so we target one - the **AVR ATmega328P**, the chip on the Arduino Uno. Its registers are real bare-metal C, and every example runs in **Wokwi**, a free in-browser simulator. You will blink your first LED in a browser tab, no hardware, no purchase, in about a minute.

## Prerequisite

This guide assumes the C from [C From Zero](/guides/c-from-zero): variables, types, pointers, and how a program is compiled. If that feels rusty, read it first - embedded C is unforgiving of half-remembered pointers. It also helps to know roughly how a machine is built underneath. [How a Computer Works](/guides/how-a-computer-works) and [CPU, RAM & Storage](/guides/cpu-ram-and-storage) give you the mental picture of CPU, memory, and buses that a microcontroller makes concrete and touchable.

## How to read this

- **Read the phases in order.** Each one builds on the last. The early phases rewire how you think about "a program"; the later ones put real peripherals under your control.
- **Keep a Wokwi tab open.** When you hit a "Try it" line, run the example. Seeing an LED blink because *you* wrote to a register teaches more than any paragraph can.
- **C rusty?** Detour through [C From Zero](/guides/c-from-zero) first, then come back here.

## The phases

1. **[What "Embedded" Even Means](01-what-embedded-even-means.md)** - bare metal, tiny memory, and the super-loop that never exits.
2. **[The C That Changes](02-the-c-that-changes.md)** - fixed-width types, `volatile`, why `malloc` is avoided, and the memory map.
3. **[Bit Manipulation, Properly](03-bit-manipulation.md)** - setting, clearing, and testing single bits, the daily language of hardware.
4. **[Talking to Hardware: Registers and GPIO](04-registers-and-gpio.md)** - what a register really is, and driving and reading pins with `DDRB`, `PORTB`, and `PINB`.
5. **[Timing and Interrupts](05-timing-and-interrupts.md)** - doing things on time without blocking, and letting the hardware call you back.
6. **[A Peripheral Tour: PWM, Serial, and Sensors](06-a-peripheral-tour.md)** - dimming an LED, talking over UART, and reading the outside world.
7. **[The Toolchain and the Real Workflow](07-the-toolchain-and-workflow.md)** - compilers, flashing, and how firmware actually gets onto a chip.
8. **[Where to Go Next](08-where-to-go-next.md)** - other chips, real-time operating systems, and what to build so it sticks.

> This guide takes you from zero to real firmware on one chip. Deeper ground - a real-time operating system in depth, DMA, low-power and sleep modes, and the jump to 32-bit ARM (STM32) - is a natural follow-up once the fundamentals here are solid.


---

# What "Embedded" Even Means

Every C program you have written so far ran as a guest. Something bigger - an operating system - handed your program some memory, gave it a terminal to print to, let it open files, and cleaned up after it when `main` returned. You never thought about that host because it was always there.

Embedded C takes the host away. Your code runs on a microcontroller: a whole tiny computer on a single chip, with the processor, the memory, and the connections to the outside world all baked into one piece of silicon. There is no operating system underneath you. There is no terminal, no file system, no `printf` writing to a screen. When your program starts, *it is the only software on the chip.* That is what "bare metal" means, and it changes what a program even looks like.

## Bare metal: nothing underneath you

On a desktop, "output" means text on a screen or bytes in a file. On a microcontroller, none of that exists by default. The chip's connection to the world is its **pins** - the metal legs sticking out of it - and your program's output is a pin going high or low. Driving a pin high puts a voltage on it (on the ATmega328P, about 5 volts); driving it low puts it at 0 volts. Wire an LED to that pin and "output" becomes a light turning on.

So the first mental shift is this: **you do not print, you act on the physical world.** No `printf`, no `console.log`. You set a pin, and something in the real world changes.

The second shift is size. The desktop you are reading this on has gigabytes of RAM and a processor running at billions of cycles per second. The ATmega328P has about **2 kilobytes of RAM**, **32 kilobytes of flash** to hold your compiled program, and runs at **16 million cycles per second** on the Uno. That is not a typo. Two kilobytes. A single large array can swallow your entire memory, so you stop assuming space is free and start counting bytes. There is no garbage collector and no virtual memory to lean on - what you allocate is what you have.

## Three words you will hear constantly

📝 **Terminology.**
- **Microcontroller** - a complete small computer on one chip: CPU, RAM, flash storage, and pins to the outside world, all together. The ATmega328P is one. It is not a "chip that helps a computer"; it *is* the computer.
- **Firmware** - the software that runs on that chip. It is called firmware, not software, because it lives in the device permanently and rarely changes. The code you are about to write is firmware.
- **Embedded** - the whole field. Software "embedded" inside a physical device (a thermostat, a microwave, a car's engine controller) rather than running on a general-purpose computer. The device is not sold as a computer, but there is one inside it running your firmware.

## The program that never ends

Here is the structural shock. A desktop program has a life story: it starts, does its work, and exits. `main` returns, the process ends, control goes back to the operating system.

Firmware has nowhere to return *to*. If your `main` finished and returned on a microcontroller, the chip would have nothing to run and would sit there doing... whatever the leftover state happens to do, which is never what you want. So embedded programs are built as a **super-loop**: set things up once, then loop forever.

```c
int main(void) {
    // setup: runs one time
    // (configure pins, peripherals, and so on)

    while (1) {
        // the loop: runs forever
        // read inputs, decide, drive outputs, repeat
    }
    // we never reach here
}
```

That `while (1)` is not a mistake or a placeholder - it is the shape of almost every piece of firmware ever written. The chip powers on, runs your setup once, then runs the loop body again and again until the power is cut. Your program does not end; it *is* the device, for as long as the device is on.

```mermaid
flowchart TD
  A[Power on] --> B[Setup: run once]
  B --> C[Read inputs]
  C --> D[Do work]
  D --> E[Drive outputs]
  E -->|loop forever| C
```

⚠️ **Gotcha.** Coming from desktop C, an infinite loop feels like a bug you would get yelled at for. On bare metal it is the correct and expected structure. The mistake here would be letting `main` return, not looping forever.

## Blink an LED in 60 seconds

Enough theory. Let us make a real chip do something, with nothing but a browser tab.

The Arduino Uno has an LED soldered onto the board, wired to one specific pin. In ATmega328P terms that pin is **PB5** - bit 5 of a group of pins the chip calls "Port B." (On the Uno's own numbering it is labelled "pin 13," and the little LED is marked "L".) We are going to turn that LED on and off directly, by writing to the chip's registers. Do not worry about *why* the registers are named this way yet - that is Phase 4. For now, read along and watch it work.

```c
#include <avr/io.h>
#include <util/delay.h>

int main(void) {
    DDRB |= (1 << PB5);          // make PB5 an output pin

    while (1) {
        PORTB |= (1 << PB5);     // drive PB5 high: LED on
        _delay_ms(500);
        PORTB &= ~(1 << PB5);    // drive PB5 low: LED off
        _delay_ms(500);
    }
}
```

Read it top to bottom. `DDRB` is the "data direction" control for Port B; setting bit 5 tells the chip that PB5 is an output we intend to drive, not an input we read. Then, forever: `PORTB |= (1 << PB5)` switches PB5 on, we wait half a second, `PORTB &= ~(1 << PB5)` switches it off, we wait again. The onboard LED blinks: half a second on, half a second off.

There is no `main` return, no exit, no operating system. Three registers and a loop, and a physical light responds.

▶ **Try it:** start a new Arduino Uno project at [wokwi.com](https://wokwi.com), paste this in, and Run. The little "L" LED on the simulated board starts blinking.

📝 **Terminology.** A Wokwi Arduino project normally hands you two functions, `setup()` and `loop()`, instead of a bare `main()`. That is the same super-loop split in two: `setup()` is the run-once part, `loop()` is the body of `while (1)`. The build links your `main()` above in place of the one the Arduino library would supply, so it runs as shown - and from Phase 4 on we use the `setup()` / `loop()` form so each example drops straight into a fresh sketch.

💡 **Key point.** That code is the whole program. Not a function called by a framework, not a script an interpreter reads - the entire software on the chip. When people say embedded gives you "direct control of the hardware," this is what they mean: `PORTB |= (1 << PB5)` is you, in C, changing the voltage on a physical pin, with nothing in between.

⚠️ **Gotcha.** `_delay_ms` needs to know the clock speed to count out real time, which it reads from a value called `F_CPU`. The Arduino and Wokwi build set `F_CPU` to `16000000` (16 MHz) for you, so the example above works as-is. In a from-scratch build outside Arduino you must define it yourself, before including the delay header:

```c
#define F_CPU 16000000UL
#include <util/delay.h>
```

Get `F_CPU` wrong and your delays are wrong by the same ratio - a common first-day surprise.

Quick gut check before the recap:

```quiz
[
  {
    "q": "Why is firmware built as a super-loop with `while (1)`, when a desktop program returns from `main` and exits?",
    "choices": [
      "Because infinite loops run faster on small chips",
      "Because the C standard requires embedded programs to loop",
      "There is no operating system to return to - if `main` returned, the chip would have nothing to run, so firmware loops forever until the power is cut",
      "To stop the LED from ever turning off"
    ],
    "answer": 2,
    "explain": "On bare metal nothing is waiting to reclaim the program. `main` never returns; the super-loop runs the device for as long as it has power."
  },
  {
    "q": "On a bare-metal microcontroller, what is the closest thing to a program's \"output\"?",
    "choices": [
      "A line printed to the terminal with printf",
      "A voltage on a physical pin - for example driving PB5 high to light an LED",
      "A file written to disk",
      "The return value of main"
    ],
    "answer": 1,
    "explain": "There is no terminal, no files, and no printf underneath you. The program acts on the world by setting pins high or low."
  },
  {
    "q": "The ATmega328P has about 2 KB of RAM and 32 KB of flash. What does that force you to do differently from desktop C?",
    "choices": [
      "Be deliberate about memory: a few large arrays can use up all your RAM, so you count bytes instead of assuming space is free",
      "Nothing - the compiler manages memory for you",
      "Write the whole program in assembly",
      "Rely on a garbage collector to reclaim space"
    ],
    "answer": 0,
    "explain": "With kilobytes instead of gigabytes, memory is a budget you track. There is no garbage collector and no virtual memory to fall back on."
  }
]
```

## Recap

1. Embedded C runs on **bare metal**: a microcontroller with no operating system, no terminal, no files, and no `printf`. The output is a physical pin going high or low.
2. A microcontroller is a whole tiny computer on one chip, with very little memory - the ATmega328P has about 2 KB of RAM and 32 KB of flash, so memory is a budget you count, not a resource you assume.
3. Firmware is a **super-loop**: set up once, then `while (1)` forever. `main` never returns, because there is no host to return to.
4. Terminology worth pinning down: *microcontroller* (the chip-computer), *firmware* (the software on it), *embedded* (software living inside a physical device).
5. You blinked an LED with `DDRB` and `PORTB` in a browser tab - real bare-metal register code, running on a simulated ATmega328P, no hardware required.

Next up, [The C That Changes](02-the-c-that-changes.md): the handful of C habits that flip the moment you leave the desktop behind.


---

# The C That Changes

Here is the good news: the C you already know still compiles and still works. Loops loop, structs group, pointers point. Embedded C is not a different language. What changes is a small set of habits - things that were harmless on a desktop and become load-bearing on a chip with 2 kilobytes of RAM and hardware wired directly into memory. This phase is those habits, one at a time.

## Fixed-width types, because `int` lies

On a desktop you probably treat `int` as "a 4-byte number" without thinking. That was never guaranteed. The C standard only promises `int` is *at least* 16 bits; the exact width is up to the compiler and the target. On your laptop it is usually 4 bytes. On the ATmega328P, an 8-bit chip, **`int` is 2 bytes.** Same keyword, different size, and code that quietly assumed 4 bytes can overflow where you did not expect.

The fix is to say exactly what you mean. `<stdint.h>` gives you types whose size is right there in the name:

- `uint8_t` - unsigned, exactly 1 byte, range 0 to 255
- `uint16_t` - unsigned, exactly 2 bytes, range 0 to 65535
- `uint32_t` - unsigned, exactly 4 bytes
- and signed versions: `int8_t`, `int16_t`, `int32_t`

These are the same size on every machine, which is exactly what you want when a variable maps to a hardware value or a byte in a protocol. Let us prove the sizes on the real target. There is no `printf` here, so to see values we send them over the USB serial link with Arduino's `Serial` helper, and read them in a serial monitor.

```c
#include <stdint.h>

void setup() {
    Serial.begin(9600);
    Serial.print("int:      "); Serial.println(sizeof(int));
    Serial.print("uint8_t:  "); Serial.println(sizeof(uint8_t));
    Serial.print("uint32_t: "); Serial.println(sizeof(uint32_t));
    Serial.print("pointer:  "); Serial.println(sizeof(void *));
}

void loop() {
}
```

```console
int:      2
uint8_t:  1
uint32_t: 4
pointer:  2
```

*What just happened:* on this 8-bit chip an `int` is 2 bytes, not the 4 you would see on most laptops. The `uint8_t` and `uint32_t` are exactly 1 and 4 bytes, and they would be on any machine - that fixed size is the reason embedded code reaches for them. Even a pointer is only 2 bytes here, because the chip's whole address space fits in 16 bits.

▶ **Try it:** start a new Arduino Uno project at [wokwi.com](https://wokwi.com), paste this in, Run, and open the serial monitor.

📝 **Terminology.** You may notice this example uses `setup()` and `loop()` instead of the `main()` with `while (1)` from Phase 1. That is the same super-loop - the Arduino framework writes the `main()` and the infinite loop for you, calls `setup()` once, then calls `loop()` forever. Underneath, it is identical to what you wrote by hand.

## `volatile`: this can change behind your back

This is the keyword that separates embedded C from the rest, and the one that produces the most baffling bugs when it is missing.

**What it actually is.** `volatile` is a promise you make to the compiler in reverse: it tells the compiler *"this variable can change without any code you can see changing it, so never cache it in a register - re-read it from memory every single time."*

**Why that matters.** A C compiler is aggressive. If it sees you read a variable in a loop and nothing in the loop writes to it, it reasonably concludes the value cannot change, reads it once into a fast register, and reuses that copy. On a desktop that optimization is invisible and correct. On a chip, two things really do change memory behind the compiler's back: **hardware registers**, and **variables written by an interrupt** (code the hardware calls on its own, which we cover in Phase 5). The compiler cannot see those writes, so it optimizes as if they never happen.

Here is the exact bug. We set a flag inside an interrupt and wait for it in `main`:

```c
#include <avr/io.h>
#include <avr/interrupt.h>

uint8_t button_pressed = 0;      // set by the interrupt below

ISR(INT0_vect) {                 // hardware runs this when the button fires
    button_pressed = 1;
}

int main(void) {
    // ... configure the INT0 interrupt on a pin ...
    sei();                       // turn interrupts on

    while (button_pressed == 0) {
        // wait here until the interrupt sets the flag
    }

    // ... react to the press ...
}
```

Compiled with optimization on, this can **hang forever, even after you press the button.** The compiler looked at the `while` loop, saw that nothing inside it writes `button_pressed`, loaded the value once, and now spins on that stale copy in a register. The interrupt dutifully writes `1` into memory, but the loop is not looking at memory anymore.

The fix is one word:

```c
volatile uint8_t button_pressed = 0;
```

Now every check of `button_pressed` re-reads it from RAM, so the interrupt's write is seen and the loop exits on the next pass.

🪖 **War story.** A classic firmware ghost story: the code works perfectly in the debug build and hangs in the release build. The cause is almost always a flag shared with an interrupt that is missing `volatile`. Debug builds turn optimization off, so the stale-register trick never happens and the bug hides. Ship the optimized build and it appears. One keyword, hours of confusion.

💡 **Key point.** You do *not* need `volatile` on the hardware registers from `<avr/io.h>` like `PORTB` or `PINB` - the header already declares them volatile for you. You add `volatile` to *your own* variables that an interrupt touches.

## Why `malloc` mostly stays in the box

On a desktop you reach for `malloc` without a second thought. In small firmware it is usually avoided, and the reasons are all about the constraints from Phase 1.

- **There is barely any room.** A heap needs free RAM to hand out. With 2 kilobytes total, there is little to spare, and a heap that runs dry fails in ways that are hard to recover from on a device with no user watching.
- **Fragmentation with no reset.** Desktop programs start and stop constantly, which resets the heap. Firmware can run for months without a reboot. Allocate and free different sizes for long enough and the free space breaks into scattered gaps: you can have plenty of total free memory and still fail to allocate one contiguous block.
- **Nondeterminism.** How long `malloc` takes depends on the state of the heap, so it is not predictable. Firmware often has hard timing requirements, and "usually fast" is not good enough.

The embedded habit is **static allocation**: fixed-size arrays and variables whose size is known at compile time. You decide up front "this buffer is 64 bytes" and it lives at a fixed spot for the life of the program. Predictable, no fragmentation, and you can see your entire memory budget by reading the code.

## The memory map: three kinds of address

To make `volatile` and the type sizes click, you need the picture of where things live. On the ATmega328P, addresses fall into three regions, and they behave very differently.

```mermaid
flowchart TD
  CPU[ATmega328P CPU] --> Flash[Flash 32 KB: program code, read-only at runtime]
  CPU --> RAM[RAM 2 KB: variables, the scarce space]
  CPU --> IO[I/O registers: addresses wired to pins]
  IO --> Pins[Physical pins]
```

- **Flash (32 KB)** holds your compiled program and any constants you mark as read-only. It is not rewritten while the program runs - it is where the code lives.
- **RAM (2 KB)** holds your variables: the stack, globals, and anything that changes at runtime. This is the scarce resource you budget.
- **Registers** are the surprising part. Some memory addresses are not ordinary storage at all - they are wired straight to the hardware. Writing to the address named `PORTB` does not "save a value," it changes the voltage on real pins. Reading the address named `PINB` reads the actual state of those pins right now. That is why those addresses must be `volatile`: their contents change because of the physical world, not because your code assigned them.

That last idea - a memory address that *is* a piece of hardware - is the heart of embedded programming, and the whole subject of Phase 4.

Quick gut check before the recap:

```quiz
[
  {
    "q": "You need a counter that only ever holds 0 to 200 on the ATmega328P. Why prefer `uint8_t` over `int`?",
    "choices": [
      "uint8_t is always faster than int on every processor",
      "`int` has no guaranteed size in C - it is 2 bytes on this chip and often 4 on a laptop - while `uint8_t` is exactly one byte everywhere, so you know the size and range you are getting",
      "int cannot store the value 200",
      "uint8_t automatically prevents the counter from overflowing"
    ],
    "answer": 1,
    "explain": "The width of `int` is implementation-defined: 2 bytes here, 4 on a typical desktop. Fixed-width types from <stdint.h> pin down size and range so you are not guessing per target."
  },
  {
    "q": "You set a flag inside an interrupt and poll it with `while (flag == 0) { }`, but the loop never exits in the optimized build. What is the most likely cause?",
    "choices": [
      "The interrupt is never actually running",
      "A variable cannot be read inside a while loop",
      "`flag` is not declared `volatile`, so the compiler cached it in a register and never re-reads the memory the interrupt writes",
      "The chip's RAM is faulty"
    ],
    "answer": 2,
    "explain": "Without volatile the compiler may hoist the read out of the loop and spin on a stale copy. `volatile` forces a fresh read from memory each time, so the interrupt's write is seen."
  },
  {
    "q": "Why is `malloc` usually avoided in small embedded firmware?",
    "choices": [
      "malloc does not exist in C",
      "With kilobytes of RAM and code that may run for months without a reboot, heap fragmentation and out-of-memory failures become likely and hard to predict, so static allocation is safer",
      "malloc is too slow to call even once",
      "The compiler refuses to link programs that call malloc"
    ],
    "answer": 1,
    "explain": "Tiny RAM plus long uptime makes fragmentation and nondeterministic failures a real risk. Fixed, compile-time allocation keeps memory use predictable."
  }
]
```

## Recap

1. Use **fixed-width types** from `<stdint.h>` (`uint8_t`, `uint16_t`, `uint32_t`). Plain `int` has no guaranteed size - it is 2 bytes on the ATmega328P and often 4 on a desktop.
2. **`volatile`** tells the compiler a variable can change behind its back, so it re-reads from memory every time. Use it for variables shared with interrupts; hardware registers from `<avr/io.h>` are already volatile for you.
3. Without `volatile`, a flag set by an interrupt and polled in a loop can get cached in a register, and the loop spins forever - the bug that works in debug and hangs in release.
4. Avoid `malloc` on tiny chips: little room, fragmentation over long uptime, and nondeterministic timing. Prefer **static, compile-time allocation**.
5. There is no `printf` on bare metal - you send bytes over serial or act on pins - and `sizeof` lets you check type sizes on the actual target.
6. Memory splits three ways: **flash** (code, read-only at runtime), **RAM** (your scarce variables), and **registers** (special addresses wired straight to the hardware).

Next up, [Bit Manipulation, Properly](03-bit-manipulation.md): the `|=`, `&=`, and `<<` you saw in the blink, taught slowly, because on hardware you flip single bits all day long.


---

# Bit Manipulation, Properly

This is the daily bread of embedded work. A hardware register is a row of eight (or sixteen, or thirty-two) tiny switches packed into one number, and each switch controls something physical: a pin's direction, whether an interrupt is armed, the speed of a timer. You will spend your career flipping one of those switches without disturbing the seven next to it. The four patterns in this phase are how you do that, and once they are in your fingers you will read hardware code that used to look like line noise.

Here is the mental model to hold onto: **you almost never assign a whole register a value. You change specific bits and leave the rest exactly as they were.** Everything below is built to do that safely.

## The one trick behind all four: `1 << n`

`1 << n` means "take the number 1 and shift it left by `n` positions." It builds a **mask**: a number that is all zeros except for a single 1 sitting in position `n`.

```c
1 << 0   ==  0b00000001
1 << 3   ==  0b00001000
1 << 5   ==  0b00100000
```

That lone 1 is the switch you want to touch. The four operations below all combine that mask with your register using a bitwise operator, and the operator you pick decides whether the bit gets forced on, forced off, flipped, or read.

## The four operations

**Set** a bit (force it to 1): `reg |= (1 << n)`

OR-ing with the mask puts a 1 in position `n`. Every other bit is OR-ed with a 0, and `anything | 0` is unchanged, so the rest stay exactly as they were.

**Clear** a bit (force it to 0): `reg &= ~(1 << n)`

`~(1 << n)` is the mask flipped inside out: all ones except a single 0 in position `n`. AND-ing with it forces bit `n` to 0 (because `anything & 0` is 0) while every other bit is AND-ed with a 1 and survives untouched.

**Toggle** a bit (flip whatever it is): `reg ^= (1 << n)`

XOR with a 1 flips a bit; XOR with a 0 leaves it alone. So the mask flips only position `n`. This is how you blink an LED: toggle the same bit over and over.

**Test** a bit (is it 1?): `if (reg & (1 << n))`

AND with the mask keeps only bit `n` and zeros everything else. The result is nonzero when the bit is set and zero when it is not, which is exactly what an `if` wants.

Here is a before-and-after view of each one acting on a single byte:

| Operation | Start | Mask | Result |
|---|---|---|---|
| Set bit 2 | `00000000` | `00000100` | `00000100` |
| Clear bit 2 | `00000100` | `00000100` | `00000000` |
| Toggle bit 5 | `00000000` | `00100000` | `00100000` |
| Test bit 5 | `00100000` | `00100000` | nonzero (set) |

## Read-modify-write: what `|=` really does

`reg |= (1 << 2)` is not one step, it is three. The compiler expands it to: **read** the current value of `reg`, **modify** it by OR-ing in the mask, then **write** the result back.

```mermaid
flowchart LR
  A["Read current reg"] -->|value in CPU| B["OR in the mask"]
  B -->|new value| C["Write back to reg"]
```

That read step is the whole point. It is what preserves the bits you are not touching. If you instead wrote `reg = (1 << 2)`, you would throw away every other bit and set the register to a bare `00000100`. Beginners reach for plain `=` and wonder why turning on one pin turned off three others. The answer is that `=` overwrites; `|=`, `&=`, and `^=` preserve.

## A worked example

This runs on your normal PC (bit math is the same everywhere), so you can watch the byte change. The helper prints all eight bits so you can see which switch moved.

```c
#include <stdio.h>
#include <stdint.h>

void print_byte(uint8_t x) {
    for (int i = 7; i >= 0; i--) {
        putchar((x & (1 << i)) ? '1' : '0');
    }
    putchar('\n');
}

int main(void) {
    uint8_t reg = 0;        // eight switches, all off

    reg |= (1 << 2);        // set bit 2
    print_byte(reg);

    reg |= (1 << 5);        // set bit 5, leaving bit 2 alone
    print_byte(reg);

    reg &= ~(1 << 2);       // clear bit 2, leaving bit 5 alone
    print_byte(reg);

    reg ^= (1 << 5);        // toggle bit 5 (turns it off)
    print_byte(reg);

    reg ^= (1 << 5);        // toggle bit 5 again (back on)
    print_byte(reg);

    if (reg & (1 << 5)) {
        printf("bit 5 is set\n");
    }
    return 0;
}
```
```console
$ gcc bits.c -o bits && ./bits
00000100
00100100
00100000
00000000
00100000
bit 5 is set
```

*What just happened:* setting bit 5 did not disturb bit 2, and clearing bit 2 did not disturb bit 5. Each operation reached into the byte and moved exactly one switch. That independence is the entire reason these patterns exist, and it is what lets you configure one pin of a port without breaking the others.

💡 **Key point.** When you see `reg |= (1 << PIN)` in a datasheet example or someone's driver code, read it out loud as "set the PIN bit of reg, leave the rest." You now know the other three on sight too.

## The gotcha that gets everyone

The bitwise operators have **lower** precedence than `==`. That single fact causes one of the most common embedded bugs there is:

```c
if (status & FLAG == 0) { ... }   // WRONG
```

You meant "is the FLAG bit clear?" But because `==` binds tighter than `&`, C reads it as `status & (FLAG == 0)`. If `FLAG` is nonzero, `FLAG == 0` is `0`, so the whole thing becomes `status & 0`, which is always `0`, so the branch never runs. The code compiles clean and fails silently.

⚠️ **Gotcha.** Always parenthesize a bit test: write `if ((status & FLAG) == 0)`. When in doubt, wrap the mask expression in parentheses. Compilers will not warn you about the precedence version by default, and a logic analyzer will not either - it looks like your hardware is broken when your parentheses are.

One more, for wide registers: `1 << n` uses a plain `int` for the `1`, so shifting by the width of an `int` or more (`1 << 32` on a 32-bit `int`) is undefined behavior, and `1 << 31` sets the sign bit, which is also trouble. On an 8-bit AVR register you will never shift past 7, so it does not bite there, but for a 32-bit peripheral register (as on ARM) write the mask as `1UL << n` to keep it unsigned and well defined.

## Recap

1. `1 << n` builds a **mask**: all zeros except a single 1 in position `n`.
2. **Set** with `reg |= (1 << n)`, **clear** with `reg &= ~(1 << n)`, **toggle** with `reg ^= (1 << n)`, **test** with `reg & (1 << n)`.
3. These change one bit and leave the rest alone. Plain `reg = ...` overwrites everything - almost never what you want on hardware.
4. `reg |= mask` is a **read-modify-write**: read the register, change bits, write it back. The read is what preserves the untouched bits.
5. Bitwise operators bind looser than `==`. Parenthesize every bit test: `if ((reg & MASK) == 0)`.
6. For registers wider than 16 bits, use `1UL << n` so the shift stays unsigned and defined.

Every register access in the next two phases is one of these four moves. Get comfortable here and the hardware code stops looking like magic.

### Check yourself

```quiz
[
  {
    "q": "You want to turn on bit 3 of a register without changing any other bit. Which line does it?",
    "choices": [
      "reg = (1 << 3);",
      "reg |= (1 << 3);",
      "reg &= (1 << 3);",
      "reg ^= (1 << 3);"
    ],
    "answer": 1,
    "explain": "OR-ing with a single-bit mask forces that bit to 1 and leaves the rest untouched.",
    "why": [
      "Plain assignment overwrites the whole register, clearing every other bit.",
      null,
      "AND with a single-bit mask clears all the OTHER bits to 0.",
      "XOR toggles bit 3, so it only ends up set if it happened to be 0 first."
    ]
  },
  {
    "q": "What is wrong with `if (status & FLAG == 0)`?",
    "choices": [
      "Nothing, it correctly checks whether FLAG is clear",
      "== binds tighter than &, so it parses as status & (FLAG == 0) and misbehaves",
      "You cannot use == together with bitwise operators at all",
      "It should use || instead of &"
    ],
    "answer": 1,
    "explain": "== has higher precedence than &, so this reads as status & (FLAG == 0). Wrap the mask test in parentheses: (status & FLAG) == 0."
  },
  {
    "q": "Why is `reg |= (1 << 2);` called a read-modify-write?",
    "choices": [
      "It reads the current value of reg, ORs in the new bit, then writes the result back",
      "It reads from one register and writes to a different one",
      "It modifies the value twice before writing",
      "It only writes and never reads"
    ],
    "answer": 0,
    "explain": "Compound assignment expands to: read reg, OR it with the mask, store the result. The read is what preserves the bits you are not touching."
  }
]
```


---

# Talking to Hardware: Registers and GPIO

In phase 3 you learned to flip individual bits in a number. Now the payoff: on a microcontroller, some of those numbers are not ordinary variables. They are **registers**, and flipping a bit in one moves real voltage on a real pin. This is where C stops being about data on a screen and starts controlling the physical world.

We will use the AVR ATmega328P, the chip on an Arduino Uno, because its registers are simple, well documented, and you can run every example in this phase for free in [Wokwi](https://wokwi.com), an in-browser simulator, without owning any hardware.

## What a register actually is

A hardware register is a fixed memory address that the chip's designers wired directly to a piece of hardware. When your C code writes a byte to that address, you are not storing data for later - you are setting the state of the circuitry connected to it. Read from that address and you are sampling the actual voltage on the pins.

On the ATmega328P, the register named `PORTB` lives at data address `0x25`. Write a bit there and a physical pin changes. That is the whole idea:

```mermaid
flowchart LR
  A["Your code sets bit PB5 in PORTB"] -->|write| B["Data address 0x25 = PORTB"]
  B -->|latches bit 5| C["Port B output driver"]
  C -->|drives 5V| D["Physical pin 13 (PB5)"]
  D -->|current flows| E["On-board LED lights"]
```

📝 **Terminology.** A **GPIO** pin is a General-Purpose Input/Output pin: one you control from software, as either an output (you set its voltage) or an input (you read its voltage). The ATmega328P groups its GPIO pins into **ports** named B, C, and D, and each port has three registers that control it.

## The three registers behind every port

For any port `x` (B, C, or D), you get three registers, and phase 3's bit operations are exactly how you drive them:

- **`DDRx`** - the Data Direction Register. A `1` bit makes that pin an **output**; a `0` bit makes it an **input**. This is the first thing you set.
- **`PORTx`** - for an output pin, the bit you write here is the level: `1` drives it high, `0` drives it low. For an *input* pin, writing `1` here enables that pin's internal pull-up resistor (more on that below).
- **`PINx`** - read this to sample the actual voltage on the pins right now. This is how you read a button.

You find these names, and every bit inside them, in the chip's **datasheet** - the ATmega328P datasheet has a register summary and a chapter per peripheral. That document is the source of truth for embedded work. Nobody memorizes it; you learn to look things up in it.

## Hello, world: blink the on-board LED

The Arduino Uno has an LED soldered to pin 13, which the AVR calls **`PB5`** (bit 5 of port B). Blinking it is the embedded equivalent of printing "hello, world."

```c
#include <avr/io.h>
#include <util/delay.h>

void setup() {
    DDRB |= (1 << PB5);        // PB5 is an OUTPUT (Uno pin 13, the on-board LED)
}

void loop() {
    PORTB |= (1 << PB5);       // drive PB5 high: LED on
    _delay_ms(500);
    PORTB &= ~(1 << PB5);      // drive PB5 low: LED off
    _delay_ms(500);
}
```

`setup()` runs once at power-up and `loop()` runs forever after - they are the two functions the AVR's startup code calls for you. In a from-scratch bare-metal program you would write the same thing as `int main(void)` with the setup at the top and the blink inside a `while (1)` loop; `setup` and `loop` are that split into two named functions. The register code inside is identical either way. The names `PB5`, `DDRB`, and `PORTB` come from `<avr/io.h>`.

▶ **Try it:** start a new Arduino Uno project at [wokwi.com](https://wokwi.com), paste this in, and Run. The on-board LED blinks once a second, no wiring needed.

## Reading a button, and why inputs need help

Now flip the direction and read a pin. Wire a pushbutton from pin **PD2** to ground. To read it you configure PD2 as an input, then sample `PIND`:

```c
#include <avr/io.h>

void setup() {
    DDRB  |= (1 << PB5);       // LED output, as before
    DDRD  &= ~(1 << PD2);      // PD2 is an INPUT (the button)
    PORTD |=  (1 << PD2);      // enable the internal pull-up on PD2
}

void loop() {
    if (PIND & (1 << PD2)) {   // pull-up holds this HIGH while the button is open
        PORTB &= ~(1 << PB5);  // not pressed: LED off
    } else {
        PORTB |=  (1 << PB5);  // pressed (pin pulled to ground): LED on
    }
}
```

Notice the logic looks inverted: the LED turns on when the read is *low*. That is because of the pull-up, and here is why it has to be there.

An input pin is extremely high-impedance - it barely draws any current, so it is happy to float. If a pin is configured as an input and nothing is actively driving it (the button is open, connecting it to nothing), its voltage drifts with stray electrical noise from nearby wires, your hand, the power supply. Read it and you get unpredictable 1s and 0s. A **floating input reads garbage.**

⚠️ **Gotcha.** A disconnected input is not a reliable 0. It is a floating antenna. If your button "works sometimes" or triggers on its own, an unconnected or un-pulled input is the first suspect.

The fix is a **pull-up** (or pull-down) resistor: a resistor that gently ties the pin to a known level when nothing else is driving it. With a pull-up, the pin sits **high** by default; press the button to connect it to ground and it goes **low**. The ATmega328P has a pull-up resistor built into every pin - you enable it by writing a `1` to the pin's `PORTx` bit while the pin is an input, which is what `PORTD |= (1 << PD2)` does above. That one line saves you from soldering an external resistor.

▶ **Try it:** in the same Wokwi project, add a pushbutton connected from pin 2 to GND, paste this in, and Run. Hold the button and the on-board LED lights; release it and the LED goes off.

## Recap

1. A **register** is a memory address wired to hardware. Writing it moves real voltage on a pin; reading it samples real voltage.
2. Each AVR port has three registers: **`DDRx`** sets direction (1 = output, 0 = input), **`PORTx`** sets an output's level (or enables an input's pull-up), and **`PINx`** reads the pins.
3. On the Uno, the on-board LED is **`PB5`** (pin 13). `DDRB |= (1 << PB5)` makes it an output; `PORTB |= (1 << PB5)` turns it on.
4. To read a button: `DDRD &= ~(1 << PD2)` makes PD2 an input, then test `PIND & (1 << PD2)`.
5. A floating input reads noise. A **pull-up** ties it to a known level; enable the AVR's internal one with `PORTD |= (1 << PD2)`, which makes an idle button read HIGH and a press read LOW.
6. The **datasheet** is where every register name and bit lives. Look things up; do not memorize.

You can now light a pin and read a pin. Phase 5 makes the reading efficient: instead of asking the button "are you pressed yet?" a million times a second, you let the hardware tell you the instant it happens.

### Check yourself

```quiz
[
  {
    "q": "What does `DDRB |= (1 << PB5);` do?",
    "choices": [
      "Sets PB5 high",
      "Configures PB5 as an output pin",
      "Reads the current state of PB5",
      "Enables the pull-up on PB5"
    ],
    "answer": 1,
    "explain": "DDRB is the data-direction register for port B. A 1 makes that pin an output; a 0 makes it an input. It sets direction, not level.",
    "why": [
      "PORTB, not DDRB, sets the output level once the pin is an output.",
      null,
      "PINB reads the pin; DDRB only sets its direction.",
      "The pull-up is enabled through PORTB while the pin is an input, not through DDRB."
    ]
  },
  {
    "q": "A pin is configured as an input with nothing connected to it. Why might reading it give random 1s and 0s?",
    "choices": [
      "The datasheet must be wrong",
      "The pin is floating - with no pull-up or pull-down it drifts with electrical noise",
      "Inputs always read zero",
      "You forgot to delay before reading"
    ],
    "answer": 1,
    "explain": "A floating input drives nothing and is driven by nothing, so its voltage wanders with stray noise and reads unpredictably. A pull-up or pull-down ties it to a known level."
  },
  {
    "q": "With the internal pull-up enabled on PD2 and a button wired from PD2 to ground, what does `PIND & (1 << PD2)` read when the button is NOT pressed?",
    "choices": [
      "Zero (LOW)",
      "Nonzero (HIGH), because the pull-up holds the pin high until the button grounds it",
      "It is undefined",
      "It depends on the value of DDRB"
    ],
    "answer": 1,
    "explain": "The pull-up holds the pin HIGH while the button is open, so the read is nonzero. Pressing connects the pin to ground, pulling it LOW. This is why button logic often looks inverted."
  }
]
```


---

# Timing and Interrupts

At the end of phase 4 your program read a button by asking, over and over, "are you pressed yet?" That works, but it wastes the entire CPU doing nothing but asking, and if the interesting event is faster than your asking, you miss it. This phase is about the better way: let the hardware interrupt you the instant something happens, and let a timer keep time in the background so your CPU is free for real work.

## Polling versus interrupts

**Polling** is a loop that keeps checking: `while (1) { if (PIND & (1 << PD2)) ... }`. It is simple and predictable, but the processor is pinned to that check and can do nothing else useful. Worse, if you slip a delay into the loop (say, to blink an LED), a quick button tap that happens *during* the delay is gone - you were not looking.

An **interrupt** flips the relationship around. You tell the hardware "when this pin goes low, stop whatever you are doing and run this function." Then your main code gets on with its life, and the moment the event fires, the CPU pauses the main program, runs your handler, and resumes exactly where it left off. Nothing is missed, and no cycles are burned waiting.

```mermaid
flowchart LR
  A["Button press on PD2"] -->|falling edge| B["Hardware sets INT0 flag"]
  B -->|CPU pauses main| C["ISR(INT0_vect) runs"]
  C -->|sets volatile flag| D["button_pressed = 1"]
  D -->|loop checks it| E["Main loop toggles LED"]
```

📝 **Terminology.** The function the CPU jumps to is an **ISR**, an Interrupt Service Routine. Each interrupt source has a fixed slot called a **vector**; `INT0_vect` names the slot for external interrupt 0. You write an ISR with the `ISR(vector_name)` macro from `<avr/interrupt.h>`.

## The rules of an ISR

An ISR is not a normal function, and treating it like one is how beginners hang their boards. Three rules:

1. **Keep it short.** Do the bare minimum and return. An ISR usually runs with other interrupts held off, and while it runs your main program is frozen. A slow ISR delays everything.
2. **Never block inside it.** No `_delay_ms()`, no waiting on `Serial`, no long loops. Blocking in an ISR stalls the whole system, including the timers other features rely on.
3. **Do the real work in the main loop.** The ISR's job is to notice the event and record it. The loop reacts to it.

⚠️ **Gotcha.** `Serial.println()` inside an ISR is a classic trap - serial output is itself interrupt-driven, so printing from an ISR can deadlock or drop characters. Set a flag in the ISR and print from the loop.

## Sharing data: the `volatile` flag

So the ISR records the event and the loop reacts. They share a variable - and this is exactly where `volatile` from phase 2 earns its keep.

The compiler optimizes aggressively. Looking at `while (1) { if (button_pressed) ... }`, it sees nothing in the loop that changes `button_pressed`, so it may "helpfully" read the variable once, cache it in a CPU register, and never look at memory again. It cannot see that an ISR writes the variable behind its back. Result: the ISR sets the flag, the loop never notices, your button does nothing.

`volatile` tells the compiler "this can change out from under you - reload it from memory every single time." Any variable shared between an ISR and the main code must be `volatile`.

💡 **Key point.** The pattern behind almost every interrupt you will ever write: **the ISR sets a `volatile` flag and returns; the main loop sees the flag and does the work.** Small ISR, no surprises.

## A worked example: an interrupt-driven button

External interrupt **INT0** is wired to pin **PD2** on the ATmega328P. We arm it to fire on a falling edge (the moment a pull-up-held pin drops to ground, which is a button press), and each press toggles the LED.

```c
#include <avr/io.h>
#include <avr/interrupt.h>

volatile uint8_t button_pressed = 0;   // shared between the ISR and loop()

ISR(INT0_vect) {
    button_pressed = 1;                 // do the minimum: set a flag, return
}

void setup() {
    Serial.begin(9600);
    DDRB  |= (1 << PB5);                // on-board LED (pin 13) is an output
    DDRD  &= ~(1 << PD2);              // PD2 (INT0) is an input
    PORTD |=  (1 << PD2);              // enable its internal pull-up

    EICRA |= (1 << ISC01);             // INT0 triggers on a FALLING edge
    EIMSK |= (1 << INT0);              // unmask (enable) the INT0 interrupt
    sei();                             // turn on interrupts globally
}

void loop() {
    if (button_pressed) {
        button_pressed = 0;            // clear the flag first
        PORTB ^= (1 << PB5);           // toggle the LED
        Serial.println("press");
    }
    // the loop is free to do other work here, and never misses a press
}
```
```console
press
press
press
```

*What just happened:* each press pulls PD2 low, the hardware fires `INT0_vect`, and the ISR does one thing - sets `button_pressed`. The loop notices the flag, clears it, toggles the LED, and prints. The heavy work (the print) stays out of the ISR, and because the interrupt catches the press directly, the loop could be busy with anything else and still not miss it.

Two register details worth naming, both from the datasheet: `EICRA` (External Interrupt Control Register A) picks the trigger, where `ISC01 = 1` with `ISC00 = 0` means falling edge; `EIMSK` (External Interrupt Mask Register) has one enable bit per external interrupt, so `1 << INT0` switches INT0 on. Finally, `sei()` sets the global interrupt-enable bit - until you call it, no interrupt fires at all. Its opposite is `cli()`.

▶ **Try it:** start a new Arduino Uno project at [wokwi.com](https://wokwi.com), wire a pushbutton from pin 2 to GND, paste this in, and Run. Open the serial monitor and click the button - each click toggles the LED and logs a press.

## Why buttons bounce, and what to do

Run that example and you may see one click log two or three presses. Your code is fine - the button is not. A pushbutton is two metal contacts snapping together, and for a few milliseconds after they touch they physically chatter, bouncing open and closed several times before settling. Each bounce is a real falling edge, so your ISR fires several times for one human press.

The fix is **debouncing**: after the first edge, ignore further edges for a short window - roughly 20 to 50 ms, longer than the bounce but shorter than a human can press twice. The lazy, reliable version records the time of the last accepted press and rejects any new one that arrives too soon after it:

```c
if (button_pressed) {
    button_pressed = 0;
    unsigned long now = millis();
    if (now - last_press > 50) {       // 50 ms since the last accepted press
        last_press = now;
        PORTB ^= (1 << PB5);           // one toggle per real press
    }
}
```

That is enough to collapse the chatter back into one press. (Hardware debouncing with a small capacitor exists too, but a time window in software costs nothing and is usually all you need.)

## Timers: keeping time without `delay()`

`_delay_ms(500)` from phase 4 is a **busy-wait**: the CPU sits in a counting loop doing nothing for the whole half second. During that delay it cannot read a sensor, answer an interrupt promptly, or do anything else. For a first blink that is fine; for a real program it is dead weight.

A **hardware timer** is a counter built into the chip that ticks on its own, driven by the system clock, completely independent of your code. You configure it once, and it counts in the background while your CPU does other things. When it reaches a target it can fire an interrupt - so you get a steady heartbeat for free.

Here is Timer1 (the 16-bit timer) set to fire once per second and toggle the LED, with no `delay()` anywhere:

```c
#include <avr/io.h>
#include <avr/interrupt.h>

volatile uint8_t tick = 0;

ISR(TIMER1_COMPA_vect) {
    tick = 1;                          // heartbeat: set a flag, return
}

void setup() {
    DDRB   |= (1 << PB5);              // LED output
    TCCR1A  = 0;                       // normal operation, no output pins driven
    TCCR1B  = (1 << WGM12)             // CTC mode: count up to OCR1A, then reset
            | (1 << CS12) | (1 << CS10); // prescaler /1024
    OCR1A   = 15624;                   // 16MHz / 1024 = 15625 ticks per second;
                                       // 0..15624 is 15625 counts = exactly 1 s
    TIMSK1 |= (1 << OCIE1A);           // fire an interrupt on compare-match A
    sei();
}

void loop() {
    if (tick) {
        tick = 0;
        PORTB ^= (1 << PB5);           // toggle once per second
    }
    // loop stays responsive the whole time; the timer runs itself
}
```

The clock ticks at 16 MHz. The `/1024` prescaler slows the timer's counting to 15625 ticks per second, and counting 0 up to `OCR1A` (15624) and resetting takes exactly one second, at which point `TIMER1_COMPA_vect` fires. Same pattern as before: the ISR sets a flag, the loop acts. The difference from `delay()` is that the loop is never blocked - it is free every moment between ticks.

🪖 **War story.** A common first embedded project is a data logger that reads a sensor and blinks a status LED. Written with `delay()` for the blink, it drops readings every time the LED is mid-blink, and the bug hides because it only loses data half the time. Rebuilt with a timer interrupt for the blink and the main loop free for the sensor, the dropped readings vanish. Once you have felt that, you stop reaching for `delay()` in anything that has to do two things at once.

## Recap

1. **Polling** burns the CPU checking, and can miss events during a delay. **Interrupts** let the hardware call you the instant something happens, freeing the CPU in between.
2. An **ISR** must be short and never block. No `_delay_ms()`, no `Serial` printing, no long loops inside it.
3. Share data between an ISR and the loop through a **`volatile`** variable, or the compiler may cache it and never see the ISR's update.
4. The core pattern: **ISR sets a `volatile` flag and returns; the main loop reacts.**
5. On AVR, arm external interrupt INT0 (pin PD2) with `EICRA` for the edge, `EIMSK` to enable it, `sei()` to turn interrupts on, and handle it in `ISR(INT0_vect)`.
6. Mechanical buttons **bounce**: one press makes several edges. Debounce by ignoring further edges for ~20 to 50 ms.
7. A **hardware timer** counts in the background and can fire an interrupt on a schedule - a steady heartbeat that replaces blocking `delay()` and keeps the loop responsive.

You can now light pins, read pins, and react to the world the moment it changes without wasting a cycle. That is the core loop of embedded programming - everything else is more peripherals and the same three registers per feature, looked up in the datasheet.

### Check yourself

```quiz
[
  {
    "q": "Why must the shared flag in `ISR(INT0_vect) { button_pressed = 1; }` be declared `volatile`?",
    "choices": [
      "volatile makes the variable faster to access",
      "Without volatile the compiler may cache the flag in a register and never see the ISR's update",
      "volatile is required on every global variable in C",
      "It stops the ISR from firing more than once"
    ],
    "answer": 1,
    "explain": "The compiler cannot see the ISR change the flag, so it may read the value once and reuse it. volatile forces every access to hit memory, so the loop sees the ISR's write."
  },
  {
    "q": "Which line does NOT belong inside an interrupt service routine?",
    "choices": [
      "flag = 1;",
      "count++;",
      "_delay_ms(500);",
      "reading a register into a variable"
    ],
    "answer": 2,
    "explain": "An ISR should do the minimum and return fast. A 500 ms busy-wait freezes the whole program while it runs and can make other interrupts miss their events. Do slow work in the main loop."
  },
  {
    "q": "A mechanical button is pressed once but the press-counter jumps by three. What is the most likely cause?",
    "choices": [
      "The interrupt is disabled",
      "Contact bounce - the metal contacts chatter for a few milliseconds, firing several edges per press",
      "volatile was left off the counter",
      "The pull-up resistor is too strong"
    ],
    "answer": 1,
    "explain": "Mechanical contacts physically bounce when they close, producing several fast edges for a single press. Debouncing - ignoring further edges for roughly 20 to 50 ms - collapses them back into one press."
  }
]
```


---

# A Peripheral Tour: PWM, Serial, and Sensors

You've made a pin go HIGH and LOW. That on/off control is the foundation, but the real world isn't only on and off - it's brightness, sound, temperature, position. This phase tours the three peripherals that bridge a digital chip to that analog world: PWM to fake an analog *output*, UART to talk back to your PC, and the ADC to read an analog *input*. One clear ATmega328P example each, not an encyclopedia.

## PWM: faking analog with a fast square wave

**What it actually is.** A digital pin has two voltages, 0 V and 5 V, nothing between. PWM (pulse-width modulation) fakes the in-between by switching between them very fast and varying how much of each cycle it spends HIGH. That fraction is the *duty cycle*: 50% duty (HIGH half the time) averages to 2.5 V, 25% duty averages to about 1.25 V. The pin never truly outputs 1.25 V - it flips between 0 and 5 so quickly that whatever it drives (an LED, a motor) responds to the average.

**Real behavior.** On the Uno, `analogWrite(pin, value)` does this for you. `value` runs 0 to 255 (8-bit), so `analogWrite(pin, 128)` is roughly 50% duty. Under the hood, one of the ATmega328P's hardware timers counts continuously and flips the pin at a compare point with no CPU involvement once it's set - the timer runs the waveform in the background while your code moves on. Only the pins marked with a tilde (`~`) on the board can do it: 3, 5, 6, 9, 10, and 11.

**Real example** - the classic LED fade on pin 9:

```c
int led = 9;
int brightness = 0;
int step = 5;

void setup() {
  pinMode(led, OUTPUT);
}

void loop() {
  analogWrite(led, brightness);   // 0 = off, 255 = full
  brightness += step;
  if (brightness <= 0 || brightness >= 255) {
    step = -step;                 // reverse direction at each end
  }
  delay(30);
}
```

The LED ramps up, then back down, forever. You wrote no timing loop to hold each brightness level - the timer hardware keeps the duty cycle steady between `analogWrite` calls.

▶ **Try it:** start a new Arduino Uno project at [wokwi.com](https://wokwi.com), wire an LED (through a resistor) from pin 9 to ground, paste this in, and Run. Watch it breathe.

⚠️ **Gotcha.** `analogWrite` is not a real analog voltage - it's a square wave averaged out. That's perfect for LEDs and motors, which don't care, but you cannot use it to feed something that needs a clean, steady voltage (an analog audio line, a sensor's reference) without an external filter. It's also a different scale from analog *input*: `analogWrite` takes 0-255, while `analogRead` (below) gives 0-1023. Two peripherals, two ranges - mixing them up is a rite of passage. One more: a hobby servo is *not* driven by `analogWrite`; it wants a 50 Hz pulse of a specific width, so you use the `Servo` library, which builds that signal for you.

**Why this saves you later.** Duty cycle is the whole idea behind LED dimming, motor speed control, and class-D audio. Once you see analog output as "a fast switch plus an average," every one of those stops being mysterious.

## UART (serial): your firmware's printf

**What it actually is.** UART (universal asynchronous receiver/transmitter) sends bytes one bit at a time down a wire, with both ends agreeing in advance on the speed - the *baud rate*. On the Uno the chip's UART is bridged through the onboard USB chip to your PC, so anything the firmware "prints" appears in the Arduino Serial Monitor. This is the single most important debugging tool you have on a microcontroller: it's how you *see* what the firmware is doing when you can't attach a debugger.

**Real behavior.** `Serial.begin(9600)` starts the UART at 9600 baud. `Serial.print` and `Serial.println` send text. The Serial Monitor on your PC must be set to the *same* baud, or you get garbage.

**Real example:**

```c
int count = 0;

void setup() {
  Serial.begin(9600);
  Serial.println("Booting up");
}

void loop() {
  Serial.print("count = ");
  Serial.println(count);
  count++;
  delay(1000);
}
```

Open the Serial Monitor and you see:

```console
Booting up
count = 0
count = 1
count = 2
count = 3
```

*What just happened:* the firmware sent each line over the UART, one character at a time at 9600 baud, and the USB bridge relayed it to your PC. That `count` value is coming from a chip with no screen - the serial line is its only voice.

⚠️ **Gotcha.** If the Serial Monitor's baud does not match `Serial.begin`, you see gibberish like `x??B?@?` instead of text. The bytes arrived fine; the two ends only disagreed on speed. Also: the UART is physically pins 0 (RX) and 1 (TX) on the Uno, so if you use `Serial`, don't also wire those pins for something else - you'll get corrupted output or a failed upload.

**Why this saves you later.** Most embedded debugging is a well-placed print. When a sensor reads wrong or a state machine sticks, a `Serial.println` at the right spot tells you what the chip actually saw - the embedded version of the print debugging you already know, and often the only window you get.

## ADC: reading the analog world

**What it actually is.** The analog-to-digital converter is PWM's mirror image: it takes a real voltage on a pin and turns it into a number your code can read. The ATmega328P's ADC is 10-bit, so it maps the 0 V to 5 V range onto the integers 0 to 1023. A potentiometer at its midpoint sits near 2.5 V, which reads about 512.

**Real behavior.** `analogRead(A0)` samples pin A0 and returns that 0-1023 number. The Uno exposes six analog inputs, A0 through A5. To turn the raw number back into volts, scale by the reference voltage: `raw * (5.0 / 1023.0)`.

**Real example** - read a potentiometer and report it over serial:

```c
void setup() {
  Serial.begin(9600);
}

void loop() {
  int raw = analogRead(A0);              // 0..1023
  float volts = raw * (5.0 / 1023.0);
  Serial.print("raw = ");
  Serial.print(raw);
  Serial.print("  volts = ");
  Serial.println(volts);
  delay(500);
}
```

Turn the knob and the numbers track it:

```console
raw = 0     volts = 0.00
raw = 512   volts = 2.50
raw = 1023  volts = 5.00
```

*What just happened:* the ADC sampled the wiper voltage and quantized it to one of 1024 steps; your code scaled that back to a readable voltage. This is how every analog sensor - a temperature probe, a light sensor, a joystick - gets into your program.

▶ **Try it:** start a new Arduino Uno project at [wokwi.com](https://wokwi.com), add a potentiometer, wire its outer pins to 5V and GND and its middle pin to A0, paste this in, and Run. Drag the knob and watch the readings move.

```mermaid
flowchart LR
  POT[Potentiometer<br/>0 to 5 volts] --> ADC[ADC<br/>sample and quantize]
  ADC --> NUM[Number 0 to 1023]
  NUM --> CODE[Your code]
```

⚠️ **Gotcha.** An analog pin with nothing connected does not read 0 - it *floats*, picking up ambient noise and giving you drifting, random-looking values. If a reading makes no sense, check the wiring before you blame the code. And keep the scales straight: input is 0-1023, output (`analogWrite`) is 0-255.

**Why this saves you later.** Every sensor that reports a level rather than a yes/no - temperature, light, pressure, a slider - reaches your code through an ADC. Understanding "voltage in, integer out, scaled by a reference" is the key that unlocks all of them.

## Recap

1. **PWM** fakes an analog output by switching a pin between 0 and 5 V fast; the *duty cycle* (fraction of time HIGH) sets the average. `analogWrite(pin, 0..255)` on pins 3, 5, 6, 9, 10, 11.
2. **UART / serial** sends bytes to your PC at an agreed baud rate; `Serial.begin` plus `Serial.print` is your firmware's `printf` and primary debug window.
3. Both ends of a serial link must agree on baud, or the text arrives as garbage.
4. **ADC** turns a pin voltage into a number - 10-bit means 0-1023 for 0-5 V on the ATmega328P; scale with `raw * (5.0 / 1023.0)`.
5. Watch the scales: analog **in** is 0-1023, PWM **out** is 0-255.

Check the three ideas most likely to bite you:

```quiz
[
  {
    "q": "On the Uno, `analogWrite(9, 64)` makes pin 9 do what?",
    "choices": [
      "Output a steady 1.25 V from a built-in analog converter",
      "Switch between 0 V and 5 V fast, HIGH about 25% of each cycle, averaging near 1.25 V",
      "Read the analog voltage on pin 9 and return 64",
      "Go fully HIGH, because any non-zero value means on"
    ],
    "answer": 1,
    "explain": "analogWrite sets a PWM duty cycle from 0 to 255. 64 out of 255 is about 25%, so the pin is HIGH a quarter of each cycle and averages near 1.25 V. There is no real digital-to-analog converter here - it's a fast square wave, and whatever it drives responds to the average."
  },
  {
    "q": "Your Serial Monitor shows `?H?@?x` instead of readable text. Most likely cause?",
    "choices": [
      "The USB cable is charge-only and cannot carry data",
      "The Monitor's baud rate does not match the value you passed to Serial.begin()",
      "You forgot to call analogRead() first",
      "The ATmega328P has run out of flash"
    ],
    "answer": 1,
    "explain": "Garbled characters are the classic symptom of a baud mismatch. The bytes arrive fine, but the two ends disagree on speed. Set the Monitor to the same baud you passed to Serial.begin (9600 here)."
  },
  {
    "q": "`analogRead(A0)` on the ATmega328P returns a value in what range, and why?",
    "choices": [
      "0 to 255, because the ADC is 8-bit",
      "0 to 1023, because the ADC is 10-bit (2^10 = 1024 steps)",
      "0 to 5, matching the input voltage in volts",
      "0.0 to 5.0 as a float, straight from the pin"
    ],
    "answer": 1,
    "explain": "The AVR ADC is 10-bit, so it maps the input range onto 1024 integer steps, 0 through 1023. To get volts back you scale it yourself: raw * (5.0 / 1023.0)."
  }
]
```


---

# The Toolchain and the Real Workflow

Writing the firmware is half the job. The other half is the machinery that carries your code from a text file on your laptop onto a chip with 2 KB of RAM and no operating system - and how you find out what went wrong once it's running there. This phase is the day-to-day loop of real embedded work: cross-compile, flash, respect the memory budget, and debug a system that can't always stop to talk to you.

## Cross-compilation: your PC builds code it can't run

**What it actually is.** Your laptop runs an x86 (or ARM) processor. The Uno runs an AVR processor - a completely different instruction set. When you build an Arduino sketch, the compiler (`avr-gcc`) runs on your PC but produces machine code for the *AVR*, not for your laptop. That's cross-compilation: the machine doing the compiling (the *host*) differs from the machine that will run the result (the *target*).

**Real behavior.** The `.hex` file that comes out is AVR machine code. Try to run it on your PC and nothing happens - your CPU cannot execute those instructions. It only means something to the ATmega328P. The Arduino IDE bundles `avr-gcc` and its toolchain, so the target is chosen for you when you pick the board; on a bare command line you'd pass `-mmcu=atmega328p` to say which chip you're aiming at.

📝 **Terminology.** *Host* = the machine you compile on. *Target* = the machine that runs the binary. On desktop they're the same, so you never think about it. In embedded they almost never are, which is why "it compiled" and "it runs on the chip" are two separate victories.

## Flashing: getting the binary onto the chip

**What it actually is.** Compiling produces a file on your PC. *Flashing* (or uploading) copies that file into the microcontroller's flash memory, where it runs. On the Uno this goes through a small program already living on the chip called the *bootloader*: when you hit Upload, the board resets, the bootloader listens on the serial line, and a PC-side tool called `avrdude` streams the new program in while the bootloader writes it to flash.

**Real behavior.** The Arduino IDE's Upload button is really two steps - compile, then flash - behind one click. A successful upload looks like this:

```console
Sketch uses 1084 bytes (3%) of program storage space. Maximum is 32256 bytes.
Global variables use 200 bytes (9%) of dynamic memory, leaving 1848 bytes for
local variables. Maximum is 2048 bytes.

avrdude: writing flash (1084 bytes):
Writing | ################################################## | 100% 0.18s
avrdude: 1084 bytes of flash written
avrdude: verifying flash memory against the input file:
Reading | ################################################## | 100% 0.14s
avrdude: 1084 bytes of flash verified
avrdude done.  Thank you.
```

*What just happened:* the toolchain reported how much flash and RAM the program needs, then `avrdude` wrote the 1084-byte program into flash and read it back to verify. The instant verification finishes, the board resets and your code starts running.

The whole inner loop of embedded work is these steps on repeat:

```mermaid
flowchart LR
  EDIT[Edit code] --> COMPILE[Compile<br/>avr-gcc]
  COMPILE --> FLASH[Flash<br/>avrdude]
  FLASH --> RUN[Runs on chip]
  RUN --> OBSERVE[Observe<br/>serial or LED]
  OBSERVE --> EDIT
```

💡 **Key point.** There's no button that runs your firmware on your laptop the way a Python script runs. The chip is the only place it can execute, so every test cycle goes all the way around: edit, compile, flash, watch the hardware.

## Memory budgets: 2 KB is the whole world

**What it actually is.** The ATmega328P has three separate memories: 32 KB of *flash* (holds the program, survives power-off), 2 KB of *SRAM* (holds your variables while running, wiped on reset), and 1 KB of *EEPROM* (small persistent storage). The size report above tells you how full the first two are. Flash is usually roomy; SRAM is the one that hurts.

**Real behavior.** That 2 KB of SRAM holds everything live at once: global variables, the stack (function calls and their locals), and the heap (anything you `malloc`). The stack grows down from the top, the heap grows up from the bottom, and if they meet in the middle they overwrite each other. There's no memory-protection unit and no crash with a tidy error message - the program starts behaving insanely instead: garbled serial output, a variable that changes on its own, spontaneous resets.

⚠️ **Gotcha.** The sneakiest RAM eater is text. Every `Serial.println("some message")` copies that string into SRAM by default. A dozen debug messages can quietly swallow hundreds of your 2048 bytes. Wrap literals in the `F()` macro - `Serial.println(F("some message"))` - to keep them in flash instead, and watch the "dynamic memory" number in the size report drop. When the IDE warns `Low memory available, stability problems may occur`, believe it.

**Why this saves you later.** On desktop you never think about 200 bytes. Here, reading the size report after every build - and knowing that SRAM is where you run out, not flash - is the difference between a stable device and one that resets every few minutes for reasons that look like black magic.

## Debugging when you can't pause the chip

**What it actually is.** On desktop, your debugger and your program run side by side under the same OS, so setting a breakpoint is trivial. On a microcontroller your code runs alone on a separate chip, with no OS and no debugger process beside it. To pause real hardware you need a physical *debug interface* wired in - and a stock Uno doesn't expose one conveniently. So embedded debugging leans on a different toolkit:

- **UART logs.** The `Serial.println` from [the peripheral tour](06-a-peripheral-tour.md) is the workhorse: print what the chip saw and where it got to.
- **Blink codes.** When you have no serial (or serial is the thing that's broken), blink the onboard LED on pin 13 in a pattern - two blinks for "reached setup," a fast flutter for "error." The lowest-tech status report there is, and it always works.
- **A logic analyzer.** A cheap tool that records what several digital pins did over time, down to the microsecond. It's how you confirm whether an I2C or SPI conversation actually happened the way you think - the pins can't lie.
- **GDB over a debug probe.** You *can* get true breakpoints, but you need a hardware debugger physically connected (an SWD probe on ARM chips, debugWIRE on AVR). It's standard on STM32 and the Pico, awkward on a bare Uno - which is why simulation (Wokwi) and serial prints do most of the day-to-day work.

🪖 **War story.** A device would reset at random, hours apart, with nothing in the logs. No breakpoint could catch something that rare. The fix was a blink code fired on a watchdog reset plus a counter kept in EEPROM, so every reset left a fingerprint behind. The lesson: when you can't pause the bug, make it record itself. Instrumentation beats a breakpoint you'll never be watching at the right moment.

## Recap

1. **Cross-compilation:** `avr-gcc` runs on your PC (the host) but builds machine code for the AVR (the target). The `.hex` output won't run on your laptop.
2. **Flashing** copies that binary into the chip's flash, usually via the bootloader and `avrdude`. The Upload button is compile-then-flash; the loop is edit, compile, flash, observe.
3. **SRAM is the budget that bites.** 2 KB holds globals, stack, and heap at once; a stack/heap collision causes corruption and random resets, not a clean error. Read the size report, and use `F()` to keep strings in flash.
4. **You can't breakpoint a bare chip easily.** Lean on serial logs, blink codes, a logic analyzer, and - with a hardware probe - GDB.

The details that trip people up first:

```quiz
[
  {
    "q": "Why is building for the Uno called *cross*-compilation?",
    "choices": [
      "Because the code has to cross from C into assembly",
      "Because avr-gcc runs on your PC but produces machine code for a different processor, the AVR, which your PC can't run",
      "Because the compiler checks your code across multiple files at once",
      "Because you compile it twice, once for debug and once for release"
    ],
    "answer": 1,
    "explain": "Cross-compilation means host and target differ: the compiler runs on your x86/ARM laptop but emits AVR machine code. The resulting .hex only runs on the microcontroller, not on the machine that built it."
  },
  {
    "q": "Your sketch compiles and flashes fine, but the board sends garbled serial and resets at random. The size report shows global variables at 96% of SRAM. Most likely cause?",
    "choices": [
      "The flash memory is corrupted and needs replacing",
      "The downward stack and the heap/globals are colliding in the tiny 2 KB SRAM, overwriting each other",
      "avrdude flashed the wrong .hex file",
      "The baud rate is too high for the chip"
    ],
    "answer": 1,
    "explain": "With SRAM nearly full, the downward-growing stack runs into the heap and globals below it, silently corrupting memory. There's no memory-protection unit to catch it, so the symptom is chaos - garbage output and spontaneous resets - not a clean error. Free up RAM, for example by moving strings to flash with F()."
  },
  {
    "q": "Why can't you set a breakpoint on a stock Arduino Uno as easily as in a desktop IDE?",
    "choices": [
      "AVR chips don't support the C language fully",
      "Breakpoints only work in interpreted languages",
      "The code runs alone on a separate chip with no OS or debugger beside it, so pausing real hardware needs a physical debug interface the Uno doesn't expose conveniently",
      "The Uno runs too fast for any debugger to keep up"
    ],
    "answer": 2,
    "explain": "On desktop the debugger shares the OS with your process. On a microcontroller your firmware runs solo on the chip, so you need a hardware debug probe (SWD or debugWIRE) wired in to pause it. Lacking that, embedded debugging leans on serial logs, blink codes, and a logic analyzer."
  }
]
```


---

# Where to Go Next

You learned this on raw registers - setting bits in `DDRB` and `PORTB` by hand, reading the ADC and timer registers directly - on purpose. It's slower to write, but now nothing about a microcontroller is magic to you. This closing phase points at the roads out: the libraries that speed you up once you understand what they hide, the idea of an operating system for tiny chips, and the boards worth graduating to.

## Bare registers vs a vendor HAL

**What it actually is.** Everything you did talked to the hardware by writing directly to special memory addresses - the registers. A *HAL* (hardware abstraction layer) wraps those registers in friendly functions: instead of flipping bits in `DDRB` and `PORTB`, you call `digitalWrite(13, HIGH)`. On ARM chips there's also *CMSIS*, a standard naming scheme for a Cortex-M core's registers that vendor HALs (like STM32's) build on top of.

**The trade-off.** Registers give you full control and full understanding, at the cost of being verbose and tied to one specific chip. A HAL is faster to write and portable across a vendor's whole line, at the cost of hiding what's happening and adding overhead - Arduino's `digitalWrite` is many times slower than flipping the port bit directly, because it does a pin-to-port lookup and safety checks on every call.

💡 **Key point.** You learned registers first so the HAL is a convenience, not a crutch. When `digitalWrite` is too slow in a tight loop, or the HAL doesn't expose some peripheral mode you need, you can drop down to the registers - and you'll actually understand what you're reading in the datasheet. Most people who start at the HAL never can.

## A one-page look at an RTOS

**The problem it solves.** So far your program is one big `loop()` - a *super-loop* that does everything in sequence. That works until you need several things happening "at once": blink an LED every second, read a sensor every 100 ms, and respond to a button the instant it's pressed. Cram all that into one loop and the timing tangles into a knot of counters and flags.

**What an RTOS gives you.** A real-time operating system lets you write each job as its own *task* - an independent function with its own stack that acts as though it had the whole chip to itself. A *scheduler* switches between tasks rapidly, so on a single core they appear to run in parallel. "Real-time" means the scheduling is predictable: a high-priority task can interrupt a lower one to hit a deadline.

**FreeRTOS** is the name you'll meet first - small, open source, and running on everything from AVR to ARM to the ESP32 (whose official SDK is built on it). Treat this as a teaser, not a tutorial: the thing to carry forward is *why* you'd want one - many jobs at once, without a single tangled super-loop.

⚠️ **Gotcha.** An RTOS isn't free: every task needs its own stack, so tasks cost RAM. On a 2 KB AVR that's painfully tight, which is why an RTOS earns its keep more on roomier chips (ESP32, STM32) than on an Uno.

## Real boards to graduate to

The Uno taught you the fundamentals; here's where people go next, and when to pick each:

- **STM32** (ARM Cortex-M) - the industry workhorse. Far more speed, memory, and peripherals than an AVR at a similar price. Reach for it when a project outgrows the Uno and you want what professionals actually ship.
- **ESP32** - built-in WiFi and Bluetooth, dual core, plenty of RAM. Reach for it the moment your project needs to talk to a network or a phone.
- **Raspberry Pi Pico (RP2040)** - beginner-friendly and cheap, with an excellent, well-documented C/C++ SDK. Reach for it when you want a gentle step past Arduino into a modern SDK while staying in C.

And keep prototyping in **Wokwi** - it simulates all of these (Uno, ESP32, Pico, STM32), so you can try a board before buying one.

## Read the datasheet

One habit separates people who guess from people who know: reading the datasheet. Every register, every timer mode, every electrical limit you touched lives in the ATmega328P datasheet, stated exactly. It's dense, and you don't read it front to back - you look things up in it. Getting comfortable doing that is the real graduation from tutorials to engineering.

## Additional resources

- [ATmega328P datasheet (Microchip)](https://www.microchip.com/en-us/product/atmega328p) - the ground truth for every register and peripheral in this guide; learn to look things up here.
- [FreeRTOS documentation](https://www.freertos.org/) - the standard on-ramp to tasks and scheduling, with a free book-length kernel guide.
- [Raspberry Pi Pico C/C++ SDK](https://www.raspberrypi.com/documentation/microcontrollers/c_sdk.html) - a clean, modern SDK and a friendly next board after the Uno.
- [Wokwi](https://wokwi.com) - keep building and simulating AVR, ESP32, Pico, and STM32 projects in the browser, no hardware required.
- *Making Embedded Systems* by Elecia White - the classic readable book on designing real firmware, not merely blinking an LED.

## Recap

1. **Registers vs HAL:** direct register writes give full control and understanding but are verbose and chip-specific; a HAL (or ARM's CMSIS) is portable and quick to write but hides detail and adds overhead. You learned registers first so the HAL is a convenience, not magic.
2. **An RTOS** lets you split work into independent *tasks* a scheduler runs "at once," instead of one tangled super-loop. FreeRTOS is the common one; tasks cost RAM, so it fits roomier chips best.
3. **Graduate boards:** STM32 for power and industry use, ESP32 for WiFi and Bluetooth, Raspberry Pi Pico for a friendly modern C/C++ SDK.
4. **Read the datasheet** - it's the authority, and looking things up in it is the real skill.

One last check before you go build something:

```quiz
[
  {
    "q": "What's the core trade-off between writing directly to registers and using a vendor HAL?",
    "choices": [
      "Registers are portable across chips; a HAL only works on one chip",
      "Registers give full control and understanding but are verbose and chip-specific; a HAL is portable and quicker to write but hides detail and adds overhead",
      "A HAL runs faster than direct register access in every case",
      "There is no real difference; a HAL compiles to exactly the code you'd write by hand"
    ],
    "answer": 1,
    "explain": "Direct register access is maximum control and zero abstraction, at the price of verbosity and code tied to one chip. A HAL trades some speed and transparency for portability and convenience - Arduino's digitalWrite, for instance, is much slower than flipping the port bit yourself."
  },
  {
    "q": "In an RTOS, what is a 'task,' and why would you use one?",
    "choices": [
      "A single interrupt that fires once at startup",
      "An independent unit of work with its own stack that a scheduler switches between, letting several jobs run 'at once' without a tangled super-loop",
      "A compiler pass that optimizes your main loop",
      "A setting that overclocks the chip for more speed"
    ],
    "answer": 1,
    "explain": "A task is an independent thread of execution with its own stack; the scheduler rapidly switches between tasks so they appear concurrent on one core. You reach for an RTOS when a single super-loop can no longer juggle several timed jobs cleanly. The cost is RAM - each task needs its own stack."
  },
  {
    "q": "Your next project needs to send sensor data to a web server over WiFi. Which board is the natural pick?",
    "choices": [
      "A second Arduino Uno, since it's what you know",
      "An ESP32, because WiFi and Bluetooth are built in",
      "An STM32, because it has the most raw compute",
      "A base Raspberry Pi Pico, because it's the cheapest"
    ],
    "answer": 1,
    "explain": "The ESP32 integrates WiFi and Bluetooth, which is exactly why people pick it for connected projects. STM32 is the industry power option and the Pico is a friendly modern SDK, but neither includes wireless the way the ESP32 does - the base Pico/RP2040 has none."
  }
]
```
