Skip to content
VLSI Mentor

UART · Module 6

The RX FSM

Five states, eight transitions, and a rule that keeps them enumerable: the next state depends on the current state, the configuration and one timing event — never on a data bit. Specified as a table first, with the RTL derived from it and checked against it.

Chapter 6.1 named a block that knows which field of the frame is arriving and deliberately said nothing about how it decides. This chapter builds it.

A UART receive FSM is a small machine — five states — and it is routinely drawn as a ring:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
IDLE → START → DATA → PARITY → STOP → IDLE

That picture is not wrong, and it is not an architecture. It omits every transition that makes the receiver robust: how DATA knows it is finished, how a configuration without parity skips a state, what happens when qualification fails, what happens when the stop condition is malformed, and what the machine does if it ever finds itself in a state it should not be in.

This chapter specifies all of them as a table first, then derives the RTL. That order matters: Chapter 6.6 later checks the diagram and the code against each other by enumeration, which is only possible if they came from one specification.

1. The Rule That Makes the Graph Finite

Before the states, the invariant they are designed around:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
next state = f(current state, configuration, sample_now)

Never a data bit. Chapter 6.1 §3 argued the consequence — a payload-dependent transition makes the reachable graph a function of what arrives rather than of the configuration, so it can no longer be enumerated, and state coverage stops implying behavioural coverage.

There is one apparent exception worth naming immediately, because it looks like a violation and is not. The transition out of START does depend on the sampled level: if the line has returned to mark, the candidate was false. But that level is not payload — it is the qualification result, a field whose only purpose is to be tested. The distinction is whether the bit carries information for the consumer. A start bit does not; a data bit does.

Everything else in the machine tests only state, configuration and timing.

2. The Five States

StateEntered whenWhile here, the FSM ownsLeft when
S_IDLEreset, or any frame endsnothing — watching for a candidatestart_cand
S_STARTa candidate was observedthe frame's timing origin is now fixedfirst sample_now
S_DATAstart qualifiedwhich data bit index is arrivingsample_now and last index
S_PARITYlast data bit stored, parity enabledthe frame's parity comparisonnext sample_now
S_STOPlast data bit (no parity) or parity checkedthe frame's acceptance decisionnext sample_now

Two observations that a ring diagram hides.

S_IDLE is the only state that does not wait for sample_now. It waits for a candidate, which is a combinational edge detection on the synchronised line. Every other state advances only on the timing event, which is what keeps the frame on the grid Chapter 6.1 §4 established.

S_START does not "wait for the start bit to finish". It waits for the first sample, which lands at the start interval's centre — 0.5 UI in. The machine leaves S_START halfway through the start bit, not at its end, and that is exactly right: the sample at 0.5 UI is the qualification, and the next sample at 1.5 UI is data[0]. A machine that waited for the start interval to end would have to know where the end is, which costs a second comparison and a second convention.

3. Every Transition

Eight transitions. This table is the specification; §5's RTL is derived from it and §6 checks the derivation.

#FromToConditionWhy it exists
1S_IDLES_STARTstart_candA departure from idle is evidence worth acting on
2S_STARTS_IDLEsample_now && rx_sync_qFalse start — line back at mark at the centre
3S_STARTS_DATAsample_now && !rx_sync_qStart qualified
4S_DATAS_PARITYsample_now && last && parity enabledConfiguration says a parity interval follows
5S_DATAS_STOPsample_now && last && no parityConfiguration says it does not
6S_PARITYS_STOPsample_nowUnconditional — the comparison happens, the transition does not depend on it
7S_STOPS_IDLEsample_nowUnconditional — a bad stop is reported, not retried
8any illegalS_IDLEalwaysRecovery from an unreachable encoding

Plus three self-loops that are the absence of a transition rather than a transition: S_IDLE holds while no candidate, S_START holds until the first sample, and S_DATA holds while sample_now arrives with a non-final index.

fsm
A five-state UART receive state machine. From Idle, a start candidate moves the machine to Start. In Start, the first sampling event tests the line: if it is still at the space level the machine advances to Data, and if it has returned to the mark level the candidate was false and the machine returns to Idle without committing. In Data, each sampling event stores one bit and the machine remains in Data until the final data index, at which point the configured parity mode selects the next state: Parity when parity is enabled, or Stop directly when it is not. From Parity the next sampling event moves unconditionally to Stop, the parity comparison having been recorded rather than affecting the transition. From Stop the next sampling event moves unconditionally back to Idle, so a malformed stop is reported through status rather than retried. An illegal state encoding also returns to Idle.S_IDLES_STARTS_DATAS_PARITYS_STOPstart_candstart_candsample: still spacesample: still spacesample: back at marksample: back at marksample: store bitsample: store bitlast + parity onlast + parity onlast + parity offlast + parity offsamplesamplesample: alwayssample:always
Figure 1 — the complete receive state graph. Every arc shown corresponds to exactly one transition in the table above and to exactly one state_q assignment in §5's RTL; the enumeration in §6 confirms the correspondence. The two arcs leaving S_DATA differ only in the configured parity mode, and the arc from S_START back to S_IDLE is the false-start path that a ring diagram omits.

