RobotForge

Courses From Blink to Bot — ESP32-S3 robotics with the Fre…

Lesson 2 · ~22 min · Free preview

Blink — the secret life of a GPIO pin

What digitalWrite actually does, why some pins kill themselves, and how to debug a circuit that compiles fine but doesn't light up.


Every roboticist's first program is Blink. Every roboticist's first dead board is also Blink. The difference between the two outcomes is understanding what is actually happening underneath digitalWrite(2, HIGH).

What a GPIO pin is

A GPIO — General Purpose Input/Output — is a physical pin on the ESP32-S3 connected to a transistor pair inside the chip. When you call pinMode(2, OUTPUT), you tell the chip: "I want to drive a voltage on this pin." When you call digitalWrite(2, HIGH), you tell it: "drive that pin to 3.3 V." LOW means drive it to 0 V (ground).

"Drive" is the active word. The pin isn't connected to a battery — it's connected to a tiny MOSFET switch inside the chip that sources current (when HIGH) or sinks it (when LOW). The amount of current it can handle is the spec that kills hobbyist circuits.

The number that matters: 40 mA

The ESP32-S3 datasheet rates each GPIO at roughly 40 mA absolute maximum. The sustainable safe number is around 20 mA. If you exceed that for any length of time, you damage the output stage. Sometimes that means the pin goes dead. Sometimes the chip browns out and resets. Sometimes the failure is intermittent and you spend an hour wondering why your code is "buggy" when actually your hardware is dying.

A red LED at 3.3 V with no resistor in series pulls about 25 mA. That's already over the safe limit. Add a 220 Ω resistor in series and you bring it down to ((3.3 − 1.8) / 220) = ~7 mA, which is fine for the GPIO and visible on the LED. That resistor isn't optional — it's the difference between a 5-year robot and a 5-hour robot.

Build it on the breadboard

Freenove's tutorial chapter 5 ("LED") has the wiring photo if you want the visual reference. The circuit is three components:

  1. GPIO 2 on the ESP32-S3 → one leg of a 220 Ω resistor.
  2. The other leg of the resistor → the anode (long leg) of an LED.
  3. The cathode (short leg) of the LED → GND on the ESP32-S3.

Current flows from the GPIO pin, through the resistor, through the LED, into ground. The LED is a diode — it only conducts in one direction, so polarity matters. If you wire it backwards, nothing breaks, but nothing lights up either.

Three reasons your LED might not light:

  • You wired it backwards. Flip the LED. Long leg toward the resistor, short leg toward GND.
  • The breadboard rows aren't where you think. The two halves of the breadboard are split at the middle ravine. The leftmost rail isn't always connected end-to-end either — many boards have a break at the midpoint.
  • You're driving a pin that isn't actually GPIO 2 on your board. Some ESP32-S3 dev boards expose the GPIO number on a silkscreen; others label them as D2, D4, D5. The number printed isn't always the GPIO number. Freenove's pinout reference for your specific board is the truth.

The code that lights it up

const int LED_PIN = 2;  // change this to whichever GPIO you wired

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

void loop() {
  digitalWrite(LED_PIN, HIGH);
  delay(500);
  digitalWrite(LED_PIN, LOW);
  delay(500);
}

Upload. Watch. Adjust the delays — try 100 and 100 (it'll look continuous because your eye averages above ~30 Hz). Try 50 ms HIGH and 950 ms LOW — that's an asymmetric blink that uses 5% of the power of a 50/50 blink. These are the building blocks of PWM, which is the topic of lesson 5.

What digitalWrite actually does

When you call digitalWrite(LED_PIN, HIGH), the Arduino library translates that into a write to one of the ESP32-S3's GPIO output registers. Each GPIO has a few control bits in a memory-mapped register; flipping the right bit causes the silicon to drive the pin.

The total latency from digitalWrite in your code to the pin actually changing voltage is around 200 nanoseconds. That's fast: in the time it takes you to read this sentence, you could toggle the pin about ten million times. For LEDs this doesn't matter. For motor control where you care about precise pulse timing, this latency adds up — and that's why the ESP32 has dedicated hardware peripherals (RMT, LEDC, MCPWM) that toggle pins on a schedule without the CPU having to wake up. We'll use them when we get to motor control.

Three ways to debug a broken Blink

The classic Blink not blinking. Here's the order to check, fastest first:

1. Is the code actually running?

Open the Serial Monitor (Tools → Serial Monitor) at 115200 baud. Add a print at the top of setup():

void setup() {
  Serial.begin(115200);
  delay(200);
  Serial.println("Blink booted");
  pinMode(LED_PIN, OUTPUT);
}

Press the reset button on the board. You should see Blink booted in the Serial Monitor. If you don't, either the board didn't actually receive the upload (try again, hold BOOT) or the USB cable is charge-only (the most common cause).

2. Is the pin actually toggling?

If the code is running but the LED is dark, the GPIO might be working but the LED circuit isn't. A multimeter is gold here — set it to DC volts, put the red probe on the GPIO pin, black on GND. You should see it bounce between 0 V and ~3.3 V at 1 Hz. If it does, the chip is fine and your wiring is wrong.

No multimeter? Bridge the LED's anode pin directly to 3.3 V on the breadboard's positive rail (with the resistor still in series). The LED should light steady. If it does, the LED is fine and the GPIO isn't reaching it.

3. Is the GPIO actually GPIO 2?

Some boards have the LED hard-wired to a different pin (commonly 48 for boards with an RGB LED, or 13 on older S3 variants). Try this sketch to scan-test eight pins at once:

const int PINS[] = {2, 4, 5, 13, 21, 47, 48, 35};

void setup() {
  for (int i = 0; i < 8; i++) pinMode(PINS[i], OUTPUT);
}

void loop() {
  for (int i = 0; i < 8; i++) {
    digitalWrite(PINS[i], HIGH);
    delay(100);
    digitalWrite(PINS[i], LOW);
  }
}

Move your LED+resistor combination from pin to pin. Whichever pin makes it blink is the right one — that's your onboard-LED pin. Update LED_PIN at the top of your code.

One last note: pull-ups and floating pins

This is going to matter in the next lesson when we start reading buttons, so it's worth seeing once now. A pin configured as INPUT is high-impedance — it's "listening" rather than driving. If you don't connect anything to it, the pin is floating: noise in the air can be enough to flip it between HIGH and LOW unpredictably.

You'll see this when you wire a button: the button connects the pin to ground (or to 3.3 V). When the button is open, the pin is floating, and your sketch sees random clicks. The fix is a pull-up or pull-down resistor — a weak ~10 kΩ resistor that pulls the pin to a known state when nothing else drives it. The ESP32-S3 has these resistors built in; you just have to enable them with pinMode(pin, INPUT_PULLUP). We'll use this constantly starting in lesson 3.

What you should know going forward

  • GPIO output drives current; current through hardware needs a resistor.
  • 20 mA is the budget you should design to; 40 mA is the cliff.
  • The fastest debugging path is: Serial println first, multimeter second, swap components third.
  • The pin number in code is the chip's GPIO number, which is not always what's printed on the silkscreen.
  • Floating inputs are the cause of about 30% of all weird microcontroller bugs.

Next lesson: Breadboards demystified — the rails, the bus, and the trap.