Skip to content
VLSI Mentor

UART · Module 6

RX Datapath and Block Partitioning

Module 5 built the receiver's timing mechanisms as separate demonstrators. Assembling them means deciding which blocks exist, what each owns, and what contract joins them — including the two oversample counters that must merge into one.

Module 5 finished with a receiver that could place a sample anywhere it wanted and decide what the line was doing at that instant. It could not hold a byte, did not know which bit was arriving, and had nothing to tell anything upstream.

Every mechanism it built arrived as a separate demonstratoruart_start_qualify, uart_os_phase, uart_sample_vote — each parameterised independently, each carrying its own counter, each correct in isolation. That was deliberate: a mechanism is easier to understand alone.

It is also not a receiver. Assembling them raises questions none of them could answer, and the first is the one Chapter 5.2 explicitly deferred: two of those blocks count oversample ticks, and a receiver must have one convention, not two.

This chapter decides the partition. It writes no frame logic — that is 6.2 onward — but it fixes the block boundaries and the contracts across them, and everything later in the module is built to this plan.

1. Start From the Contract, Not the Code

The most common way to get a receiver wrong is to write the state machine first and let the interface emerge from it. What emerges is an interface that reflects the implementation's convenience rather than the consumer's needs, and by then it is load-bearing.

So the interface comes first:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Conceptual SystemVerilog — the specification, not yet an implementation.
// Every signal here is a decision, and Chapters 6.2-6.5 justify each one.
module uart_rx #(
    parameter int unsigned DATA_W     = 8,
    parameter int unsigned OVERSAMPLE = 16
) (
    input  logic              clk,            // one fabric clock, Chapter 5.1
    input  logic              rst_n,
    input  logic              rx_i,           // asynchronous — the only such pin
    input  logic              os_tick_i,      // enable, M per bit — Module 8
    input  parity_mode_t      parity_mode_i,  // configuration, Chapter 3.5

    output logic [DATA_W-1:0] rx_data_o,      // the assembled byte
    output logic              rx_valid_o,     // a byte is available
    input  logic              rx_ready_i,     // the consumer takes it
    output logic              rx_parity_err_o,
    output logic              rx_frame_err_o,
    output logic              rx_overrun_o
);

Four things in that list are decisions rather than inevitabilities, and each is argued later:

os_tick_i is an input, not a generated clock. Chapter 5.3 §8 already rejected an os_clk: dividing the fabric clock would create a domain crossing inside the receiver for no benefit. Here it becomes structural — the receiver has one clock, and every timing decision happens under an enable.

rx_ready_i exists at all. A receiver could simply pulse rx_valid_o for one cycle and be done. Chapter 6.5 argues the choice; the interface is shown with it because the alternative changes what the other status signals can mean.

Status is per-frame, not global. Three separate outputs, each belonging to the byte currently presented. Chapter 6.4 shows what breaks when that ownership is not enforced.

Configuration is an input, not a parameter. parity_mode_i can change between frames; DATA_W cannot. That split is not arbitrary — §5 explains it.

2. The Partition

Six blocks, each with one job.

A UART receiver partitioned into six blocks. The asynchronous receive input enters a two-stage synchroniser, which produces a synchronised version of the line. An edge detector watches that synchronised signal for a departure from the idle level and produces a start candidate pulse. A sample timing block counts oversample ticks from that candidate and produces a single named sampling event. A control state machine consumes the sampling event and decides which field of the frame is currently arriving. A datapath block containing the bit counter, shift register and parity accumulator stores each sampled bit under the state machine's direction. A status and holding register block presents the assembled byte together with its parity, framing and overrun status to the consumer.Synchroniser2 stages · Ch 5.1Edge detectorstart candidate · Ch 5.2Sample timingone counter · Ch 6.3Control FSMwhich field · Ch 6.2Datapathbit idx · shreg · parityHold + statusthe interface · Ch 6.5rx_sync_qstart_candsample_nowfieldbyte + flags12
Figure 1 — the receiver's block partition. The single asynchronous input enters at the left and is resolved to a synchronous representation before anything else observes it. The timing chain produces one named event, sample_now; the frame chain consumes that event and knows nothing about oversampling. Control and datapath are separated so that the state machine decides what a sample means while the datapath decides where the bit goes.

Synchroniser. Resolves the one asynchronous input to a synchronous signal. Chapter 5.1 established two stages and reset to the idle level, so a receiver leaving reset does not present a fabricated departure. Everything downstream sees rx_sync_q and nothing sees rx_i.

