The UART receiver has the harder job. Its clock is unrelated to the transmitter's, the line may be noisy, and the two baud rates are never exactly equal. It must find the start of each frame, sample every bit near its middle, and report frames that are broken. This article builds a receiver that does that, and measures in simulation how much baud rate mismatch it tolerates.
9 min read | Intermediate
What You'll Learn
- Synchronize an asynchronous input before using it
- Detect a start bit and reject glitches
- Sample each bit in its middle and detect framing errors
- Calculate and measure the baud rate tolerance
The Plan
- Synchronize. The
rxline changes at any time relative to the receiver clock, so it passes through two flip-flops first, as described in Bridges and Clock Domain Crossing. - Find the start bit. In IDLE, a low line means a start bit may have begun.
- Check it. Half a bit time later, the line must still be low; otherwise it was a glitch and the receiver goes back to IDLE.
- Sample. From the middle of the start bit, every full bit time lands in the middle of the next bit: eight data bits, then parity, then stop.
- Check the stop bit. It must be 1; if not, the frame is broken and
frame_erris set.
The RTL
// UART receiver: samples each bit in its middle.
// It synchronizes rx with two flip-flops, waits for a falling edge (start bit), checks the start
// bit again half a bit later, then samples every following bit one bit time apart.
// frame_err: the stop bit was 0. parity_err: the parity bit did not match.
module uart_rx #(
parameter CLKS_PER_BIT = 16,
parameter PARITY = 0
) (
input clk,
input rst,
input rx,
output reg [7:0] data,
output reg valid, // one-cycle pulse when a byte has been received
output reg frame_err,
output reg parity_err
);
localparam IDLE = 3'd0, START = 3'd1, DATA = 3'd2, PAR = 3'd3, STOP = 3'd4;
reg rx_s1;
reg rx_s2; // rx after the two-flip-flop synchronizer
reg [2:0] state;
reg [15:0] cnt;
reg [2:0] bit_idx;
reg [7:0] shreg;
reg par;
always @(posedge clk) begin
rx_s1 <= rx;
rx_s2 <= rx_s1;
end
always @(posedge clk) begin
if (rst) begin
state <= IDLE;
valid <= 1'b0;
frame_err <= 1'b0;
parity_err <= 1'b0;
end
else begin
valid <= 1'b0;
case (state)
IDLE: begin
if (!rx_s2) begin // falling edge: a start bit begins
cnt <= 16'd0;
state <= START;
end
end
START: begin
if (cnt == CLKS_PER_BIT / 2 - 1) begin
if (!rx_s2) begin // still low in the middle: a real start bit
cnt <= 16'd0;
bit_idx <= 3'd0;
par <= (PARITY == 2);
state <= DATA;
end
else begin
state <= IDLE; // a glitch, not a start bit
end
end
else begin
cnt <= cnt + 16'd1;
end
end
DATA: begin
if (cnt == CLKS_PER_BIT - 1) begin // the middle of the next bit
cnt <= 16'd0;
shreg <= {rx_s2, shreg[7:1]}; // LSB arrives first
par <= par ^ rx_s2;
if (bit_idx == 3'd7)
state <= (PARITY != 0) ? PAR : STOP;
else
bit_idx <= bit_idx + 3'd1;
end
else begin
cnt <= cnt + 16'd1;
end
end
PAR: begin
if (cnt == CLKS_PER_BIT - 1) begin
cnt <= 16'd0;
parity_err <= (par != rx_s2);
state <= STOP;
end
else begin
cnt <= cnt + 16'd1;
end
end
default: begin // STOP
if (cnt == CLKS_PER_BIT - 1) begin
data <= shreg;
valid <= 1'b1;
frame_err <= !rx_s2; // the stop bit must be 1
state <= IDLE;
end
else begin
cnt <= cnt + 16'd1;
end
end
endcase
end
end
endmodule
The data bits enter at the top of the shift register and move down, so after eight bits the first-received bit (the least significant) sits in bit 0. valid is a one-cycle pulse, and frame_err and parity_err describe the byte delivered with it. A real receiver feeds data into a FIFO that software reads.
Quick Check
Q: A 20 ns noise pulse pulls the idle line low. With 160 ns bit times, does the receiver deliver a byte?
Show Answer
No. It enters START, but half a bit time (80 ns) later the line is high again, so it returns to IDLE. Checking the middle of the start bit is what rejects short glitches.
How Much Mismatch Is Allowed?
Because the receiver restarts its timing at every start bit, errors only accumulate across one frame. Its last sample, in the stop bit, comes 9.5 bit times after the falling edge. If the transmitter's bits are a fraction e longer or shorter, that sample moves by 9.5 × e bit times. Once that exceeds half a bit, the sample falls into the neighbouring bit. That gives a limit of about 0.5 / 9.5 = 5 percent between the two sides, and less in practice, because the receiver also loses a few clock cycles to the synchronizer and to the coarse timing of its own counter.
The simulation tests it: the transmitter uses 17 clocks per bit, the receiver expects 16, a mismatch of about 6 percent:
25 tx sends 0x4b 1595 rx got 0x4b FRAMING ERROR 1735 tx sends 0x55 3305 rx got 0x55 FRAMING ERROR 3445 tx sends 0xa3 5015 rx got 0xa3
The data bits are all sampled correctly, but by the end of the frame the samples have drifted by more than half a bit. The stop sample falls inside data bit 7. For 0x4B and 0x55, bit 7 is 0, so the receiver reports a framing error; for 0xA3, bit 7 is 1 and the error goes unnoticed. A 6 percent mismatch is too much. Clock sources with a fraction of a percent of error, such as crystals, keep a UART link safe.
Quick Check
Q: Why did the receiver report no error for 0xA3, even though the timing was just as wrong?
Show Answer
Its stop sample landed in data bit 7, which is 1 for 0xA3, the same value a stop bit has. The receiver cannot tell the difference, so a framing error only catches some timing problems.
Oversampling
Many UARTs count in sixteenths of a bit (16x oversampling) and take three samples around the middle of each bit, using the majority value. That rejects short noise spikes inside a bit and allows the start edge to be found to within 1/16 of a bit. The receiver here does the same with its counter, sampling once; adding the majority vote is a small change.
Running It in ChipVerify Lab
The example runs the transmitter at 17 clocks per bit against a receiver at 16. Try 16 and 15, and find where it stops working.
Common Mistakes
Mistake #1: Using rx without a synchronizer
The line is asynchronous to the receiver clock. Feeding it straight into the state machine risks metastability and different flip-flops seeing different values in the same cycle.
Mistake #2: Sampling at the bit edges
A receiver that counts full bit times from the falling edge samples at the boundaries between bits, exactly where the line changes. Wait half a bit first.
Mistake #3: Ignoring the stop bit
Checking the stop bit is the receiver's only way to notice a wrong baud rate or a broken line. Report framing errors to software.
Practice Exercise
Challenge: A Fast Transmitter
Set TX_CLKS to 15 and RX_CLKS to 16: the transmitter is now about 6 percent fast instead of slow. Is the result the same as with 17?
Hint
The receiver samples a little after the middle of each bit because of its synchronizer. Does that help or hurt when the bits are shorter than expected?
Solution
25 tx sends 0x4b 1535 tx sends 0x55 1595 rx got 0x8b FRAMING ERROR 3045 tx sends 0xa3 3125 rx got 0xa5 FRAMING ERROR 4655 rx got 0xd3
It is worse: even the data is wrong, and the third byte is corrupted without any error. Because the receiver's samples already sit a few cycles after the middle of each bit, shortening the bits pushes them over the edge sooner than lengthening them does. The tolerance of a real receiver is not symmetric, and both directions need checking.
Open the ready-made example in the lab and run the simulation.
Summary
- Synchronize
rxwith two flip-flops before using it. - Detect the start bit on the falling edge and confirm it half a bit later.
- Sample every bit one bit time apart, in its middle, LSB first.
- A low stop bit is a framing error; it catches some, not all, timing problems.
- The total baud rate mismatch must stay well below about 5 percent.