UART receiver state machine

The control state machine of a UART receiver, with its counters outside it. Priorities, Mealy outputs on arrows, a framing error and a false start.

Level
rtl
Code
uart-receiver-fsm
Checked
Verilator 5.052, Icarus Verilog 13.0, Yosys 0.66
Published
Authorship
Drafted with AI assistance
Status
In progress — published so it can be reviewed, not finished.

A UART receiver is a datapath and a small state machine that tells it what to do. The datapath counts: a baud counter raises tick in the middle of every bit on the line, and a bit counter raises last once the final data bit has been shifted in. The machine decides: wait for a start bit, check that it is real, shift in the data bits, and check the stop bit.

Its outputs are of two kinds. busy is a Moore output, high in every state but IDLE, and printed inside the states. shift, done and err are Mealy outputs: each belongs to an arrow, is printed after the / on its label, and is high while that arrow is the one that would fire.

!rxtick && !rxline 50tick && rxline 51tick && lasttick / shiftrx / doneline 61!rx / errrst_nIDLESTARTbusy=1lines 49–52, 77DATAbusy=1STOPbusy=1lines 60–68, 79

Run plays this scenario from the start; driving an input continues from here.

A state machine that runs: a state diagram and its waveform on one time axis. Inputs: rst_n, rx, tick, last. Outputs: busy, shift, done, err. clk runs at 8 ticks a period. It has 4 states, IDLE, START, DATA and STOP, and starts in IDLE on reset; 7 transitions. The waveform shows clk, rst_n, rx, tick, last, state, shift, done, err.

Figure 1. Press Run. One frame in four clock periods: a start bit, one data bit and the stop bit.

How it works, edge by edge

The reset is released at tick 2, and at tick 3 rx falls: the start bit. rx is the line itself and changes between edges; the machine sees it at the next rising edge, and at edge 0 it takes the arrow from IDLE to START on !rx.

To fit a frame into four clock periods, tick stays high from edge 0 on, as if every clock were the middle of a bit. On a real line it comes once per bit period, many clocks apart, and the machine simply waits in each state between ticks.

At edge 1 START checks the line in the middle of the start bit: it is still low, so the start bit was real and the machine enters DATA. Straight away shift goes high, a whole period before the edge that shifts: a Mealy output follows the arrow that would fire, and in DATA with tick high that is the loop that shifts a bit in. At edge 2 it does, and last rises: in this short frame that was the only data bit.

Now both conditions out of DATA are true: tick && last to STOP, and tick round the loop. The first has priority 1, so it is the one selected, and shift falls at once, a period before the edge: Mealy outputs follow the selected arrow, not every true condition. At edge 3 the machine enters STOP, where the script pauses. That edge was the middle of the stop bit, and STOP reads the line at its very next edge, still inside the stop bit. The line is high, so the arrow back to IDLE on rx is the one that would fire, and done is high.

Framing error leaves rx low through the stop bit: the other arrow from STOP fires, and err reports it on the way back to IDLE. False start lets rx go high again before the middle of the start bit; at the next tick START sees it and goes back to IDLE.

The code

uart_rx.sv
// The control state machine of a UART receiver; the counters are outside it.
// tick marks the middle of each bit, last that the final data bit is in, rx is
// the line. STOP is entered mid stop bit and reads rx at its first edge.
//
// Reset style for this module: asynchronous, active low. One style per page,
// stated up front, so nothing on the page has to be read twice.
module uart_rx (
    input  logic clk,
    input  logic rst_n,
    input  logic rx,
    input  logic tick,
    input  logic last,
    output logic busy,
    output logic shift,
    output logic done,
    output logic err
);

  typedef enum logic [1:0] {
    IDLE  = 2'd0,
    START = 2'd1,
    DATA  = 2'd2,
    STOP  = 2'd3
  } state_t;

  state_t state, next;

  // The state register: the only flip-flops in the module.
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      state <= IDLE;
    end else begin
      state <= next;
    end
  end

  // Next state, and the Mealy outputs: they belong to a transition, so they
  // are driven where the transition is chosen. Every default is assigned
  // first, so no branch can leave a latch behind.
  always_comb begin
    next  = state;
    shift = 1'b0;
    done  = 1'b0;
    err   = 1'b0;
    unique case (state)
      IDLE: begin
        if (!rx) next = START;
      end
      START: begin
        if (tick && !rx) next = DATA;
        else if (tick && rx) next = IDLE;
      end
      DATA: begin
        if (tick && last) next = STOP;  // priority 1
        else if (tick) begin  // priority 2
          next  = DATA;
          shift = 1'b1;
        end
      end
      STOP: begin
        if (rx) begin
          next = IDLE;
          done = 1'b1;
        end else if (!rx) begin
          next = IDLE;
          err  = 1'b1;
        end
      end
      default: next = IDLE;
    endcase
  end

  // Moore output: busy is a function of the state alone.
  always_comb begin
    busy = 1'b0;
    unique case (state)
      START:   busy = 1'b1;
      DATA:    busy = 1'b1;
      STOP:    busy = 1'b1;
      default: busy = 1'b0;
    endcase
  end