Edge detector. One registered copy and a comparison, producing a one-cycle start_cand pulse on the transition from mark to space. Chapter 5.2 established that this is evidence, not a frame.

Sample timing. Counts oversample ticks and produces exactly one named event — sample_now — at the centre of each interval. This is the block §3 and §4 are about.

Control FSM. Knows which field of the frame is arriving. Consumes sample_now and the sampled level; produces the decisions the datapath acts on. Holds no data.

Datapath. Bit counter, shift register, parity accumulator. Stores what it is told to store. Decides nothing.

Holding register and status. Owns the byte the consumer sees and the three flags that belong to it.

3. Control and Datapath Are Split on Purpose

The FSM knows which field is arriving. The datapath knows where the bit goes. Neither knows the other's business, and the separation pays three times.

The FSM has no data path through it. Its next state depends on the current state, the configuration and sample_now — never on the value of a data bit. That makes the state graph finite and small, and it makes Chapter 6.2's enumeration of transitions complete rather than indicative.

The datapath has no branching. The shift register performs the same operation on every data bit. There is no "if this is the last bit" special case in the storage logic — the FSM handles lastness by changing state, and Chapter 6.3 shows that the counter wrapping at that moment is harmless precisely because of this split.

Each is verifiable on its own terms. The FSM is checked by asserting reachability and legal transitions; the datapath is checked by driving byte values and comparing. A design that mixes them has to verify their product.

4. The Two Counters Must Become One

Here is the assembly problem Module 5 left open, and it is the most consequential decision in this chapter.

Chapter 5.2's uart_start_qualify carries a counter that starts at the candidate and counts to M/2 to validate the start. Chapter 5.3's uart_os_phase carries a counter that is rephased by the accepted start and fires at each interval centre.

Instantiate both and the receiver has two counters counting the same ticks from different origins. Chapter 5.2 §4 named the hazard exactly:

a design that mixes them — counts qualification from the candidate but then schedules data from the acceptance as though it were the frame boundary — places every sample half a bit interval late.

That is not a hypothetical. It is the natural result of wiring two independently-correct blocks together without asking which origin each assumes.

The resolution is one counter with one origin, and the origin is the candidate edge:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
phase 0 is loaded at the start candidate.
A sample event fires on every oversample tick that carries the phase count
to CENTER, and the phase wraps after OVERSAMPLE ticks.

That single rule produces every sampling instant the receiver needs:

SampleFires atPosition from candidateWhat the FSM does with it
1stCENTER ticks0.5 UIqualify the start — still space?
2nd+ M1.5 UIstore data[0]
3rd+ M2.5 UIstore data[1]
9th+ M8.5 UIstore data[7]
10th+ M9.5 UIcheck parity (if enabled)
11th+ M10.5 UIvalidate stop

Every one of those lands at an interval centre, which is what Chapter 2.5 required. One counter, one comparison, one convention — and start qualification stops being a separate mechanism. It is simply what the FSM does with the first sample.

5. What Is a Parameter and What Is an Input

The receiver has configuration of two kinds, and conflating them produces either dead silicon or a receiver that cannot be reconfigured.

QuantityKindWhy
DATA_WparameterSets the shift register's width and the bit counter's width. Changing it changes the hardware.
OVERSAMPLEparameterSets the phase counter's width and the centre comparison. Changing it changes the hardware.
parity_mode_iinputSelects a path through an existing state graph. The hardware is the same either way.

The test is direct: does changing it change what gets synthesised? A quantity that alters a register's width must be elaboration-time. A quantity that selects between behaviours the hardware already contains can be runtime.

This matters because the temptation runs the other way. Making DATA_W an input would mean building the widest shift register and masking — which is legitimate for an IP that must genuinely support runtime reconfiguration (Module 13 owns that register interface), and wasteful for one that does not. Neither choice is universally right, and the tutorial receiver states which it made rather than leaving it implied.

The receiver here fixes DATA_W and OVERSAMPLE at elaboration and takes parity mode at runtime, which matches the common case of a UART configured once at boot but whose parity is set by software.

6. The Contracts Between Blocks

A partition is only as good as the interfaces it creates. Five contracts hold this one together, and every later chapter is written against them:

ContractProducerConsumerStatement
rx_sync_qsynchronisereverythingA synchronous representation of the line. No other block sees rx_i.
start_candedge detectortimingOne cycle, on the first observation of space having been mark. Evidence only.
sample_nowtimingFSM + datapathOne cycle, at an interval centre. Carries no information about which interval.
stateFSMdatapathWhich field is arriving. Carries no data.
rx_valid_o + data + flagsholding registerconsumerOne frame's worth of result, stable together.

