Lesson 1 of 5 · 16 min
The ESP32 at a glance
The Arduino Uno taught you the shape of every embedded program: configure, act, wait. The ESP32 keeps that shape but changes the scale. It is a 32-bit dual-core system with a built-in radio, priced like an Uno clone. The price of that power is a handful of rules you must respect, because a board that connects to Wi-Fi also has far more ways to surprise you. This lesson maps the chip, and the traps, before we use the radio.
What is inside the chip
The classic ESP32 has two Xtensa LX6 cores that run at up to 240 MHz, 520 KB of on-chip SRAM, a Wi-Fi radio (802.11 b/g/n, 2.4 GHz only) and Bluetooth with both classic and Low Energy (BLE). Program code lives in external SPI flash, typically 4 MB, which the chip reads through a cache.
| Feature | Arduino Uno (ATmega328P) | ESP32 (classic) |
|---|---|---|
| CPU | 8-bit AVR, 1 core, 16 MHz | 32-bit Xtensa LX6, 2 cores, up to 240 MHz |
| SRAM | 2 KB | 520 KB |
| Program storage | 32 KB on chip | 4 MB typical, external flash |
| Logic level | 5 V | 3.3 V |
| ADC | 10-bit, 6 channels | 12-bit, up to 18 channels, two units |
| Radio | none | Wi-Fi + Bluetooth classic + BLE |
Do the ratios: 240 MHz / 16 MHz = 15 times the clock, and 520 KB / 2 KB = 260 times the RAM. A single TLS connection needs tens of kilobytes for its buffers, which an Uno could never hold. That is why networking only became practical on hobby boards once chips like this arrived.
Blue pins: analog inputs. Orange pins: digital I/O.
Two cores and an RTOS
Underneath the Arduino API the ESP32 runs FreeRTOS, a small real-time operating system. The Arduino setup() and loop() run inside one task, pinned to core 1. The Wi-Fi and TCP/IP stack run in system tasks, mostly on core 0. So while your loop() is busy, the radio keeps working in the background and the connection stays alive.
That has a consequence worth knowing: a long delay() in loop() no longer freezes the whole chip, but a task that blocks for seconds can still starve your own network code. The habit from the Uno course, using millis() instead of long delays, matters even more here.
3.3 V logic
Every ESP32 pin works at 3.3 V, and pins are not 5 V tolerant. A pin can only drive a modest current (design for 12 mA or less, the datasheet absolute maximum is 40 mA), and feeding it 5 V can damage the input stage over time or at once.
To read a 5 V signal, use a resistor divider:
Vout = Vin x R2 / (R1 + R2) = 5 V x 2 kΩ / (1 kΩ + 2 kΩ) = 3.33 V
For a fast or bidirectional signal, such as I2C to a 5 V device, use a proper level-shifter module instead.
The GPIO matrix and pin traps
On an Uno, pin 9 means Timer 1 and nothing else. On the ESP32, most peripheral signals (UART, SPI, I2C, PWM) go through a GPIO matrix, a programmable crossbar that can connect a peripheral to almost any pin. That freedom is why Wire.begin(21, 22) can be moved to other pins in one line.
Freedom has exceptions:
- GPIO 34 to 39 are input only and have no internal pull-up or pull-down.
- GPIO 6 to 11 are wired to the flash chip. Never use them.
- Strapping pins are sampled at reset to choose the boot mode. If your circuit pulls them the wrong way, the board will not start.
| Pin | Sampled at boot for | Safe rule |
|---|---|---|
| GPIO 0 | Low means serial download mode | Leave floating or high at power-up |
| GPIO 2 | Must be low or floating to flash | Fine for the on-board LED |
| GPIO 12 | Flash voltage (high selects 1.8 V) | Do not hold high at boot on 3.3 V flash |
| GPIO 5, 15 | Boot log and timing | Avoid heavy loads at boot |
GPIO 12 is the classic trap: a sensor that pulls it high at power-up makes the chip configure the flash for the wrong voltage, and the board crash-loops with no useful message.
The ADC2 and Wi-Fi conflict
The ESP32 has two ADC units. ADC1 is on GPIO 32 to 39. ADC2 is on GPIO 0, 2, 4, 12 to 15 and 25 to 27, but the Wi-Fi driver needs ADC2 for itself. While Wi-Fi is running, analogRead() on an ADC2 pin fails and returns garbage or zero.
So for any project that connects to a network, put analog sensors on ADC1. A 12-bit reading gives 2^12 = 4096 steps, so with a 3.3 V range one step is 3.3 V / 4095 = 0.8 mV. The converter is somewhat nonlinear near the ends of its range, so for readings in millivolts use analogReadMilliVolts(), which applies factory calibration.
Your first ESP32 sketch
This sketch reports what the chip really has, then blinks. Open the Serial Monitor at 115200 baud.
const uint8_t LED_PIN = 2; // on-board LED on many DevKit boards (also a strapping pin)
void setup() {
Serial.begin(115200); // the ESP32 console default is much faster than 9600
delay(500); // let the monitor attach
Serial.printf("Chip: %s rev %d, %d cores\n",
ESP.getChipModel(), ESP.getChipRevision(), ESP.getChipCores());
Serial.printf("CPU: %u MHz\n", ESP.getCpuFreqMHz());
Serial.printf("Heap: %u of %u bytes free\n", ESP.getFreeHeap(), ESP.getHeapSize());
Serial.printf("Flash: %u KB\n", ESP.getFlashChipSize() / 1024);
Serial.printf("setup() runs on core %d\n", xPortGetCoreID());
pinMode(LED_PIN, OUTPUT); // configure the LED pin as an output
}
void loop() {
digitalWrite(LED_PIN, HIGH); // 3.3 V, LED on
delay(500);
digitalWrite(LED_PIN, LOW); // 0 V, LED off
delay(500);
}
Expect to see a free heap well under 520 KB. Part of the SRAM is reserved for the system, and Wi-Fi will take a further share when you turn it on in the next lesson.
Check yourself
Which pin group is safe for analogRead while Wi-Fi is running?
Check yourself
A sensor outputs 5 V and you build a divider with 1 kΩ on top and 2 kΩ to ground. What voltage reaches the ESP32 pin?