dankbuild

First Light: Blinking an LED on the ESP32-S3

17 June 2026·Board: 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.

Bill of materials
PartQtyWhat 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.

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.

The blink circuit: GPIO2, a 220 ohm resistor, an LED, and ground A schematic line drawing, not a photograph. One series loop leaves the ESP32-S3 DevKitC at the terminal labelled GPIO2, runs through a 220 ohm resistor, then through the LED — the long leg (anode) on the GPIO2 side, the short leg (cathode) on the far side — and on to ground, so current runs GPIO2 to LED to ground. The resistor is drawn on the GPIO2 side because it has to be drawn somewhere; it is a series circuit and it limits the same current in either position. No header pin position, board silkscreen, wire colour or resistor band pattern is shown, because this log does not record any. current: GPIO2 → LED → ground ESP32-S3 DevKitC GPIO2 220 Ω ground anode (long leg) cathode (short leg)
A drawing of that sentence, not a photograph — nothing here is traced from a board, and the site's other figures are drawings too. It carries only what this page already states: wire the LED to 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.

Every first-run failure this section describes, as symptom → cause → what to do. It is an index, not a replacement: each case is stated in full in the sub-sections below, and nothing appears here that is not argued there.
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:

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.

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:

  1. 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.
  2. LED polarity. The code drives GPIO2 high 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.
  3. Wrong rail. If the LED runs from the 3V3 rail back into GPIO2 instead of from GPIO2 out to ground, it will still blink — inverted, lit when the code says LOW. Electrically fine, logically backwards. If the blink works but feels out of phase with the code, this is why.
  4. 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.
  5. 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.
  6. 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.

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.


← All electronics build-logs

Back to the top