journal 2026-04-28

Project 03 RTL — UART transmitter

project-03uartrtlfsm

Late-night session. Two things landed:

  1. Wide layout. The site was capping content to 80rem, which on a 2560-wide monitor left huge gutters and crushed the chip viewer into a postage stamp. Bumped --max-width-page to 96rem and reworked .article-prose into a CSS grid where prose columns stay at the readable 66ch limit but figures, viewers, source blocks, and metrics grids escape into a wide track. Best of both worlds: long-form text still has a comfortable measure, but the chip viewer can finally breathe.

  2. Project 03 RTL pass. UART transmitter. Eleven-state FSM, 16-bit baud counter, 8-bit data latch. Compiles under iverilog -g2012 with one benign warning (constant selects in always_* processes) and the testbench passes first try across 0x55, 0xA5, 0x00, 0xFF.

[150000]  sending 0x55  decoded 0x55 OK
[1250000] sending 0xa5  decoded 0xa5 OK
[2350000] sending 0x00  decoded 0x00 OK
[3450000] sending 0xff  decoded 0xff OK
PASS: 4 frames sent, all decoded correctly.

The decoder in the testbench is itself a behavioural UART RX — watches for the falling edge of tx, waits 1.5 bit times to land mid-D0, samples every bit time. Same algorithm a real receiver runs.

projects/03_uart_tx/src/top.sv system-verilog · L86-103
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n)                                 data_q <= 8'h00;
    else if (state == S_IDLE && start)          data_q <= data;
  end

  // ---- next-state logic ----
  // Linear walk through the states. The only branch is IDLE → START on
  // `start`; everything else is "advance on bit_tick".
  always_comb begin
    state_next = state;
    unique case (state)
      S_IDLE:  if (start)    state_next = S_START;
      S_START: if (bit_tick) state_next = S_D0;
      S_D0:    if (bit_tick) state_next = S_D1;
      S_D1:    if (bit_tick) state_next = S_D2;
      S_D2:    if (bit_tick) state_next = S_D3;
      S_D3:    if (bit_tick) state_next = S_D4;
      S_D4:    if (bit_tick) state_next = S_D5;
The next-state walk. Linear because UART is linear: idle, start, eight data, stop.

The shape that surprised me: I almost reached for a counter (“number of bits sent so far”) and a single state for “shifting”. The eleven-state spelled-out walk is more code, but it’s cleaner — every state has exactly one tx output, and tx is a flat mux over state, not a shift register read. Easier to read in gtkwave, easier to reason about, and synthesis is going to flatten the encoding anyway.

Tomorrow: harden it. Predictions in the project page; we’ll see how they hold up.