Published on July 2, 2026

It Boots! #2 - the light, the datasheet, and me

Last time the win condition was simple. The chip boots, it doesn't reboot loop. I won, okay-ish.

Now it's time for the light.

I want the led on the board to turn on. That's it. No arduino. No digitalWrite(). Just me, the chip, and the datasheet.

Here's the thing. The esp32-s3 doesn't have a "pin 2 is on" instruction. It has memory addresses. Write the right number to the right address, and the hardware reacts. Nothing in between.

how memory-mapped registers actually work

The gpio peripheral lives at 0x60004000. Think of that as a street, and each register as a house on it, a specific address that the gpio hardware is wired to watch. Write to one of these addresses and you're not storing data for later, you're triggering an action right then.

  • 0x60004004 is the direction register. One bit per pin. 0 means input, 1 means output.
  • 0x60004008 is the set register. Write a 1 to a bit here, that pin goes high.
  • 0x6000400C is the clear register. Write a 1 to a bit here, that pin goes low.

So the actual blink is close to nothing:

*(volatile uint32_t*)0x60004008 = (1 << 2);  // set pin 2
*(volatile uint32_t*)0x6000400C = (1 << 2);  // clear pin 2

That's a light turning on and off, forever, in theory.

why nothing happened the first time

I flashed it and stared at a dark board for a while. (this is apparently the theme of the whole project, flash, stare, confused, find the one thing I skipped)

I forgot the direction register. Every pin defaults to input. Try to drive an output signal onto a pin that's still set as input, and the hardware just ignores you, there's no output driver connected for it to reach.

So first this:

*(volatile uint32_t*)0x60004004 |= (1 << 2);  // this pin is output now

Then set, clear, loop. Light.

why set/clear registers exist instead of a normal write

If gpio only gave you one plain register to hold all pin states, turning on pin 2 without touching the others means read the whole register, flip one bit in your local copy, write the whole thing back. A read-modify-write. Fine on a single core doing one thing. Not fine if an interrupt or the other core touches that same register in the gap between your read and your write, because now one of the two writes gets silently lost.

The set and clear registers dodge this entirely. Writing a 1 to bit 2 in the set register only affects bit 2. Every other bit you write as 0 is left alone by the hardware, not cleared, just ignored.

0x60004008, writing (1 << 2):
bit:   ...  4  3  2  1  0
value: ...  0  0  1  0  0

Only pin 2 moves. No read first, no chance for something else to sneak in between. That's the whole reason it's called w1ts, write-1-to-set.

running it in qemu

There's no real led in qemu, so that version just prints the register addresses over uart instead. I mostly wanted to confirm the logic was sound before trusting real hardware.

Qemu's -kernel mode skips more than I expected though. On real hardware, the rom bootloader sets up the stack pointer before it ever jumps to your code. Qemu doesn't. So my program ran into a stack that didn't exist and immediately broke. Had to build the stack by hand in assembly, load an address, shift it, or in the flags, point sp at it myself.

(a stack pointer isn't something I ever think about on a normal computer. someone else always did it first)

Next stop is real uart output on the hardware itself, so the chip can talk back instead of me reasoning about register addresses in the dark. After that, still far off, still a five dollar chip: doom.

subscribe to my substack

Get monthly summaries, books read, essays, and link digests delivered to your inbox.

Get updates viasubstackorsubscribe to RSS