The third is the one that makes the design work, and it is worth stating as a rule rather than a row: the timing block does not know what a sample is for. It fires at interval centres; the FSM decides whether that centre is a start, a data bit, a parity bit or a stop. That is why one counter suffices for a frame whose shape changes with configuration — the timing is identical for 8N1 and 8E2, and only the FSM's path differs.

7. Verification Consequences of the Partition

The partition decides what can be checked independently, and this is the practical payoff.

The timing block can be verified without frames at all. Drive start_cand and os_tick_i, and assert that sample_now occurs exactly every OVERSAMPLE ticks with the first at CENTER. No data, no parity, no configuration. A failure here is unambiguous.

The FSM can be verified without a line. Drive sample_now and a sampled level directly, and walk the state graph. Every configuration's path can be covered in a handful of cycles rather than a handful of frames.

The datapath can be verified without timing. Assert sample_now with chosen bit values and check the shift register. Chapter 6.3 does exactly this for bit ordering.

Only the assembled receiver needs real frames — and by then the failures that remain are integration failures, which is the class this chapter's contracts exist to make rare.

8. What This Means on an FPGA

One clock, one enable, no exceptions. Every block above is clocked by clk and gated by os_tick_i where it needs bit-rate timing. There is no second clock domain inside the receiver, so there are no internal timing exceptions to write and nothing for CDC analysis to flag except the synchroniser at the input.

The synchroniser is the only structure needing a constraint, and Module 12 owns what that constraint is. Its placement is fixed here: as close to the pin as the tools allow, and with nothing tapping rx_i in between.

The partition maps to recognisable resources. Two flip-flops for the synchroniser and one more for the edge detector; $clog2(OVERSAMPLE) for the phase counter and $clog2(DATA_W) for the bit counter; DATA_W for the shift register and DATA_W again for the holding register; two for the parity accumulator and its captured result; three for the state; four for valid and the three status flags. For DATA_W = 8 and OVERSAMPLE = 16 that totals 35 flip-flops — small enough that no part of this design should be optimised before it is correct. Chapter 6.3 derives the two counter widths rather than assuming them.

Probe the contracts, not the internals. sample_now, the state and rx_valid_o make the receiver's behaviour legible on a logic analyser, and they are the three signals §7's failure classes are distinguished by. Chapter 6.6 returns to this as a bring-up recommendation.

9. Understanding Check

10. Summary

Module 5's mechanisms were separate demonstrators, each carrying its own counter and each correct against its own contract. A receiver is not their sum; it is a partition with stated boundaries.

The interface comes before the implementation. Six ports and three status flags, each with a lifetime the consumer can rely on without reading the code — which rules out valid pulses of state-dependent duration, flags of ambiguous ownership, and data that moves while valid is high.

Six blocks: synchroniser, edge detector, sample timing, control FSM, datapath, holding register and status. Control and datapath are split so that the FSM's next state never depends on a data bit and the datapath never branches — which is what makes the state graph enumerable from the configuration and each half independently verifiable.

The two counters become one. Chapter 5.2 deferred the origin decision to this module, and the answer is: phase 0 at the candidate, a sample event on every tick carrying the count to CENTER, wrapping at OVERSAMPLE. That places samples at 0.5, 1.5, … , 10.5 UI — start qualification, eight data bits, parity and stop — from one counter and one comparison. Wiring Module 5's blocks together without settling this places every sample half an interval late.

sample_now carries no field identity, which is why the same timing serves every configuration.

Parameters change hardware; inputs select paths. DATA_W and OVERSAMPLE are elaboration-time; parity_mode_i is runtime.

11. What Comes Next

The partition names a block that decides which field is arriving and says nothing about how it decides.

Chapter 6.2 builds that state machine: the five states, the entry and exit condition of each, the configuration-dependent transition out of the last data bit, the false-start path back to idle, the behaviour on a malformed stop, and the recovery that keeps one bad frame from wedging the receiver. It specifies every transition as a table first and derives the RTL from it, so that the diagram and the code describe the same machine — which Chapter 6.6 later checks by enumeration.

Browse the full path on the UART tutorials index. For the sampling mechanisms this partition assembles, read back to Chapter 5.3 and Chapter 5.4.

Continue learning

Where this fits

Part of the UART curriculum.