4. The Three Transitions Implementations Get Wrong

Transition 4/5 — the configuration branch. This is the only place in the receiver where the parity mode affects control flow, and putting the decision anywhere else duplicates the frame shape. A receiver that instead counts "data bits plus parity" in one counter and compares against a configuration-dependent total has the same information spread across a counter width, a comparison constant and a state; changing DATA_W then requires all three to move together. Branching once, at one point, keeps the frame shape in one place.

Transition 6 — parity does not change the path. The parity comparison produces a status bit, and the machine advances to S_STOP whether it matched or not. The temptation is to abort the frame on a parity error, and it is wrong for a reason worth stating: the receiver has no idea what the consumer wants. A logging application wants the byte with its error flag; a command interface wants to discard it. Making that decision in the FSM removes the choice from the layer that can actually make it. Chapter 6.4 develops the policy; the FSM's job is to finish the frame and report.

Transition 7 — a bad stop is not retried. This is the one that produces wedged receivers. The tempting behaviour is to stay in S_STOP until the line returns to mark, on the reasoning that the frame is not really over until it does.

Transition 8 — the default. A five-state machine in a three-bit encoding has three unreachable codes. They should be unreachable, and a single-event upset or an incompletely-reset register can produce one anyway. default: state_q <= S_IDLE costs nothing, is optimised away by synthesis if the tool can prove unreachability, and turns a permanent hang into a lost frame if it cannot.

5. The RTL, Derived From the Table

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Synthesizable SystemVerilog — the receive state machine.
// sample_now and the bit index come from Chapter 6.3; the datapath the
// FSM directs is also 6.3; status capture is 6.4. This is control only.
typedef enum logic [2:0] {
    S_IDLE   = 3'd0,
    S_START  = 3'd1,
    S_DATA   = 3'd2,
    S_PARITY = 3'd3,
    S_STOP   = 3'd4
} rx_state_e;

rx_state_e state_q;

