In this lesson:
- the UART frame: idle, start bit, data (LSB-first), parity, stop
- baud rate and how the receiver recovers timing by oversampling
- wiring: crossed TX/RX, common ground, full-duplex
- an interrupt-driven RX ring buffer (the canonical driver)
- the bugs: baud mismatch, framing/overrun errors, level mismatch
The async workhorse
UART (Universal Asynchronous Receiver/Transmitter) is the go-to for debug logs and simple chip-to-chip serial. It's asynchronous (no clock line, previous lesson): the two ends just agree on a baud rate beforehand and send framed bytes over a single data line each way.
A UART line idles high. To send a byte, the transmitter frames it:
idle start d0 d1 d2 d3 d4 d5 d6 d7 [par] stop idle
βββββββ²______β±βββ²__β±βββββ²________________β±βββββββ
β 0 β βββββ 8 data bits, LSB first βββββΆ β β 1 β
start parity stop- Start bit (always 0), the falling edge tells the receiver "a frame is coming."
- Data bits, usually 8, sent least-significant bit first.
- Parity (optional), one bit for a basic error check (even/odd), or none.
- Stop bit(s) (always 1), return to idle; 1 or 2 bit-times.
Both ends must be configured identically. The shorthand "8N1" = 8 data bits, No parity, 1 stop bit, the most common setting.
Baud and how the receiver finds the bits
Baud rate is bits per second (e.g., 115200). With no clock line, the receiver can't be told when each bit is, it recovers timing locally:
- It oversamples the line (commonly 16Γ the baud rate).
- On the falling start-bit edge, it starts counting.
- It samples each bit near its center (most stable point), one bit-time apart.
oversample ticks: ββββββββββββββββ (16 per bit)
data: ββββ²________________ start bit
β² sample at center (tick 8), then every 16 ticksThis is why both ends need accurate, matched baud: the receiver free-runs its sampling from the start edge across the whole frame. If the clocks differ by more than ~2-3%, by the last bit the sample point has drifted off the bit and you read garbage (the baud-mismatch lesson quantifies this).
Wiring
UART is full-duplex, separate TX and RX lines. The catch: TX connects to the other side's RX (crossed), and both devices must share a common ground.
MCU A MCU B
TX ββββββββββββββββΆ RX
RX ββββββββββββββββ TX
GND βββββββββββββββββ GND (mandatory common reference)Connecting TXβTX is the classic first-time mistake, nothing works.
A driver: transmit, and an interrupt-driven RX ring buffer
Transmit by writing the data register when it's empty:
void uart_putc(char c) {
while (!(USARTx->SR & USART_SR_TXE)) { } // wait until TX data register empty
USARTx->DR = (uint8_t)c; // writing DR sends the byte
}Receive with an interrupt feeding a ring buffer (the DSA-module structure) so bytes are captured the instant they arrive and the main loop consumes them at its leisure:
static volatile uint8_t rx_buf[64];
static volatile uint8_t head, tail; // SPSC ring buffer (ISR=producer, main=consumer)
void USARTx_IRQHandler(void) {
if (USARTx->SR & USART_SR_RXNE) { // a byte arrived
uint8_t b = (uint8_t)USARTx->DR; // reading DR clears RXNE
uint8_t next = (head + 1) & 63; // power-of-two wrap
if (next != tail) { // drop if full (or overwrite)
rx_buf[head] = b;
head = next;
}
}
}
bool uart_getc(uint8_t *out) { // main loop polls this
if (tail == head) return false; // empty
*out = rx_buf[tail];
tail = (tail + 1) & 63;
return true;
}This is the bread-and-butter pattern: the ISR is tiny (grab byte, enqueue), and the main loop processes a frame when it's ready. For high rates, DMA replaces the per-byte interrupt.
Gotchas
- Baud mismatch = garbage. Both ends must use the same baud within ~2-3%. A wrong baud (or a wrong assumed peripheral clock, see the clock-tree lesson) yields consistent gibberish. Symptom: same wrong characters every time.
- TXβRX must cross, and grounds must be common. TXβTX gets you silence; a missing common ground gets you intermittent noise/garbage. Both are classic bring-up mistakes.
- Overrun if RX isn't serviced fast enough. If a new byte arrives before you read the previous one, the hardware sets an overrun-error flag and you lose data. Use an interrupt + ring buffer (above) or DMA; don't poll RX in a slow loop at high baud.
- Framing error = baud/noise. If the stop bit isn't read as 1, the UART flags a framing error, usually a baud mismatch, noise, or a break condition. Check baud and grounding first.
- Voltage levels must match. MCU UART is logic-level (3.3 V/5 V); RS-232 is Β±12 V. Connect them only through a transceiver (previous lesson) or you damage the MCU.
TL;DR
- UART is asynchronous serial: a line that idles high, framed as start (0) β 8 data bits LSB-first β optional parity β stop (1), configured identically on both ends (commonly 8N1).
- With no clock line, the receiver recovers timing by oversampling (e.g., 16Γ) and sampling each bit at its center, free-running from the start edge, so both ends need matched baud within ~2-3%.
- It's full-duplex with separate lines: wire TXβRX (crossed) and share a common ground.
- The canonical driver is an interrupt-driven RX ring buffer (tiny ISR enqueues, main loop consumes); DMA for high rates.
- Top bugs: baud mismatch (garbage), TX/RX not crossed or no common ground, RX overrun (service fast enough), framing errors (baud/noise), and voltage-level mismatch (use a transceiver).