Digital Design / Module 12: Projects and interview preparation
64. Minor project: UART transmitter and receiver
Build a real serial link.
Lab: Protocol waveforms
The goal
Build a UART transmitter and receiver, the classic first real interface. It needs no shared clock, just an agreed bit rate. This project combines counters, , an FSM and a .
The frame
The line sits high when idle. One frame is a start bit (low), then 8 data bits with LSB first, then a stop bit (high). At 115200 baud, each bit lasts about 8.68 µs. Use the protocol lab above to see the waveform for any byte.
Transmitter
- Baud generator: a counter that pulses once per bit.
- A shift register that loads
{stop, data, start}when a send is requested. - An FSM, or just a bit counter, to know when the frame is complete.
Register the TX output, so that it can't . Remember the receiver is watching this line for a falling edge.
Receiver
- Synchronize RX first. It comes from outside your .
- Oversample, commonly at 16 times the baud rate.
- On a falling edge, wait half a bit, and confirm it's still low. This filters out noise.
- Then sample once per bit at about the middle, shifting into a register.
- Check that the stop bit is high. Otherwise, raise a framing error.
Why oversampling helps
The two ends use different oscillators, so the bit rates differ a little. Sampling in the middle of each bit tolerates that mismatch: the error accumulates across the 10-bit frame, and the middle sample has margin in both directions.
Requirements for your project
- Parameterised clock frequency and baud rate
- A valid/ready interface to the user logic
- A self-checking loopback testbench, using random bytes
- Tests with baud mismatch of about 2%, a false start bit, and a bad stop bit
Common mistake
Calculating the divisor with integer truncation, which adds rate error that you never checked. For 100 MHz and 115200 baud, 100,000,000 / 115,200 = 868.06, so the small error is fine here. At high baud rates, or with slow clocks, it can matter. Always compute the actual baud error.
Key ideas
- Baud generator, framing, and oversampled receive.
- Synchronize RX before use.
- Test with loopback.
Your exercise
Find the bug
This code has one deliberate bug. Read the symptom, then click the line you think is wrong.
The fix
assign bit_out = data[idx];
Why it failed
UART sends the least significant bit first. This transmitter walks from bit 7 downwards, so the far end reassembles the bits in reverse order. The frame looks fine on a scope, so this is easy to miss unless you check the decoded value in a loopback test.
Interview questions
coreWhy oversample UART RX?
To find the start edge and sample near mid-bit, tolerating clock mismatch.
basicA UART runs at 9600 baud with 8 data bits, no parity and one stop bit. How long is one frame, and what is the maximum byte rate?
Each bit lasts 1 / 9600 s, about 104.2 microseconds. A frame is a start bit, 8 data bits and a stop bit, 10 bits in total, so it takes about 1.042 ms. Back to back, that is 9600 / 10 = 960 bytes per second. The start and stop bits are why the byte rate is a tenth of the baud rate, not an eighth.
basicSketch the line levels for sending 0x41 over a UART, from idle to idle.
The line idles high. Then comes the start bit, low. 0x41 is 0100_0001, and the data goes LSB first, so the data bits are 1, 0, 0, 0, 0, 0, 1, 0. Then the stop bit is high, and the line stays high at idle. The receiver finds the frame from the falling edge at the start of the start bit.
coreA 50 MHz system clock drives a UART at 115200 baud with 16 times oversampling. What tick divisor do you use, and what is the actual baud error?
The ideal divisor is 50,000,000 / (115,200 x 16), about 27.13, so a counter uses 27. The real bit time is then 27 x 16 = 432 clocks, so the actual rate is 50,000,000 / 432, about 115,741 baud. That is about 0.47 percent fast, which is acceptable. The point is to compute the real rate after rounding, not to assume it.
coreWhy does the receiver synchronize RX first, and then check the start bit again half a bit after the falling edge?
RX comes from another chip with its own clock, so it can change at any moment relative to the receiver's clock and must go through a two-flop synchronizer before any logic uses it. Otherwise several flops in the state machine could read the same edge differently. The half-bit check filters noise: a short glitch low will be high again at the middle of the would-be start bit, so the receiver returns to idle. If it is still low, the start is real, and every later sample lands near the middle of a bit.
coreA loopback test that only ever sends 0xA5 passes, but real traffic arrives wrong. The transmitter's data bit line is shown. What is the bug, and why did the test miss it?
always @(*)
case (state)
IDLE: line_d = 1'b1;
START: line_d = 1'b0;
DATA: line_d = data[7 - idx]; // idx counts 0 to 7
default: line_d = 1'b1; // STOP
endcaseThe transmitter sends data[7 - idx], so the MSB goes out first, while a UART receiver expects the LSB first. Every byte arrives bit-reversed: 0x01 comes back as 0x80, and 0x41 as 0x82. 0xA5 is 1010_0101, which reads the same in both directions, so it hides the bug. Random bytes in the loopback test catch it straight away.
coreIf the line is held low for a long time, a break condition, what should your receiver do?
The receiver sees a valid start bit, samples eight zeros and then a low stop bit, so it should flag a framing error and not deliver 0x00 as a normal byte. If it then looks for a new start bit straight away, the still-low line would look like start bits again and again, producing a stream of errors. A common choice is to wait for the line to return high before searching for the next start bit, and optionally report the break to the user logic.
advancedWith 16 times oversampling, the receiver finds the start edge up to one tick late and samples the stop bit about 9.5 bits after the edge. Roughly how much clock mismatch can it tolerate?
The last sample must stay within half a bit of the middle of the stop bit. Over 9.5 bits, half a bit of drift is 0.5 / 9.5, about 5.3 percent. In the direction where the late start adds to the drift, one tick, 1/16 of a bit, is already used, leaving (0.5 - 0.0625) / 9.5, about 4.6 percent. With only 4 ticks per bit, that side drops to (0.5 - 0.25) / 9.5, about 2.6 percent, which is why finer oversampling gives more margin. Real links budget well below these limits, to leave room for edge distortion and noise.
advancedA 10 MHz system clock must run a UART at 921,600 baud. Can it use 16 times oversampling?
16 times oversampling needs ticks at 921,600 x 16, about 14.7 MHz, faster than the 10 MHz clock, so it is impossible. Even 8 times needs about 7.37 MHz, a divisor of 1.36; rounding to 1 gives 1.25 Mbaud, about 36 percent fast. Lower oversampling ratios cut the rounding error but also cut the sampling margin. The honest answer is to use a faster or better-matched clock, or a lower baud rate, and to compute the actual error for any option.