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:
IDLE → START → DATA → PARITY → STOP → IDLEThat 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:
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
| State | Entered when | While here, the FSM owns | Left when |
|---|---|---|---|
S_IDLE | reset, or any frame ends | nothing — watching for a candidate | start_cand |
S_START | a candidate was observed | the frame's timing origin is now fixed | first sample_now |
S_DATA | start qualified | which data bit index is arriving | sample_now and last index |
S_PARITY | last data bit stored, parity enabled | the frame's parity comparison | next sample_now |
S_STOP | last data bit (no parity) or parity checked | the frame's acceptance decision | next 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.
| # | From | To | Condition | Why it exists |
|---|---|---|---|---|
| 1 | S_IDLE | S_START | start_cand | A departure from idle is evidence worth acting on |
| 2 | S_START | S_IDLE | sample_now && rx_sync_q | False start — line back at mark at the centre |
| 3 | S_START | S_DATA | sample_now && !rx_sync_q | Start qualified |
| 4 | S_DATA | S_PARITY | sample_now && last && parity enabled | Configuration says a parity interval follows |
| 5 | S_DATA | S_STOP | sample_now && last && no parity | Configuration says it does not |
| 6 | S_PARITY | S_STOP | sample_now | Unconditional — the comparison happens, the transition does not depend on it |
| 7 | S_STOP | S_IDLE | sample_now | Unconditional — a bad stop is reported, not retried |
| 8 | any illegal | S_IDLE | always | Recovery 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.
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
// 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
endThree 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:
| # | Table | RTL assignment | Figure 1 arc |
|---|---|---|---|
| 1 | IDLE→START | if (start_cand) | ✓ |
| 2 | START→IDLE | if (rx_sync_q) | ✓ |
| 3 | START→DATA | else | ✓ |
| 4 | DATA→PARITY | else (parity on) | ✓ |
| 5 | DATA→STOP | if (PARITY_NONE) | ✓ |
| 6 | PARITY→STOP | unconditional | ✓ |
| 7 | STOP→IDLE | unconditional | ✓ |
| 8 | default→IDLE | default: | ✓ |
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 correctness — S_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.
// 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
Related tutorials
- Related topic
Why Receiving Is Harder Than Transmitting
A transmitter executes a schedule it wrote itself. A receiver must decide whether something is happening, whether it was real, where the positions are, and what value was there — four judgements from one edge on an input it does not control.
- Related topic
Data-Valid Generation and the Receiver Handshake
A receiver cannot tell the far end to wait. That one fact decides the shape of the output interface, forces a policy for the frame that arrives while the last is still held, and explains why the answer is not a FIFO.
- Related topic
Complete RX RTL Architecture
One synthesizable receiver assembled from the module's five preceding chapters — assumptions stated first, walked block by block with the invariant each maintains, then reviewed the way a reviewer would, including the defects found during its own development.
- Related topic
The TX FSM and Frame Sequencing
Five states, eight transitions, and one rule that keeps the line honest: tx_o is a register written only inside the bit-boundary branch. Includes the off-by-one that emits nine data bits, and the state table the RTL was checked against.
Where this fits
Part of the UART curriculum.
