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:

  1. Idle: between bytes the line sits high (a 1, at 3.3 V on an ESP32).
  2. 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.
  3. Data bits: 8 bits, least significant bit first, so bit 0 goes out before bit 7.
  4. 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 1/115,2001 / 115{,}200 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.

tbit=1115,200 s≈8.68 μst_{bit} = \frac{1}{115{,}200} \text{ s} \approx 8.68\ \mu\text{s}

(µs is microseconds, millionths of a second.) The 10-bit frame takes 10×8.68≈86.810 \times 8.68 \approx 86.8 µs.

Bytes per second. Each byte costs 10 bit times, so

115,200 bits per second10 bits per byte=11,520 bytes per second\frac{115{,}200 \text{ bits per second}}{10 \text{ bits per byte}} = 11{,}520 \text{ bytes per second}

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

Back to ESP32 development: assembly, C, MicroPython, and CircuitPython