always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        state_q <= S_IDLE;                       // §7: the only safe reset state
    end else if (state_q == S_IDLE) begin
        // The one state that does NOT wait for the timing event.
        if (start_cand) state_q <= S_START;      // transition 1
    end else if (os_tick_i) begin
        if (sample_now) begin
            case (state_q)
                S_START: begin
                    // The sampled level IS the qualification result, which is
                    // why this transition may test it — §1.
                    if (rx_sync_q) state_q <= S_IDLE;    // transition 2
                    else           state_q <= S_DATA;    // transition 3
                end
                S_DATA: begin
                    if (bit_idx_q == BI_W'(DATA_W - 1)) begin
                        // The ONLY place configuration steers control flow.
                        if (parity_mode_i == PARITY_NONE) state_q <= S_STOP;    // 5
                        else                              state_q <= S_PARITY;  // 4
                    end
                    // else: hold in S_DATA — the self-loop, written as absence
                end
                S_PARITY: state_q <= S_STOP;     // transition 6 — unconditional
                S_STOP:   state_q <= S_IDLE;     // transition 7 — unconditional
                default:  state_q <= S_IDLE;     // transition 8 — recovery
            endcase
        end
    end
end

Three details are deliberate.

A single always_ff, not the two-process state_d/state_q pattern. The two-process form is a good default when next-state logic is large or shared, and here it would add a combinational block whose only content is this same case statement. The one-process form has no latch risk — every path either assigns or holds a register — and keeps the transition list in one place, which is what makes §6's enumeration meaningful. This is a judgement, not a rule: a machine with substantially more next-state logic is better split.

case rather than unique case. unique asserts that exactly one branch matches and that a branch always matches. The second half is false here by construction — default exists precisely to catch encodings the enumeration does not cover — so unique would either be redundant with default or, in tools that treat it as a synthesis directive, license the optimisation away of the recovery path this section just argued for. Chapter 6.6 returns to when unique is the right choice.

The os_tick_i guard is outer, sample_now inner. sample_now already implies os_tick_i (Chapter 6.3 defines it that way), so the outer guard is not logically required. It is there because the same else if (os_tick_i) branch carries the phase counter's increment, and keeping all tick-rate behaviour under one condition makes the enable structure visible to a reviewer and to synthesis.

6. Diagram and RTL Must Be the Same Machine

§3's table has eight transitions. §5's RTL has eight state_q assignments. Figure 1 has eight arcs. That correspondence is checkable mechanically, and it was:

#TableRTL assignmentFigure 1 arc
1IDLE→STARTif (start_cand)
2START→IDLEif (rx_sync_q)
3START→DATAelse
4DATA→PARITYelse (parity on)
5DATA→STOPif (PARITY_NONE)
6PARITY→STOPunconditional
7STOP→IDLEunconditional
8default→IDLEdefault:

This is not a formality. The common failure is a diagram drawn early, an RTL change made later, and no one reconciling them — after which the diagram is worse than no diagram, because reviewers trust it. Extracting the assignments from the source and counting them takes seconds and is the kind of check worth automating in a repository where the diagram is a maintained artefact.

7. Reset and Mid-Frame Reset

S_IDLE is the only defensible reset state. Any other leaves the receiver believing a frame is in progress that is not, and it will publish a fabricated byte built from whatever the line does next.

The counters and the shift register do not need a reset value for correctnessS_IDLE entry reinitialises them at the next candidate, so their reset values are never observed. They are reset anyway in Chapter 6.6's assembled module, for simulation determinism and because the cost is nil. What genuinely requires reset is the state, the synchroniser (to the idle level, per Chapter 5.1) and the output valid.

Reset during a frame is where the interesting behaviour is. The in-flight byte is discarded and the machine returns to S_IDLE — that part is straightforward. What is not:

8. Verification

Drive the FSM without a line. The partition of Chapter 6.1 makes this possible: supply sample_now and a sampled level directly and walk the graph. Every configuration's path is a handful of cycles, so all of them can be covered exhaustively rather than sampled.

Cover every transition, not every state. State coverage is satisfied by one clean frame; transitions 2, 7-with-bad-stop and 8 are not exercised by any clean frame at all. A coverage model built on states will read 100% while three of the eight arcs have never fired.

Transition 8 needs forcing. It is unreachable by construction, so it is only reachable in simulation by driving the state register directly. It is worth doing once: the check is that the machine returns to S_IDLE and then receives normally, which proves the recovery path is connected rather than merely present.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Assertion — the machine never rests outside the enumeration.
property p_state_legal;
    @(posedge clk) disable iff (!rst_n)
        state_q inside {S_IDLE, S_START, S_DATA, S_PARITY, S_STOP};
endproperty
assert property (p_state_legal);

// Assertion — no state but IDLE advances without the timing event.
property p_advance_only_on_sample;
    @(posedge clk) disable iff (!rst_n)
        (state_q != S_IDLE && !sample_now) |=> $stable(state_q);
endproperty
assert property (p_advance_only_on_sample);

// Assertion — S_PARITY is entered only when the configuration says so.
property p_parity_state_requires_parity;
    @(posedge clk) disable iff (!rst_n)
        (state_q == S_PARITY) |-> (parity_mode_i != PARITY_NONE);
endproperty
assert property (p_parity_state_requires_parity);

The second is the one that would have caught the two-counter hazard of Chapter 6.1 §4 in a different form: a machine advancing on os_tick_i rather than sample_now walks the frame sixteen times too fast, and this property fires on the first frame.

9. What This Means on an FPGA

Five states in three bits, or one-hot — let the tool decide. At this size the encoding is not a decision worth making by hand; both fit in the same slice count. What is worth ensuring is that default survives, and a full_case directive or an over-eager unique is what removes it.

The FSM is the single most useful probe. Three bits on a logic analyser make the receiver's behaviour legible: a machine that never leaves S_IDLE has a detection problem, one that reaches S_START and returns is rejecting qualification, and one that cycles correctly while data is wrong has a datapath problem. That narrows Chapter 6.6's debugging table to a third of its rows before any other signal is examined.

Watch for a state register that is not reset. On an FPGA the initial value comes from the bitstream and the machine starts in S_IDLE whether or not rst_n is connected — which means a missing reset connection is invisible until the design is ported to an ASIC flow or until the first mid-traffic reset. The assertion in §8 does not catch it either. Only a directed mid-frame reset test does.

10. Understanding Check

11. Summary

The receive FSM is five states and eight transitions, and it is built around one rule: next state = f(state, configuration, sample_now)never a data bit. The transition out of S_START tests the sampled level and is not an exception, because a start bit carries no information for the consumer; it exists to be tested.

S_IDLE is the only state that does not wait for the timing event, and S_START leaves at the start interval's centre, not its end — which is what lets one counter and one comparison serve qualification and every data sample alike.

Three transitions carry the design's real content. The configuration branch out of the last data bit is the only place parity mode affects control flow. Parity does not change the path — it produces status, because the receiver cannot know whether the consumer wants a corrupt byte. The stop transition is unconditional, because a machine that waits for the line to return to mark is permanently disabled by a break.

The default transition costs nothing and converts a hang into a lost frame. unique case is the wrong tool here precisely because it can license removing it.

The diagram, the table and the RTL were checked against each other by enumeration — eight arcs, eight rows, eight state_q assignments.

And a receiver released from reset onto a live line fabricates a byte: simulation produced 0xFA from an interrupted 0xA6. Correct, unavoidable without idle detection, and a trap for testbenches.

12. What Comes Next

This chapter used two quantities without defining either: sample_now, and the condition "last data bit".

Chapter 6.3 derives both. It builds the single phase counter Chapter 6.1 §4 specified, derives its width from the parameters rather than assuming sixteen, states precisely what phase zero means and on which tick the sample event fires, and then builds the bit counter and the shift register that reassemble the byte. It settles the bit-index convention the whole module depends on, shows the LSB-first reconstruction against a concrete byte traced out of the simulator, and explains why the bit counter wrapping to zero at the end of the frame is harmless rather than a bug waiting to happen.

Browse the full path on the UART tutorials index. For the frame shapes whose configuration drives transitions 4 and 5, read back to Chapter 3.5.

Continue learning

Where this fits

Part of the UART curriculum.