Custom RTL Design: TX & RX
This is the first hands-on RTL episode. We translate the UART protocol specification directly into synthesizable SystemVerilog — starting from the mathematical relationship between clock frequency and baud rate, then building the TX FSM and RX center-sampling logic. Each design decision is derived from first principles so you can reproduce and modify it for any FPGA or baud rate.
Design Requirements
Target: 50 MHz system clock, 8E1 format
| Requirement | Specification |
|---|---|
| System Clock | 50 MHz |
| Baud Rate | 9,600 / 115,200 / 460,800 bps |
| Data Width | 8 bits |
| Parity | Even parity |
| Stop Bits | 1 |
| Format | 8E1 (11 bits total per frame) |
Baud counter maximum:
$$\mathrm{BAUD}_{\mathrm{CNT,MAX}} = \frac{50{,}000{,}000}{115{,}200} \approx 434$$
The counter counts system clock cycles. It counts from 0 through 433 (434 clocks), then resets. The resulting one-cycle pulse (baud_tick) illustrates a bit-period enable; the complete TX below uses a timer restarted when a request is accepted.
Integer division accuracy: 50,000,000 / 115,200 = 434.027…; using 434 gives about 0.0064% baud-rate error. After 11 bits, the accumulated offset is about 0.0007 bit periods. The total receive margin also depends on both oscillators and the sampling implementation.
Baud Rate Generator
Counts system clock cycles and produces a one-cycle baud_tick pulse.
| |
$clog2(BAUD_CNT_MAX) automatically sizes the counter register — for 434 counts, 9 bits are required. Using $clog2 keeps the code portable: changing CLK_FREQ or BAUD_RATE automatically adjusts the counter width without any manual recalculation.
TX implementation — captured frame
IDLE → START → DATA (8 bits) → PARITY → STOP → IDLE
This complete TX example accepts a one-clock tx_start on any rising clock edge while tx_ready is high. It captures the entire 8E1 frame, including parity, then holds each bit for CLK_FREQ / BAUD_RATE clocks. Input data may change after acceptance. Requests while busy are not accepted; wait for tx_ready. The stop bit completes before readiness returns. Use parameters with at least two clocks per bit. This TX has its own timer; the free-running baud generator above is a separate teaching example.
| |
RX FSM — Center Sampling
Sample each bit at its center for maximum noise margin.
$$\text{Center offset} = \frac{\mathrm{BAUD}_{\mathrm{CNT,MAX}}}{2} \approx 217,\text{CLK cycles}$$
The following is pseudocode, not compilable RTL. RX needs its own timer aligned to the synchronized start bit; do not reuse the free-running TX tick. Restart the timer after the half-bit start check, then sample D0 one full bit later. Synchronize the asynchronous input and define error/valid behavior before integration.
| |
False start detection: After the falling edge triggers the START state, the FSM waits half a bit period and re-checks
uart_rx. If the line is already High again, the transition was noise — the FSM returns to IDLE without consuming a byte. This glitch rejection is critical on noisy lines and is the reason the half-period wait exists.
Center Sampling — Animated
The receiver must sample at the center of each bit period — not at the edge — to avoid noise contamination near signal transitions.
Testbench Strategy
| |
The snippet above shows request timing only. A complete loopback test also needs RX instantiation, reset, a receive monitor, and a timeout. Verify the wire independently so matching TX/RX bugs cannot hide each other.
Download the standalone TX testbench: checks one-clock requests, input changes after acceptance, busy request rejection, bit/Stop duration, and reset during transmission. This tests the new TX example above; the existing demonstration video below is separate.
Additional verification steps:
- Confirm TX waveform: Start(0) → D0..D7 LSB-first → Parity → Stop(1)
- Verify RX samples Start at half a bit, then D0 through Stop at full-bit intervals.
- Parity error injection: force a wrong bit on the wire, confirm
parity_errasserts - Frame error injection: hold RX low during Stop bit, confirm
frame_errasserts
UART Frame Simulation Demo
Vivado simulation waveform demo (xsim)
Episode 5 Summary
- Baud Generator:
cnt == BAUD_CNT_MAX-1→ one-cyclebaud_tick - TX: capture Start, data, parity, and Stop together; serialize one bit per bit period
- RX FSM: half-period delay → center sampling; validate Start and Stop bits
- Even Parity: capture
^tx_datawhen accepting the request - Testbench: standalone TX checks provided; add loopback and error injection after integrating a complete RX
Next Episode
Episode 6: Top Module — Integrating TX and RX Clock wizard, reset logic, parameterization, reusable interface design. Continue in the Inflearn UART course (Korean).
Key Takeaways
- The baud generator uses integer division —
CLK_FREQ / BAUD_RATErounded down; the resulting error (~0.006% for 115200 bps at 50 MHz) is negligible within one UART frame - The TX captures requests on the system clock and times every bit, including Stop, with its own counter.
- RX center sampling delays
BAUD_CNT_MAX/2after the Start Bit edge before the first sample; this provides maximum margin against clock drift and edge noise - False start detection (re-check Start Bit at the half-period point) rejects glitches that would otherwise corrupt a received byte
Code and References
- RTL source repository: https://github.com/Easy-FPGA/easyfpga-uart-core-sv.git
- YouTube: https://www.youtube.com/@easy-fpga