Lab 2.3 — UART Decode
← Course 2 syllabus · Module 2 · Prev: « Lab 2.2 · Next: Lab 3.1 »
Goal
Get text off the STM32 and onto your host, and understand — bit by bit — how it gets there. You will configure USART2 on the NUCLEO-L476RG (pins PA2/PA3), which is routed through the on-board ST-LINK to a USB Virtual COM Port, send printf-style output, and open it in a host serial terminal. Then you will put the Saleae Async Serial analyzer on the TX line and decode the exact same bytes off the wire — watching the start bit, eight data bits, and stop bit go by, and confirming that each bit is one bit-time wide. This is the debugging channel you will use in every firmware lab from here on, and understanding the frame format (rather than treating “serial” as magic) is what lets you diagnose baud-rate mismatches and framing errors instead of guessing.
Recommended reading
- Kuo Ch. 1 — the embedded DSP system and its host/console link. Light.
- STM32L476 reference manual (RM0351) — the USART chapter: frame format (start/data/parity/stop), and the baud-rate register (BRR) / oversampling. Read the frame-format figure.
- NUCLEO-L476RG user manual (UM1724) — the section confirming USART2 (PA2 TX / PA3 RX) is connected to the ST-LINK VCP by default.
- Saleae Logic 2 docs — “Async Serial analyzer”: setting baud, bits, parity, stop bits, and bit order.
Equipment & parts
- STM32 NUCLEO-L476RG + USB cable (this single cable carries power, debug, and the virtual COM port).
- Saleae Logic 8 + Logic 2, with one channel free to tap TX (PA2) and a GND lead.
- Host with STM32CubeIDE, plus a serial terminal: the CubeIDE built-in console,
screen/minicom, orpyserial(python -m serial.tools.miniterm).
Safety & don’t-break-it
- 3.3 V logic, share ground. The USART lines idle high at 3.3 V. Clip a Saleae GND to a Nucleo GND pin before touching the signal, and set the Saleae logic threshold to 3.3 V.
- Tap TX, don’t drive it. Connect the Saleae channel to PA2 (TX) as a passive observer. Never configure two devices to drive the same line — if you also wire something to TX as an output, you create bus contention that can damage a pin.
- Do not cross TX and RX to the level shifter carelessly. In this lab the TX line stays in the 3.3 V domain (Nucleo ↔︎ ST-LINK, on-board). If you later wire USART to an external 5 V device, use the Coliao level shifter — a 5 V idle-high line into PA3 (RX) violates the non-5 V-tolerant analog constraint on some pins.
- Match the terminal’s settings to the firmware (baud, 8N1). A mismatch shows up as garbage, not damage — but it wastes time. Decide the baud once and use it everywhere.
- Don’t call
printffrom inside a fast ISR (see Lab 2.2) — a blocking UART write in a 10 kHz interrupt overruns. Print frommain().
Background
An asynchronous serial (UART) line carries one byte at a time as a frame. There is no shared clock — sender and receiver each generate their own clock from an agreed baud rate, and re-synchronize on every frame’s start bit. The line idles high. A standard 8N1 frame is:
- 1 start bit (line pulled low) — the falling edge tells the receiver “a frame is coming; sample the next bits.”
- 8 data bits, least-significant-bit first.
- 0 parity bits (the “N” = none), or 1 parity bit if enabled.
- 1 stop bit (line returns high).
So one 8N1 byte occupies 10 bit-times on the wire. The fundamental unit is the bit time:
\[t_{\text{bit}} = \frac{1}{\text{baud}}.\]
At a common 115200 baud:
\[t_{\text{bit}} = \frac{1}{115200} \approx 8.68\ \mu\text{s}, \qquad t_{\text{frame}} = 10\,t_{\text{bit}} \approx 86.8\ \mu\text{s}.\]
The receiver samples each data bit near the middle of its bit-time (STM32 USART oversamples by 16 or 8 to find that center). If the two ends disagree on baud by more than a few percent, the sampling point drifts across a full bit over the frame and the last bits are read wrong — a framing error (the stop bit is sampled low instead of high). Framing errors are how a baud mismatch announces itself; the USART sets a FE flag when the stop bit is not high where expected. Parity, when enabled, catches single-bit corruption but is orthogonal to baud.
The STM32 generates its baud by dividing the USART clock; the value in BRR is \(f_{CK}/\text{baud}\) (with oversampling factored in). CubeMX computes it for you from the requested baud, but the point is that the actual on-wire bit time is \(1/\text{baud}\), and that is what the Saleae will confirm.
Procedure
Part A — Configure USART2 and print.
In the
.ioc: USART2 is usually already enabled (Mode = Asynchronous) with PA2 = USART2_TX, PA3 = USART2_RX, because the board template wires it to the ST-LINK VCP. Confirm this. Set Baud Rate = 115200, Word Length = 8 bits, Parity = None, Stop Bits = 1 (this is 8N1).Generate code. Retarget
printfto USART2 by providing_write(or useHAL_UART_Transmitdirectly). Illustrative:/* Redirect printf to USART2 (huart2). Provide _write for newlib. */ int _write(int file, char *ptr, int len) { HAL_UART_Transmit(&huart2, (uint8_t *)ptr, len, HAL_MAX_DELAY); return len; }and in
main():/* USER CODE BEGIN WHILE */ uint32_t n = 0; while (1) { printf("hello %lu\r\n", n++); HAL_Delay(500); }(Illustrative — write your own. Note
\r\nso terminals show clean line breaks;HAL_UART_Transmitblocks until the frame(s) are clocked out, which is fine at 2 Hz.)Build and flash over the ST-LINK.
Part B — See it on the host terminal.
- Find the virtual COM port: on macOS it appears as
/dev/tty.usbmodemXXXX. Open it at 115200 8N1. Options:python -m serial.tools.miniterm /dev/tty.usbmodemXXXX 115200screen /dev/tty.usbmodemXXXX 115200(exit withCtrl-Athenk)- or the STM32CubeIDE built-in serial terminal (set the same 115200 8N1).
- You should see
hello 0,hello 1, … tick by twice a second. If you get garbage, the baud in the terminal doesn’t match the firmware (see Analysis).
Part C — Decode the wire with the Saleae.
- Clip a Saleae channel to PA2 (TX) on the Morpho/Arduino header and a GND lead to a Nucleo GND. Set the logic threshold to 3.3 V.
- Add the Async Serial analyzer in Logic 2: set Baud = 115200, 8 data bits, No parity, 1 stop bit, LSB first, idle level high. (Logic 2 can also auto-detect baud from the shortest pulse — try that and confirm it lands near 115200.)
- Capture while the board prints. The analyzer should overlay decoded ASCII (
h,e,l,l,o, …) on the waveform. Zoom into one byte and identify the start bit, the eight data bits (LSB first), and the stop bit.
Part D — Measure the bit time and break it.
- With a timing measurement, span one bit and read its width → compare to \(t_{\text{bit}}=1/115200\approx8.68\ \mu\text{s}\). Span a full 8N1 frame (start → stop) → ≈ 86.8 µs.
- Deliberately induce a framing error: set the terminal (or the Saleae analyzer) to a different baud (e.g. 9600) while the firmware stays at 115200, and observe the garbage / mis-decode. Then set the firmware to a non-standard baud and confirm the Saleae’s measured bit-time changes as \(1/\text{baud}\).
- Optionally enable parity = Even in both the
.iocand the Saleae analyzer and watch the extra bit appear before the stop bit.
Deliverable & expected results
docs/lab-2-3.md plus a Logic 2 capture with the Async Serial analyzer decoding your message, and a screenshot of the host terminal output.
| Quantity | Predicted | Measured |
|---|---|---|
| Bit time at 115200 baud | 8.68 µs | … |
| 8N1 frame time (10 bits) | 86.8 µs | … |
| Decoded byte for ASCII ‘h’ (0x68) | LSB-first 0 0 0 1 0 1 1 0 between start/stop |
… |
| Bit time after changing baud to 57600 | 17.36 µs | … |
| Effect of terminal/firmware baud mismatch | garbage / framing error | … |
Analysis & reconciliation
Confirm the measured bit-time equals \(1/\text{baud}\) to within a fraction of a percent — the STM32’s BRR divider quantizes the baud slightly, so the actual baud may differ from the nominal by a small amount; if the Saleae’s auto-detected baud is, say, 115108 instead of 115200, that is the divider rounding, and it is well within the ~2–3% tolerance UART allows. Verify the frame really is 10 bit-times (start + 8 + stop) and that the data bits are LSB-first by decoding one known character by hand: ‘h’ = 0x68 = 0110 1000, sent LSB-first as 0,0,0,1,0,1,1,0 between a low start bit and a high stop bit.
Explain the mismatch experiment: when the receiver’s baud disagrees with the sender’s, its mid-bit sampling instants drift; by the stop bit the sample lands in the wrong bit, the stop bit reads low, and the USART flags a framing error (FE). That is why “garbage on the terminal” almost always means wrong baud, not broken hardware — a lesson worth internalizing now, because you will use this console to debug every subsequent lab.
Going further
- Switch
HAL_UART_Transmit(blocking) to interrupt or DMA transmit (HAL_UART_Transmit_IT/_DMA) somain()isn’t stalled during the frame — measure on the Saleae that the bytes still go out cleanly while the CPU is free. - Add RX: echo received characters back, and decode both TX and RX on two Saleae channels simultaneously to see the full-duplex exchange.
- Bridge to Module 3: the same logic-analyzer decode workflow applies to I²C — Lab 3.1 puts the Saleae’s I²C analyzer on the bus to scan device addresses.