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

  1. Synchronize. The rx line 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.
  2. Find the start bit. In IDLE, a low line means a start bit may have begun.
  3. 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.
  4. 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.
  5. Check the stop bit. It must be 1; if not, the frame is broken and frame_err is 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:

Output
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
Output
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 rx with 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.
Did you find what you were looking for?

Quiz

No quiz questions available for this article.

Cron Job Starts