prerequisite
Serial communication basics
How bytes travel one bit at a time over a wire: baud rate, start and stop bits, and the difference between a UART and USB.
Before this
This page assumes you are comfortable with:
Why you need this
Almost everything you do with an ESP32 goes over a serial link: flashing a program, reading its log, typing at a MicroPython prompt, and the uppercase echo itself. When that link misbehaves, you see garbage characters, silence, or a port that will not open. Knowing how one byte is framed on a wire, and what sits between the chip and your computer, turns those symptoms into causes.
The idea
Parallel and serial
A byte is 8 bits. A parallel link sends all 8 at once on 8 wires. A serial link sends them one after another on a single wire. Serial needs far fewer pins and cables, and it is how almost every microcontroller talks to a computer.
Asynchronous serial: the UART frame
A UART (universal asynchronous receiver-transmitter) is the piece of hardware that sends and receives bytes this way. "Asynchronous" means there is no shared clock wire: both ends must agree in advance on how long each bit lasts. Every byte travels inside a frame:
- Idle: between bytes the line sits high (a 1, at 3.3 V on an ESP32).
- Start bit: the sender pulls the line low (0) for one bit time. The falling edge tells the receiver "a byte begins now" and starts its timer.
- Data bits: 8 bits, least significant bit first, so bit 0 goes out before bit 7.
- Stop bit: the line goes high (1) for at least one bit time, ready for the next start bit.
This format is called 8N1: 8 data bits, no parity bit (an optional extra error-check bit), 1 stop bit. A frame is 10 bit times for 8 bits of data.
Baud rate
The baud rate is how many bit times fit in one second. At 115200 baud, one bit lasts s. The receiver samples near the middle of each bit time, counting from the start bit's edge. If the two ends disagree on the baud rate, the receiver samples at the wrong moments and reads wrong bits: the classic stream of garbage characters. The ESP32 Inspector's classic ESP32 echo program notes that the chip's ROM sets its first UART to 115200 baud, 8N1, at power-on, which is why 115200 is the usual setting.
TX and RX cross
Each side has a TX (transmit) pin it drives and an RX (receive) pin it listens on. So one board's TX connects to the other's RX, and the other way round, plus a shared GND so both agree on what 0 V is. The ESP32 Inspector's C6 clone project wires two C6 boards this way:
SOURCE GPIO16 (U0TXD) ─► TARGET GPIO17 (U0RXD)
SOURCE GPIO17 (U0RXD) ◄─ TARGET GPIO16 (U0TXD)
GND ───────────────────── GND
UART bridges and native USB
Your computer has USB, not a 3.3 V UART. Two arrangements close the gap:
- A USB-to-serial bridge chip on the board, such as a CH340 or CP2102, speaks USB to the computer and UART to the ESP32. The CYD uses a CH340; the WROOM-32 DevKit a CP2102, which on Windows needs Silicon Labs' driver before a port appears. Here the baud rate matters, because a real UART frame crosses the board.
- Native USB on the chip. The ESP32-S3, C6, and P4 have a built-in USB-Serial/JTAG controller, so the chip itself is the USB device; the author's notes record it showing up with USB ID
303A:1001. There is no UART frame on that path and no baud rate to match. The chip collects bytes and sends them as USB packets.
What the computer sees
Either way, the computer shows a serial port: on Windows a COM port such as COM5, on Linux a device such as /dev/ttyUSB0. A serial monitor, esptool, or Thonny opens that port. Only one program can hold a port at a time.
Type a character to see its frame. Switch between 9600 and 115200 baud and watch the bit time change. Tick toUpper with a lowercase letter and only one bit changes: bit 5, shown in orange.
Worked example
Send a at 115200 baud, 8N1.
The byte. a is 0x61, which is 0b01100001. Sent least significant bit first, bit 0 to bit 7, the data bits are 1 0 0 0 0 1 1 0.
The frame on the wire, left to right in time:
| Slot | idle | start | b0 | b1 | b2 | b3 | b4 | b5 | b6 | b7 | stop | idle |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Level | 1 | 0 | 1 | 0 | 0 | 0 | 0 | 1 | 1 | 0 | 1 | 1 |
The echo sends back A, 0x41 = 0b01000001. Its frame differs in one slot: b5 becomes 0.
The bit time.
(µs is microseconds, millionths of a second.) The 10-bit frame takes µs.
Bytes per second. Each byte costs 10 bit times, so
That is the most a UART at 115200 baud can carry, with no gaps between frames.
| Baud | Bit time | Frame time | Bytes per second |
|---|---|---|---|
| 9600 | 104.17 µs | 1041.7 µs | 960 |
| 115200 | 8.68 µs | 86.8 µs | 11,520 |
A test pattern. The C6 clone project's loopback test sends 0x55 and checks it comes back. 0x55 is 0b01010101, so its data bits go out as 1 0 1 0 1 0 1 0: after the start bit the line flips at every bit, which makes any timing error easy to see on an oscilloscope.
In an ESP32 project
On UART boards, the program reads and writes the UART's registers. This excerpt is from the ESP32 Inspector's echo for the classic ESP32 (the lx6-uart0 config), which has run on real hardware; it is Xtensa assembly, and its comments show it relying on the ROM's 115200 8N1 setup:
# UART0 TX/RX is routed through the CH340 USB-to-serial bridge.
# The ROM bootloader initializes UART0 to 115200 baud, 8N1.
# We inherit that configuration
...
echo_loop:
movi a2, 0x3FF4001C # UART_STATUS_REG
l32i a3, a2, 0 # read status
extui a3, a3, 0, 8 # extract bits 7:0 = RXFIFO_CNT
beqz a3, echo_loop # spin until at least one byte arrives
The program never sees start or stop bits. The UART hardware strips them and drops each received byte into a small queue (a FIFO); the code just waits until the count of waiting bytes is above zero. Serial over UART and USB compares this with the native USB version register by register.
Common mistakes
- Mismatched baud rate. Symptom: a stream of random characters instead of text. Set both ends to the same value, usually 115200.
- TX wired to TX. Symptom: silence in both directions. Cross them.
- No shared GND between two boards. Symptom: intermittent garbage or nothing.
- A charge-only USB cable. Symptom: the board powers up but no port appears. The author's CYD setup notes list this first under "Board not found".
- Port held by another program. Symptom: "port busy" or "access denied" when flashing. Close the serial monitor first.
- Two boards with the same bridge chip. Symptom: the port names swap, and you flash the wrong board. Plug in one at a time.
Cost
A UART link costs two pins and almost no CPU time, because the hardware shifts bits out on its own. Its limit is speed: at 115200 baud, at most 11,520 bytes per second, so 1 KB (1024 bytes) takes about 89 ms, and a long burst of log messages can slow a program that waits for each one to send. Native USB costs no extra chip on the board and is not limited by a baud rate, but the port can disappear and come back when the chip resets (the author's notes record this for a press of the reset button), which tools must handle.
Going further
- Parity bits and two stop bits: the rest of the 8N1 family.
- Flow control with RTS and CTS lines, and how bridge boards reuse DTR and RTS to reset the chip.
- RS-232 and RS-485, the same framing at different voltages and distances.
- I2C and SPI peripherals, serial buses that do share a clock wire.
- Serial over UART and USB, the ESP32-specific version of this page.
Leads to
- techniqueChips, boards, and configsThe ESP32 family as the ESP32 Inspector catalogs it: which chips and boards exist on the workbench, and the five configs that group them by processor core and USB path.
- techniqueI2C and SPI peripheralsThe two buses most sensors and displays use: I2C addresses and pull-ups, SPI clock and chip selects, and how to find a device that will not answer.
- techniqueSerial over UART and USBThe two ways an ESP32 talks to your computer, UART0 through a bridge chip or the chip's own USB-Serial/JTAG, and why the echo program differs between them.
Back to ESP32 development: assembly, C, MicroPython, and CircuitPython