endmodule

The reset style is stated at the top: asynchronous, active low. The Mealy outputs are driven in the next-state block, where the arrow they belong to is chosen: each gets its default at the top, and the branch that takes the arrow sets it. busy belongs to the states alone, so it has an output block of its own. No output is driven from two blocks. The FSM viewer draws a state machine of your own from its code, its Mealy outputs on its arrows as here.

The two arrows out of DATA are an if and an else if, in the order of their priorities, which the comments on those lines record. The order is the whole of the priority: it is what makes the loop on tick the second choice.

Synthesis result

Yosys's generic synthesis turns uart_rx.sv into 21 cells, 2 of them flip-flops. The cells are Yosys's own gate library, not an FPGA's or a standard-cell library's: the count measures the logic, not the area of any chip.

The code gives state a 2-bit encoding, and these numbers keep it. Left to its fsm pass, Yosys re-encodes it one-hot: 4 flip-flops, 22 cells in all.

Cells after synth -nofsm, by type
CellWhat it isCount
$_DFFE_PN0P_D flip-flop on the rising edge, active-low asynchronous reset to 0, active-high enable2
$_AND_2-input AND5
$_ANDNOT_AND with its second input inverted, A & ~B2
$_NAND_2-input NAND3
$_NOR_2-input NOR2
$_NOT_inverter1
$_OR_2-input OR3
$_ORNOT_OR with its second input inverted, A | ~B2
$_XNOR_2-input XNOR1
Total21

Lint by Verilator 5.052, the testbench by Icarus Verilog 13.0, synthesis by Yosys 0.66.

Icarus replayed this page's three scenarios on this code, and all 1,062 values the waveform draws match.

The netlist before synthesis

The netlist of uart_rx.sv, drawn by netlistsvg 1.0.2, is 1,155 × 1,233 px, too large to read inline: open it on its own, where the browser can zoom.

Pitfalls

A Mealy output lasts as long as its condition. done and err are high for the one period the machine spends in STOP, before the edge that returns to IDLE. Whatever consumes them has to sample them at that edge, on the same clock.

Reading the stop bit a bit late. STOP is entered on the tick in the middle of the stop bit, and it reads rx at its next edge rather than waiting for another tick. Waiting would read the line a whole bit period later, which on a busy line is the next frame's start bit: a false framing error, and the start bit gone.

Priority by position. Swap the two DATA lines and the arrow to STOP can never be taken: whenever tick && last is true, tick is true too, and the loop is tried first. The machine shifts on every tick and never reaches STOP. Nothing in the syntax says the order matters.

rx is asynchronous. In the figure it changes between edges and is sampled at the next one. In silicon, a line from outside the chip goes through a synchronizer before a state machine reads it.

Check yourself

Once the last data bit is in, tick && last and tick are both true in DATA. Which arrow is taken, and what decides it?
The one to STOP. It has priority 1, and the code writes it first in the if / else if chain, so the self-loop on tick is only tried when the first condition is false. With the two lines swapped the arrow to STOP could never be taken, because whenever tick && last is true, tick is true too.
shift is high in the waveform before the edge that shifts the bit in. Why is it early?
It is not early; it is a Mealy output. It belongs to the arrow DATA to DATA and is high while that arrow is the one that would fire, which is during the period before the edge that takes it. The shift register downstream samples it at that same edge.
Why does START look at rx again at the next tick instead of going straight to DATA?
To tell a start bit from a glitch. A real start bit is still low in the middle of the bit; a spike on the line is not. False start shows the second case going back to IDLE.

Tags rtl state-machine mealy uart

Found an error? Send a correction