This lesson has one job: integrate the UART as a block the boot ROM can rely on for its first output. The UART is the simplest peripheral on the chip and the easiest to misconfigure: one divider sets the baud, two enables gate the interrupts, and two status bits report registers exactly one byte deep. At 115,384 baud a byte arrives every 86.7 us, so the integration question is never throughput but correctness at the edges: the divider rounding, the overrun when software is late, and the interrupt that must fire neither early nor twice.
The register block is four words. DATA transmits on write and receives on read. STATUS reports TXE, transmit holding empty, and RXNE, receive holding full, both read-only. BAUDDIV divides the 24 MHz peripheral clock by 16 times the divider: with the reset value 13 the baud is 24 MHz divided by 208, which is 115,384.6, about 0.16 percent above the nominal 115,200. CTRL holds the peripheral enable and the two interrupt enables, TXIE and RXIE, all reset to 0 so the block is silent until software opts in. The interrupt is the OR of transmit-empty-when-wanted and receive-full-when-wanted: irq_uart equals TXIE AND TXE, OR-ed with RXIE AND RXNE, held while the condition lasts. Reading DATA drops the receive term; writing the next byte drops the transmit term, and with nothing left to send the handler must clear TXIE, or a transmit-empty interrupt never stops.
One byte of holding on each side sets the whole timing story. Transmit (a NanoSoC choice, not a universal rule): software writes a byte, TXE clears until the shifter finishes all 10 wire bits, then TXE sets and the interrupt, if enabled, fires for the next byte, so TXE here means transmitter idle; a UART with a separate holding register sets TXE as soon as the byte moves to the shifter. Receive: a complete byte sets RXNE and raises the interrupt; if a second byte finishes before software reads the first, the first is lost to overrun (also a NanoSoC teaching choice). The 86.67 us deadline from the latency topic is the budget this hardware gives software, generous against a 2.03 us worst-case path, which is why the UART never overruns unless its interrupt is masked or its handler is stuck behind something unbounded.
The script below derives the baud from the reset divider, checks the error against nominal 115,200, prices the boot banner of 16 characters at 10 bits each, and confirms the overrun deadline leaves a margin of better than forty to one over the worst-case interrupt path.
clk_per = 24e6
bauddiv = 0x000D # reset value 13
baud = clk_per / (16 * bauddiv)
print(f"baud: {baud:.1f}")
error = (baud - 115200) / 115200
print(f"error vs 115200: {error * 100:+.2f}%")
assert abs(error) < 0.005
banner_chars = 16 # "NanoSoC boot ok" plus newline
banner_time = banner_chars * 10 / baud
print(f"16-char banner: {banner_time * 1e6:.1f} us")
assert abs(banner_time - 1386.667e-6) < 1e-6
byte_time = 10 / baud
T_CPU = 1 / 192e6
worst_path = 182.3e-9 + 281.25e-9 + (10 + 40 + 200 + 50) * T_CPU # same budget as latency lesson
print(f"byte period (overrun deadline): {byte_time * 1e6:.2f} us")
print(f"worst-case path: {worst_path * 1e6:.3f} us")
assert 42 < byte_time / worst_path < 44
print("PASS: 115384.6 baud, banner 1386.7 us, deadline margin 43x")BAUDDIV is programmed for the board's crystal, not copied from an example; the error against nominal is computed and under half a percent.
RXIE is enabled only after the receive handler is installed, and the handler reads DATA before returning so the level drops.
The overrun behaviour is documented: which byte survives, which status bit reports it, and what software must do.
Loop the transmit pin to the receive pin on the bench and send the banner: every character must echo with RXNE setting once per byte. If the baud is off, the framing breaks on specific bit patterns first, long runs of alternating bits, which is the signature of a divider error rather than noise. If characters arrive doubled, the handler reads DATA twice or the interrupt enable was set before the handler existed; if they never arrive, BAUDDIV is wrong or the peripheral clock is gated.
The receive input is floating and noise frames phantom bytes, each setting RXNE. Reading DATA drops the level for the current byte, but the cure is at the pad: P1 carries uart_rx once UART.CTRL.EN is 1, its idle level is high, so the pad has a pull-up (a pull-down would read as a continuous break), which is the pad topic's lesson.