First Light: Blinking an LED on the ESP32-S3
Kicking off the electronics build-logs — flashing the ESP32-S3, blinking an LED, and, at the end, where this section actually went afterwards.
This build-log started on dankdev.com. That address, dankdev.com/electronics/hello-esp32, now carries a short notice that sends you straight back to this page — the full write-up is the one you are reading.
This is the start of the Electronics section — build-logs from my journey into ESP32 and electrical engineering. Two more pages have gone up here since; they are linked, with what each one gives you, under What came next.
Why this section exists
I build web things for a living, but lately I've been pulled toward hardware — microcontrollers, breadboards, and the satisfying moment when code makes something in the physical world move. These posts are my notebook: what I built, the bill of materials, the wiring, the code, and the gotchas.
Bill of materials: the parts this build uses
The third column carries nothing new. Every line in it is a sentence from further down this page, quoted word for word and linked back to the section that argues it. It is there because “ESP32-S3 DevKitC” and “1” do not tell you which board the two-port behaviour below was read off, or which cable can stop an upload dead — and this page says both, a long way below the point where you would be ordering parts. Nothing in the column is a price, a seller or a part number this page had not already printed; where the log recorded nothing further about a part, the cell is empty, and the reason is under the table.
| Part | Qty | What exactly — in this page’s own words |
|---|---|---|
| ESP32-S3 DevKitC | 1 | Espressif's ESP32-S3-DevKitC-1 user guide describes two USB-C connectors on the board, silkscreened UART and USB; a cable without data lines; no revision, no flash or PSRAM size, no seller |
| 5 mm LED | 1 | Colour, forward voltage and rated current are not. |
| 220 Ω resistor | 1 | 220 Ω is recorded as a value only — no tolerance and no wattage. The 220 Ω resistor may sit on either side of the LED; it is a series circuit and it limits the same current in either position. |
| Breadboard + jumper wires | 1 |
Why one cell in that column is empty. The breadboard-and-jumper-wires row has
nothing in it, and that is the entry rather than a gap someone forgot to fill. This log recorded a
breadboard and jumper wires and never recorded which ones: no size, no tie-point count, no wire
length, no brand, no seller. What the page will say about a breadboard is how it catches you out
— the centre channel splits every row
— and a note about how a part fails you is not a note about which part to put in a basket.
Filling the cell would mean inventing a purchase. It is the same refusal as
the bench parts page, which ships no
bench-tray.stl because any size it published would be a guess about somebody
else’s bench.
The classic “hello world”: blink
Every embedded journey starts with blinking an LED. Here it is on an ESP32-S3 using the Arduino core:
#define LED_PIN 2
void setup() {
pinMode(LED_PIN, OUTPUT);
}
void loop() {
digitalWrite(LED_PIN, HIGH);
delay(500);
digitalWrite(LED_PIN, LOW);
delay(500);
}
Wiring the LED: GPIO2 through a 220 Ω resistor to ground
Wire the LED to GPIO2 through a 220 Ω resistor to ground, flash,
and you've got first light. The rest of this section is what “flash” actually
involves, and what to do when there is no first light.
GPIO2 through a 220 Ω resistor to ground, with the long leg (anode)
toward GPIO2 and the short leg (cathode) toward ground. The resistor is drawn on the GPIO2
side of the LED for one reason — it has to be drawn somewhere. It may sit on either
side of the LED; it is a series circuit and it limits the same current in either position.
The two ends are placed in schematic order, not in their positions on the board's headers:
there is deliberately no header pin, no silkscreen, no wire colour and no resistor band
pattern in this drawing, because this log does not record any of them.Flashing it, and what goes wrong the first time
Between “here is the code” and “the LED is blinking” sits a short chain of decisions, and most of them have one obvious wrong answer. What follows is those decisions, stated as decisions — not a transcript of a session. Everything here is either a consequence of the circuit and the sketch printed above, or is read off Espressif's published documentation for this board and chip, and is labelled as such where it is.
| What you see | Why it happens | What to do |
|---|---|---|
| The upload never handshakes and eventually times out, with a wiring diagram that was correct the whole time | The cable is in one connector and the port selected in the IDE belongs to the other. Either connector will power the board, so “it has power” tells you nothing about which port your computer is talking to. | If exactly one serial device appears when the board is plugged in and vanishes when you unplug it, that is the one to select; if two appear, you want the bridge. |
| The serial port disappears and re-enumerates on every reset | That is the USB connector, produced by the chip's own USB-Serial-JTAG peripheral, so it only exists while the chip is up and presenting it. | Use the UART connector: the bridge is a separate device from the ESP32-S3, so it enumerates whenever the board has power, which is what makes it the port that still works when the firmware is wedged. |
| GPIO19 and GPIO20 are not behaving as free pins | Espressif's ESP32-S3 datasheet assigns that peripheral GPIO19 and GPIO20. | Those two pins are not yours while you are using the USB connector. |
| A successful-looking upload and a dead board | A plain ESP32 board entry was chosen. The original ESP32 and the ESP32-S3 are different chips with different architectures, and a binary built for the wrong target will not run. | Install arduino-esp32 through the board manager and choose an ESP32-S3
board entry. Pick the target for the chip, then pick the port. |
| Serial output goes missing on this board | The per-board USB CDC On Boot option decides whether the core's
Serial object is routed to the chip's native USB port or out through the
UART bridge. |
Worth knowing about, but it cannot affect this sketch: the code above never calls
Serial at all. |
| The port is there and the upload still fails | The chip is not sitting in its serial bootloader — a cable without data lines, a bridge the host has not enumerated, or firmware holding the CPU busy. | Hold BOOT, tap RESET, release BOOT, and start the upload then. If the manual entry works and the automatic one never does, the problem is the cable or the reset circuit, not the sketch. |
| Nothing blinks at all | Upload failures are loud and they are the most likely cause. | Read the last few lines of the uploader output before you touch a single jumper wire. |
| The LED just sits there dark, and the uploader output was clean | LED polarity. Backwards, an LED does not blow, it simply does not conduct, which makes this the quietest failure on the list. | The code drives GPIO2 high, so current has to run GPIO2 → LED → ground: the long leg (anode) toward GPIO2, the short leg (cathode) toward ground. |
| It blinks, but out of phase with the code | Wrong rail: the LED runs from the 3V3 rail back into GPIO2 instead of from GPIO2 out to
ground, so it is lit when the code says LOW. |
Electrically fine, logically backwards. Run it from GPIO2 out to ground instead. |
| A dark LED behind a perfectly good sketch | Wrong hole. The sketch hard-codes pin 2, and a jumper one row over on the breadboard, or one header pin over on the board, misses it. | Count the row again: the jumper has to land on GPIO2 itself. |
| The board's own user LED does nothing | This sketch does not drive it. Nothing here writes to any pin except GPIO2. | Watch the LED you wired, not the on-board one — the on-board LED tells you nothing about whether the program is running. |
| An LED and a resistor that look like they are in series, and nothing lights | Breadboard continuity. The centre channel splits every row, and the power rails on many boards are split in the middle too. | They can be sitting in two unconnected halves of the same row; re-seat them into one connected half. |
| A dark LED, and no way to tell whether the program is running | This sketch gives you no way to tell “the program is not running” apart from “the program is running and the LED is wrong.” Both look identical. | Add Serial.begin(115200) to
setup() and a Serial.println() inside the loop, flash again, and
open a serial monitor. If the lines arrive, the fault is in the six wires; if nothing
arrives, stop looking at the breadboard. |
The two USB ports, and which one carries what
This is the failure that eats the most first evenings with this board family, and it has nothing to do with your code. Espressif's ESP32-S3-DevKitC-1 user guide describes two USB-C connectors on the board, silkscreened UART and USB, and they are not interchangeable:
- UART runs through the on-board USB-to-UART bridge chip. The bridge is a separate device from the ESP32-S3, so this port enumerates on your computer as a serial port whenever the board has power, whether or not the ESP32-S3 is running anything at all. That is what makes it the port that still works when the firmware is wedged.
- USB is wired to the ESP32-S3's own USB peripheral — the guide calls it USB-Serial-JTAG. The port is produced by the chip itself, so it only exists while the chip is up and presenting it, and it disappears and re-enumerates on every reset, which includes every reset the uploader performs. Espressif's ESP32-S3 datasheet assigns that peripheral GPIO19 and GPIO20, so those two pins are not yours while you are using this connector.
Either connector will power the board, so “it has power” tells you nothing about which port your computer is talking to. The classic version of this failure is a cable in one connector and a port selected in the IDE that belongs to the other: the upload never handshakes and eventually times out, with a wiring diagram that was correct the whole time. If exactly one serial device appears when the board is plugged in and vanishes when you unplug it, that is the one to select; if two appear, you are looking at both connectors and you want the bridge.
Board target and core
The sketch above is written against the Arduino core, which in practice means installing
Espressif's arduino-esp32 package through the Arduino IDE's board manager and then
choosing an ESP32-S3 board entry. Choosing a plain ESP32 entry is a real and
easy mistake: the original ESP32 and the ESP32-S3 are different chips with different
architectures, and a binary built for the wrong target will not run — you get a successful-looking
upload and a dead board. Pick the target for the chip, then pick the port.
The arduino-esp32 core also exposes a per-board USB CDC On Boot option,
which decides whether the core's Serial object is routed to the chip's native USB
port or out through the UART bridge. It is worth knowing about because it explains why serial
output can go missing on this board, but it cannot affect this sketch: the code above never
calls Serial at all.
If the port is there and the upload still fails
Uploading requires the chip to be sitting in its serial bootloader. The toolchain normally puts it there for you by toggling the bridge's control lines as the upload starts. When that does not happen — a cable without data lines, a bridge the host has not enumerated, firmware holding the CPU busy — the board carries the manual equivalent: the user guide documents BOOT and RESET buttons. Hold BOOT, tap RESET, release BOOT, and the chip comes up in download mode; start the upload then. If a manual download-mode entry works and the automatic one never does, the problem is the cable or the reset circuit, not the sketch.
What it looks like when it works
The upload finishes, the chip resets, and the sketch starts. The only thing this program
produces is the LED: delay(500) on each side of the loop, so an even on/off blink
at roughly one cycle per second. There is no serial banner and no boot message from the sketch,
because it never opens a serial port — anything you do see in a serial monitor came from the
bootloader or the core, not from these nine lines. Nothing else on the board is touched: the
sketch writes to GPIO2 and to no other pin.
One thing that makes GPIO2 a safe choice here rather than a lucky one:
Espressif's ESP32-S3 datasheet lists the chip's strapping pins as GPIO0, GPIO3, GPIO45 and
GPIO46, and GPIO2 is not among them, so an LED and a 220 Ω resistor hanging on it
cannot change how the board boots. That was not true of the original ESP32, which is part of
why blink examples for the two chips disagree about which pin to use — copying a pin number
from an ESP32 tutorial into an ESP32-S3 sketch is its own small trap.
When nothing blinks, in this order
Cheapest checks first. Each of these is a statement about the circuit and the code on this page, not a report about any particular board:
- Did the upload actually finish? Upload failures are loud and they are the most likely cause. Read the last few lines of the uploader output before you touch a single jumper wire.
- LED polarity. The code drives
GPIO2high to light the LED, so current has to run GPIO2 → LED → ground: the long leg (anode) toward GPIO2, the short leg (cathode) toward ground. Backwards, an LED does not blow, it simply does not conduct — it just sits there dark, which makes this the quietest failure on the list and the first wiring thing to check. The 220 Ω resistor may sit on either side of the LED; it is a series circuit and it limits the same current in either position. - Wrong rail. If the LED runs from the 3V3 rail back into
GPIO2instead of from GPIO2 out to ground, it will still blink — inverted, lit when the code saysLOW. Electrically fine, logically backwards. If the blink works but feels out of phase with the code, this is why. - Wrong hole. The sketch hard-codes pin 2. A jumper one row over on the breadboard, or one header pin over on the board, is a dark LED behind a perfectly good sketch.
- Wrong LED. If your board carries its own user LED, this sketch does not
drive it. Nothing here writes to any pin except
GPIO2, so watching the on-board LED tells you nothing about whether the program is running. - Breadboard continuity. The centre channel splits every row, and the power rails on many boards are split in the middle too. An LED and a resistor that look like they are in series can be sitting in two unconnected halves of the same row.
There is a limit to how far that list can take you, and it is worth naming: this sketch gives
you no way to tell “the program is not running” apart from “the program is
running and the LED is wrong.” Both look identical — a dark LED. The cheapest way out is to
add Serial.begin(115200) to setup() and a Serial.println()
inside the loop, flash again, and open a serial monitor. If the lines arrive, the board is fine
and the fault is in the six wires; if nothing arrives, stop looking at the breadboard. A blink
sketch is the standard first program because it needs no serial port — which is exactly the
reason it is a poor diagnostic when it fails.
What came next
Two pages have gone up on this site since this one, and neither of them is more firmware. Both are about the same awkward step: you have the board in front of you, you want to print something to hold it, and the dimension you need is not written down anywhere you can cite. Both also say up front that nothing on them has been printed — they are geometry checked by code, not parts anyone has held.
In Measuring a dev board for 3D printing when the size is not published you get the method: how to work out a dimension the vendor never wrote down, why that page argues for a stepped gauge over a confident guess, how leaving the uncertain axis open-ended makes being wrong cheap, and the four layers of code that check the solid before any filament is spent.
On Four printable ESP32-S3 bench parts you get the files that method produced: three of the four ship as STL downloads, and the fourth is generated in the browser only. The downloads are a bench plate, a fit gauge, and a jumper-wire comb whose teeth stand on the 2.54 mm breadboard pitch; the fourth, a parts tray, has no STL at all, because there is no tray size that page could defend as a default. What all four do have is an in-page generator — four of them, one per part — rebuilding the solid around dimensions you type in yourself, running the same generator code in your own browser with nothing uploaded.
Follow new build-logs by RSS: paste dankbuild.com/feed.xml into any feed reader. It is a plain XML file — no signup, no email address, no account. One feed covers the whole site, so the next build-log arrives without you having to check back.
What this log does not record
The notebook is thinner than the sentence at the top of it. Here is exactly what it holds and what it does not, so nobody rebuilds from it expecting a completeness that is not there.
- The board variant. The bill of materials says “ESP32-S3 DevKitC” and that is the whole of it: no revision, no flash or PSRAM size, no seller. The two-port behaviour described above is read off Espressif's published user guide for boards sold under that name — it is documentation, not an observation of a specific board.
- The LED. “5 mm LED” is recorded. Colour, forward voltage and rated current are not.
- The resistor rating. 220 Ω is recorded as a value only — no tolerance and no wattage. Because the LED's forward voltage was never written down, the current actually flowing through it is not recoverable from this page, and this page does not pretend to compute it.
- The serial output. There is none, and that is a fact about the sketch
rather than a gap in the notes: the code never calls
Serial, so it printed nothing. No boot log, chip ID or flash-size readout was captured. - Which port this build used. The section above says which connector carries what according to the vendor's documentation. It does not say which one was plugged in here, or whether the first upload succeeded.
- Timing. 500 ms is what the sketch asks for. Nothing recorded what the LED actually did, so the blink rate on this page is the code's intent, not a reading.
- Whether it was re-run. This was built once, on 17 June 2026. There is no record of it being rebuilt, re-flashed onto a second board, or left running.
- Photographs. There are none of this build. Nothing on this page is illustrated.
And one thing this page holds that no other page on this site does: a circuit that physically existed. Measuring a dev board for 3D printing is a method note — an argument about how to proceed when a dimension is missing — and the bench parts page is a catalogue of geometry checked by code, which states on its own face that nothing on it has ever been printed or held. Both are longer and more thoroughly worked than this page. Neither describes hardware anyone assembled. This short entry is the only build-log here where a part was put in a breadboard and something lit up, and that is worth knowing if you landed on this page first and were about to judge the site by its length.