UART Logging + Debugging
Serial Frames, Baud Rate, USB Bridges, and Logs That Survive Bring-Up
The robot hand is on the bench. A new firmware image boots, the fingers twitch once, and then nothing visible happens. No Wi-Fi packet arrives. The display is blank. The motor driver enable pin might be low, or the boot mode might be wrong, or the code might be trapped before setup finishes.
UART is how the board speaks while the rest of the system is still unreliable. Before the app, before the network, before the polished dashboard, there is usually a TX pin, an RX pin, ground, a baud rate and a terminal window.
UART is not glamorous, but it is one of the first interfaces a working engineer reaches for. It is simple enough to bring up early, precise enough to debug timing, and humble enough to stay in the product as a service port or manufacturing log.
By the end, you can
- Wire a UART debug link without confusing TX, RX, ground, USB, TTL and RS-232
- Read an 8N1 UART frame: idle level, start bit, LSB-first data bits and stop bit
- Compute bit time, frame time and practical byte throughput from baud rate
- Recognize baud mismatch, voltage-level mismatch and missing-ground failures
- Design firmware logs that help during bring-up without breaking timing
- Use UART as a repeatable debug workflow, not just as scattered print statements
UART starts with three wires
A plain UART link has transmit, receive and a shared reference. One device's TX goes to the other device's RX. The return path is ground. That ground wire is not optional: without it, the receiver does not know what voltage "high" and "low" mean.
The confusing part is naming. A header labeled TX on the adapter is the adapter's transmitter, so it usually connects to RX on the target. A header labeled TX on the target connects to RX on the adapter. If both sides talk into TX, nobody is listening. If both sides listen on RX, nobody is talking.
Voltage levels are just as important. A 3.3 V microcontroller UART is usually idle high near 3.3 V and active low near ground. A 5 V adapter can damage a 3.3 V-only input. A classic RS-232 port is a different electrical standard entirely, with inverted signaling and voltage swings that can go negative. A DB9 connector and a UART header are not the same thing.
You connect a USB-to-UART adapter to an ESP32 debug header. Which connection is most likely correct?
-
Correct. UART signal names are local to each device, and both receivers need the same reference ground.
-
That connects talker to talker and listener to listener, then removes the reference the receiver needs.
-
A 5 V output can exceed a 3.3 V input's safe range.
-
RS-232 uses different voltage levels and polarity. Use an RS-232 transceiver if that is the interface.
The frame is self-clocking enough
UART stands for Universal Asynchronous Receiver/Transmitter. The important word is asynchronous: there is no separate clock wire. The receiver watches for a falling edge, waits to the middle of each bit time, and samples.
In the common 8N1 format, one byte travels as ten bit times:
- One start bit, held low.
- Eight data bits, least significant bit first.
- No parity bit.
- One stop bit, held high.
For 0x55, the binary pattern is alternating ones and zeros. Because UART sends the
least significant bit first, 0x55 is a useful scope pattern: it makes transitions on
nearly every bit and reveals baud-rate problems quickly.
Try these moves:
- Set the byte to 85, which is
0x55, and notice the alternating data bits. - Change the baud rate. The waveform shape is the same, but bit time and byte time shrink.
- Move receiver baud error toward plus or minus 8 percent. The orange sample points drift until the decoded byte can change.
- Try byte 65, which is
0x41, and watch how a printable character is only a pattern of bits.
Baud rate is a timing contract
Baud rate is the symbol rate. For ordinary UART, one symbol is one bit, so 115200 baud means 115200 bits per second.
The bit time is:
At 115200 baud:
With 8N1, each payload byte takes ten bit times:
The ideal byte rate is therefore:
At 115200 baud, that is 11520 bytes per second before terminal overhead, formatting, driver buffering and firmware costs.
A UART uses 115200 baud and 8N1 framing. Roughly how many payload bytes per second can the wire carry?
-
Correct. 8 data bits plus start and stop bits means 10 line bits per payload byte.
-
Baud is symbols per second. With 8N1 you spend 10 symbols per payload byte.
-
9600 baud is common, but it is not implied here.
-
ADC code range has nothing to do with UART throughput.
At 9600 baud with 8N1 framing, estimate the time to transmit a 40-byte status line.
Show worked solution
Each payload byte uses 10 line bits, so 40 bytes uses 400 bit times. At 9600 baud, one bit is about 104.17 microseconds. The message takes about 41.7 milliseconds on the wire, before terminal and firmware overhead.
USB is often just the way into UART
Your laptop does not usually expose a 3.3 V UART pin. Development boards hide the conversion with a USB connector and a bridge chip. The bridge enumerates as a serial port on the host and drives TX/RX pins on the microcontroller side.
On many ESP32 development boards, the same bridge also helps with flashing. Control lines such as DTR and RTS may tug reset and boot-mode pins through a small transistor network. That is why a board can enter the ROM bootloader automatically when you flash it from a tool.
Some newer microcontrollers expose a native USB device and implement a USB CDC serial port without a separate bridge chip. The workflow looks similar in your terminal, but the electrical path is different: USB packets reach firmware directly instead of arriving as a UART waveform on TX/RX pins. During early bring-up, knowing which path your board uses matters.
Logs are an instrument
The lazy version of UART debugging is to scatter prints until the bug moves. The useful version is to design logs like a measurement channel.
Good bring-up logs have structure:
- A boot banner with firmware version, build time and reset reason.
- One line when each rail, peripheral and task changes state.
- Severity levels: error, warning, info and debug.
- A timestamp or monotonically increasing tick.
- Values with units.
- Rate limits for noisy paths.
- A way to compile out verbose logs in timing-sensitive builds.
For the robot hand, a bad log is failed. A useful log is closer to: gripper current
limit hit, phase A sense 1.8 A, rail 7.4 V, command 40 percent, tick 184223. The second
line points you toward a supply, current-sense or motor-control question. The first
line points nowhere.
Logs can also break the thing they are measuring. Printing inside a fast interrupt, inside a tight control loop or during a timing-sensitive bus transaction can change the timing enough to hide the original bug. Buffer logs, lower their rate, or mark events with counters and print them later.
A motor board sometimes resets when Wi-Fi starts. Write the fields you would want in a UART log line before and after the Wi-Fi transmit attempt.
Show worked solution
Useful fields include tick time, reset reason from the previous boot, supply voltage, regulator status if available, Wi-Fi state, radio transmit start/end markers, heap or task state if relevant, and a sequence number. For this failure, voltage and reset reason matter more than a generic "starting Wi-Fi" string.
Debug the link before debugging the firmware
When UART output is missing, prove the path before changing the application. The fault is often outside the code.
Start with this order:
- Confirm the serial port name and permission on the host.
- Confirm baud rate, data bits, parity and stop bits. For ESP32 logs, 115200 8N1 is common.
- Tie grounds.
- Cross TX and RX.
- Check the adapter voltage setting.
- Press reset and watch for boot ROM text.
- Try a known-good loopback on the adapter: TX connected to RX should echo characters.
- If a DB9 connector is involved, verify whether it is RS-232 and use the right transceiver.
A terminal shows nonsense characters immediately after reset. The board is otherwise alive. What should you check first?
-
Correct. Garbled serial output is often a baud or format mismatch. A bad electrical level can also corrupt data.
-
ADC filtering does not decode UART characters.
-
I2C addresses are unrelated to UART terminal decoding.
-
PWM duty does not set serial bit timing.
Production still needs a serial story
UART is not only a beginner debugging tool. It often becomes a manufacturing fixture, service console, calibration channel or emergency recovery path. That means it needs a policy.
Decide what happens after the product leaves the bench:
- Is the UART connector populated, hidden as pads, or removed?
- Are debug logs compiled out, rate-limited or protected?
- Can the service console change calibration or only report state?
- Are secrets ever printed? They should not be.
- Can a manufacturing jig read serial numbers, test results and firmware version?
- What is the recovery path if the main network stack fails?
Key takeaways
- UART needs TX, RX and a shared ground; signal names are local, so TX usually crosses to RX.
- 8N1 sends ten line bits per byte: start, eight data bits LSB first and stop.
- and ideal 8N1 throughput is baud divided by 10.
- USB-to-UART, TTL UART and RS-232 are different layers. Do not connect them by connector shape alone.
- Good logs carry time, state, units and context; bad logs merely announce that something failed.
- A repeatable UART debug workflow starts by proving the link before rewriting firmware.
UART is the board's quietest voice. It will not move a motor or sample a sensor by itself, but it lets the system explain what it is doing while every other interface is still suspect. Learn to read the waveform, respect the voltage levels, and write logs that behave like measurements. Then a blank bench becomes a conversation.