UART · Module 6
Sample Counters, Bit Counters and the RX Shift Register
Two counters and a shift register, with every width derived from the parameters and every semantic stated before the RTL — plus the simulator-extracted trace of a byte being reassembled, and why the bit counter wrapping to zero is harmless.
Chapter 6.2 used two quantities without defining either: sample_now, and the condition "last data bit". Both come from counters, and counters are where UART receivers actually fail.
Not through arithmetic — incrementing a register is not difficult. They fail through semantics: what the zero value means, whether a comparison happens before or after an increment, whether an event fires on the tick that reaches a position or the one after it. Each of those is a one-line difference in RTL and a half-bit-interval difference on the wire.
So this chapter states every semantic before writing the RTL that implements it, derives every width from the parameters, and then checks the result against a trace extracted from simulation rather than reasoned about on paper.
1. Semantics First — the Phase Counter
Six questions, answered before any code exists. Chapter 5.2 §6 established that this is the discipline that prevents off-by-one defects, and Chapter 6.1 §4 fixed the origin these answers depend on.
| Question | Answer |
|---|---|
| What does it count? | Oversample ticks, not fabric clocks. It advances only when os_tick_i is high. |
| What resets it to zero? | The start candidate — not the accepted start. |
| Is the candidate itself tick zero? | No. Phase is loaded with 0 at the candidate; the first tick after it makes the count 1. |
| When does it increment? | On every os_tick_i while the receiver is not in S_IDLE. |
| What value means "sample here"? | The event fires on the tick that carries the count to CENTER, so the comparison is against CENTER - 1. |
| What causes it to wrap? | Reaching OVERSAMPLE - 1; the next tick returns it to 0. |
The fifth row is the one that decides correctness, and it is worth restating as Chapter 5.3 did: comparing against CENTER rather than CENTER - 1 places every sample one tick late — 1/M of a bit interval, 6.25% at M = 16, applied to every sample in every frame.
candidate phase loaded with 0
tick 1 count = 1
tick 2 count = 2
...
tick 7 count = 7 <-- comparison matches here (CENTER - 1 = 7)
tick 8 count = 8 <-- the sample event fires ON this tickThe event fires on tick 8, which is M/2 ticks after the candidate — the centre of the start interval, 0.5 UI in.
2. Deriving the Phase Counter's Width
Not assuming four bits because sixteen is conventional:
// The counter holds values 0 .. OVERSAMPLE-1, so its largest value is
// OVERSAMPLE-1 and it needs ceil(log2(OVERSAMPLE)) bits.
// The guard exists because $clog2(1) is 0, and a zero-width vector is
// illegal — a degenerate case that only appears when someone parameterises
// OVERSAMPLE to 1 to "turn off oversampling".
localparam int unsigned PH_W = (OVERSAMPLE <= 1) ? 1 : $clog2(OVERSAMPLE);OVERSAMPLE | largest value | PH_W | range the register holds | exact fit? |
|---|---|---|---|---|
| 8 | 7 | 3 | 0–7 | yes |
| 16 | 15 | 4 | 0–15 | yes |
| 32 | 31 | 5 | 0–31 | yes |
| 13 | 12 | 4 | 0–15 | no — holds more than it uses |
The last row is the reason the wrap must be an explicit comparison rather than natural overflow:
// CORRECT for any OVERSAMPLE — the wrap is stated, not inherited.
phase_q <= (phase_q == PH_W'(OVERSAMPLE - 1)) ? '0 : phase_q + 1'b1;
// WRONG for any non-power-of-two OVERSAMPLE — relies on the register
// wrapping at 2**PH_W, which equals OVERSAMPLE only by coincidence.
phase_q <= phase_q + 1'b1;At M = 16 both forms behave identically, which is exactly why the second survives review. At M = 13 the second counts to 15 and the frame stretches by three ticks per interval — an error that accumulates linearly and presents as a baud-rate problem.
3. The Sample Event
One named signal, produced once:
// THE contract of Chapter 6.1 §6: one cycle, at an interval centre,
// carrying no information about WHICH interval. The FSM supplies that.
assign sample_now = os_tick_i && (phase_q == PH_W'(CENTER - 1));CENTER is derived, never written as a literal:
localparam int unsigned CENTER = OVERSAMPLE / 2;Chapter 5.3 §6 showed the defect that a hard-coded 7 produces: correct at M = 16 and silently wrong at every other factor, with no compile-time complaint. The same argument applies to every position in this chapter.
Because sample_now includes os_tick_i in its own definition, the FSM's outer else if (os_tick_i) guard in Chapter 6.2 §5 is not logically required — it is there to keep all tick-rate behaviour visibly under one condition.
4. The Bit Counter — Deciding What Zero Means
Three defensible conventions, and the whole module depends on choosing one:
| Convention | bit_idx_q == 0 means | Increment relative to store |
|---|---|---|
| A | data bit 0 is about to be sampled | store, then increment |
| B | data bit 0 was just stored | increment, then store |
| C | zero data bits received so far | equivalent to A, phrased as a count |
This receiver uses A, which is also C — they are the same register with two descriptions, and the count phrasing is often clearer when reasoning about the terminal condition.
bit_idx_q is the index of the data bit ARRIVING NOW.
Equivalently: the number of data bits already stored.Both sentences are true simultaneously, and that is not a coincidence — if n bits are stored, the next to arrive is index n.
The consequence for the terminal comparison is direct and is where convention B would differ:
// The LAST data bit is the one being stored on THIS edge, so the test is
// against DATA_W-1 rather than DATA_W. bit_idx_q still holds the index of
// the bit arriving now; its increment on this same edge is concurrent and
// does not affect this comparison.
if (bit_idx_q == BI_W'(DATA_W - 1)) begin ... endUnder convention B the same test would be bit_idx_q == DATA_W, and a design that documents A while testing for DATA_W receives one bit too many — the parity interval is stored as a data bit and everything after it is displaced. That defect produces correct-looking data for PARITY_NONE and corruption only when parity is enabled, which is a genuinely confusing signature.
5. Deriving the Bit Counter's Width, and the Wrap That Looks Like a Bug
localparam int unsigned BI_W = (DATA_W <= 1) ? 1 : $clog2(DATA_W);DATA_W | largest index | BI_W | range held | at the last increment |
|---|---|---|---|---|
| 5 | 4 | 3 | 0–7 | stops at 5 |
| 7 | 6 | 3 | 0–7 | stops at 7 |
| 8 | 7 | 3 | 0–7 | wraps to 0 |
| 9 | 8 | 4 | 0–15 | stops at 9 |
At the common DATA_W = 8, the counter is three bits, holds 0–7, and 7 + 1 wraps to zero. In the simulator trace of §7 this is directly visible: bit_idx_q reads 0 in the PARITY and STOP rows.
6. The Shift Register and LSB-First Reconstruction
Chapter 3.2 established the requirement: the payload arrives least-significant bit first.
The architectural consequence is that the receiver needs a deterministic map from arrival index to bit position, and there are two ways to build it.
Indexed write — place each bit where it belongs as it arrives:
// Conceptual SystemVerilog — correct, and not what this receiver uses.
shreg_q[bit_idx_q] <= rx_sync_q;Right shift with MSB insert — let position emerge from the shifting:
// Synthesizable SystemVerilog — what this receiver uses.
shreg_q <= {rx_sync_q, shreg_q[DATA_W-1:1]};Both reconstruct the byte correctly. They are not equivalent in hardware:
| Indexed write | Right shift | |
|---|---|---|
| Logic per bit | a decoder from bit_idx_q to a write enable | a fixed wire from bit n+1 to bit n |
Scales with DATA_W as | decoder grows | unchanged |
| Needs the bit counter? | yes — for the write address | no — only the FSM needs it |
| Partial byte on early abort | bits land in final positions | byte is right-justified only when complete |
The shift form is chosen because it removes the counter from the datapath entirely. bit_idx_q then exists only to tell the FSM when the field ends — which is what makes §5's wrap harmless, and what makes the datapath branch-free.
The direction is the part to get right. Shifting right and inserting at the MSB means the first-arrived bit is pushed steadily toward bit 0, arriving there exactly when the eighth bit is inserted. Shifting left and inserting at the LSB reconstructs the byte reversed, and §9 shows what that looks like.
7. The Trace, Extracted From Simulation
This table was printed by a compiled design under simulation — not derived on paper. Frame 0xA6, 8E1, M = 16.
| tick from candidate | position | phase_q at sample | state | bit_idx_q | sampled bit | shreg_q after |
|---|---|---|---|---|---|---|
| 8 | 0.500 UI | 7 | S_START | 0 | 0 | 0x00 |
| 24 | 1.500 UI | 7 | S_DATA | 0 | 0 | 0x00 |
| 40 | 2.500 UI | 7 | S_DATA | 1 | 1 | 0x80 |
| 56 | 3.500 UI | 7 | S_DATA | 2 | 1 | 0xC0 |
| 72 | 4.500 UI | 7 | S_DATA | 3 | 0 | 0x60 |
| 88 | 5.500 UI | 7 | S_DATA | 4 | 0 | 0x30 |
| 104 | 6.500 UI | 7 | S_DATA | 5 | 1 | 0x98 |
| 120 | 7.500 UI | 7 | S_DATA | 6 | 0 | 0x4C |
| 136 | 8.500 UI | 7 | S_DATA | 7 | 1 | 0xA6 |
| 152 | 9.500 UI | 7 | S_PARITY | 0 | 0 | 0xA6 |
| 168 | 10.500 UI | 7 | S_STOP | 0 | 1 | 0xA6 |
Four things this table proves that prose cannot:
phase_q reads 7 at every single sample. One counter, one comparison, every field — exactly Chapter 6.1 §4's claim, now observed.
The positions are 0.5, 1.5, … , 10.5 UI. Every sample at an interval centre, including the start qualification and the stop validation.
The sampled-bit column is 0,1,1,0,0,1,0,1 — which is 0xA6 = 1010_0110 read least-significant bit first. The wire order matches Chapter 3.2 exactly.
bit_idx_q reads 0 in the last two rows. The §5 wrap, observed, and harmless.
LSB-first reconstruction of 0xA6
11 cycles8. The Complete Datapath RTL
// Synthesizable SystemVerilog — the receiver's counters and shift register.
// The FSM that supplies `state_q` is Chapter 6.2; the parity accumulator is
// shown here because it shares the store event, but its MEANING is 6.4.
localparam int unsigned CENTER = OVERSAMPLE / 2;
localparam int unsigned PH_W = (OVERSAMPLE <= 1) ? 1 : $clog2(OVERSAMPLE);
localparam int unsigned BI_W = (DATA_W <= 1) ? 1 : $clog2(DATA_W);
initial begin
// Elaboration-time legality. These are not defensive programming: each
// marks a parameter value for which the ARITHMETIC ABOVE is undefined.
if (OVERSAMPLE < 2)
$fatal(1, "uart_rx: OVERSAMPLE = %0d has no centre to sample", OVERSAMPLE);
if (DATA_W < 1)
$fatal(1, "uart_rx: DATA_W = %0d is not a frame", DATA_W);
end
logic [PH_W-1:0] phase_q;
logic [BI_W-1:0] bit_idx_q;
logic [DATA_W-1:0] shreg_q;
logic par_acc_q;
assign sample_now = os_tick_i && (phase_q == PH_W'(CENTER - 1));
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
phase_q <= '0;
bit_idx_q <= '0;
shreg_q <= '0;
par_acc_q <= 1'b0;
end else if (state_q == S_IDLE) begin
if (start_cand) begin
// Everything the frame needs is initialised HERE, at the origin.
// This is why §11 can say the reset values are never observed.
phase_q <= '0;
bit_idx_q <= '0;
par_acc_q <= 1'b0;
end
end else if (os_tick_i) begin
// Explicit wrap — §2 shows why natural overflow is wrong for any
// OVERSAMPLE that is not a power of two.
phase_q <= (phase_q == PH_W'(OVERSAMPLE - 1)) ? '0 : phase_q + 1'b1;
if (sample_now && state_q == S_DATA) begin
// Right shift, MSB insert: §6. No branch, no write address, and
// no dependence on bit_idx_q — which is what makes the counter's
// wrap in §5 unobservable.
shreg_q <= {rx_sync_q, shreg_q[DATA_W-1:1]};
par_acc_q <= par_acc_q ^ rx_sync_q;
bit_idx_q <= bit_idx_q + 1'b1;
end
end
endStore and increment are concurrent, not ordered. Both are non-blocking assignments on the same edge, so the shift uses the old shreg_q and the FSM's terminal test uses the old bit_idx_q. That is the convention A of §4 working: there is no "store then increment" sequence to get wrong because there is no sequence at all. A design using blocking assignments here would introduce one, and with it the ordering bug that convention B invites.
shreg_q is not cleared at frame start. It does not need to be — eight stores replace all eight bits — and clearing it would suggest a partial byte is meaningful, which §6's table says it is not. The reset value exists only for simulation determinism.
9. What Goes Wrong, and What It Looks Like
Four defects, each a one-line change, each with a distinct signature.
| Defect | The line | Symptom |
|---|---|---|
| Sample one tick late | phase_q == CENTER | Every sample 1/M UI late — 6.25% at M = 16. Works on a good link, fails first at the frame's end under mismatch. |
| Natural wrap | phase_q <= phase_q + 1 | Correct at power-of-two M, frame stretches at any other. Presents as a baud-rate error. |
| Terminal off-by-one | bit_idx_q == DATA_W | One extra bit received. Correct with PARITY_NONE, corrupt with parity enabled. |
| Wrong shift direction | {shreg_q[DATA_W-2:0], rx_sync_q} | Every byte bit-reversed. |
The last deserves its own numbers, because the choice of test pattern decides whether it is visible at all:
| Transmitted | Reversed | Detected by a byte comparison? |
|---|---|---|
0x53 | 0xCA | yes |
0xA6 | 0x65 | yes |
0x01 | 0x80 | yes |
0x55 | 0xAA | yes — but reverses into another test value |
0xFF | 0xFF | no — palindromic |
0x00 | 0x00 | no — palindromic |
A testbench that checks only 0x00 and 0xFF cannot detect a reversed shift register. Those are the two patterns most likely to be written first, because they are the easiest to reason about. 0x53 and 0x01 are the ones that earn their place, and Chapter 6.6 §8 explains what each pattern in the standard set is for.
10. Verification
Verify the datapath without timing. Chapter 6.1's partition allows driving sample_now and rx_sync_q directly with the FSM held in S_DATA, then checking shreg_q after DATA_W stores. Bit ordering is provable in DATA_W cycles rather than a frame time, and across all 256 byte values in well under a millisecond of simulation.
Check the trace, not just the result. A byte-level comparison confirms the final value and says nothing about how it was assembled. Recording shreg_q at each store — the 00, 80, C0, 60, 30, 98, 4C, A6 of §7 — distinguishes a correct reconstruction from one that is right by symmetry.
Sweep OVERSAMPLE including a non-power-of-two. 13 is a good choice precisely because nothing sensible uses it: it fails on exactly the natural-wrap defect and passes everything else.
Sweep DATA_W across the counter-width boundary. 8 wraps; 7 and 9 do not. A receiver correct only at DATA_W = 8 is usually one that assumed the wrap either could not happen or must be prevented.
// Assertion — the bit index never exceeds the last data index WHILE
// the index is meaningful. The S_DATA qualifier is essential: §5's wrap
// makes the value meaningless, not illegal, once the state has left.
property p_bit_idx_in_range;
@(posedge clk) disable iff (!rst_n)
(state_q == S_DATA) |-> (bit_idx_q <= BI_W'(DATA_W - 1));
endproperty
assert property (p_bit_idx_in_range);
// Assertion — the phase counter never exceeds its configured modulus.
property p_phase_in_range;
@(posedge clk) disable iff (!rst_n)
phase_q <= PH_W'(OVERSAMPLE - 1);
endproperty
assert property (p_phase_in_range);
// Assertion — exactly DATA_W stores occur between frame start and the
// exit from S_DATA. This is the one that catches the terminal off-by-one.
property p_exact_store_count;
@(posedge clk) disable iff (!rst_n)
$rose(state_q == S_DATA) |-> ##[1:$] (state_q != S_DATA)
&& (bit_idx_q == '0); // wrapped exactly once at DATA_W = 8
endpropertyThe second property is worth noting for what it is not: at OVERSAMPLE = 13 the register can physically hold 15, so this asserts a design intent that the width alone does not guarantee. That is the correct use of an assertion — stating a contract the structure permits violating.
11. Reset
Only state_q and the synchroniser genuinely need reset values. Every register in §8 is reinitialised on the start_cand that begins a frame, so their reset values are never observed by any frame the receiver actually delivers.
They are reset anyway, for two reasons that are worth separating from correctness. Simulation determinism: an unreset register is X in RTL simulation, and X propagating through a comparison produces failures that look like logic errors. Cost: on both FPGA and ASIC flows a reset on these registers is free or nearly so at this scale.
What must not happen is resetting them to values that imply a frame is in progress. shreg_q <= '0 is fine because nothing reads a partial byte; state_q <= S_DATA would be a disaster. Chapter 5.1's rule for the synchroniser — reset to the idle level, not to zero — is the same principle applied where it does have observable consequences.
12. What This Means on an FPGA
The counters are the smallest thing in the design and the easiest to get wrong. Four bits and three bits at the common parameters. No part of this warrants a manual optimisation, and the derived widths cost nothing over hard-coded ones.
$clog2 is evaluated at elaboration on every mainstream tool, so the derived widths carry no runtime cost. The guard against $clog2(1) == 0 matters because a zero-width vector is an elaboration error in some tools and a silently-one-bit vector in others — the $fatal in §8 turns both into the same clear message.
Probe sample_now and bit_idx_q during bring-up. Together they answer the first question in any receive failure: is the receiver sampling at the right rate, and does it think the right number of bits arrived? A sample_now at 16× the expected rate is the FSM-advancing-on-os_tick_i defect; a bit_idx_q reaching the wrong terminal value is the off-by-one; correct on both with wrong data is the shift direction.
The shift register maps to a plain SRL or flip-flop chain. Nothing here should infer a RAM, and if a tool reports one, DATA_W has been parameterised far beyond what a UART needs.
13. Understanding Check
14. Summary
Counters fail through semantics, not arithmetic, so every semantic is stated before its RTL.
The phase counter counts oversample ticks, loads 0 at the candidate, and fires sample_now on the tick that carries the count to CENTER — hence a comparison against CENTER - 1. Its width is $clog2(OVERSAMPLE) with a guard for the degenerate value, and its wrap must be an explicit comparison: natural overflow is correct at every power-of-two factor and stretches the frame at any other.
The bit counter means the index of the data bit arriving now, equivalently the number already stored. Its terminal test is against DATA_W - 1, and at DATA_W = 8 the final increment wraps 7 to 0 — harmless, because the FSM leaves S_DATA on the same edge and nothing downstream reads it.
The shift register is a right shift with MSB insert, chosen over an indexed write because it removes the counter from the datapath entirely, leaving it branch-free.
The simulator-extracted trace confirms all of it: phase_q reads 7 at every sample, positions land at 0.5 through 10.5 UI, the sampled bits are 0,1,1,0,0,1,0,1 — 0xA6 LSB-first — and the register walks 00, 80, C0, 60, 30, 98, 4C, A6.
Four one-line defects have four distinct signatures, and the shift-direction one is invisible to 0x00 and 0xFF because both are palindromic. 0x53 → 0xCA and 0x01 → 0x80 are the patterns that catch it.
15. What Comes Next
The receiver can now assemble a byte. It cannot yet say whether the byte is any good.
Chapter 6.4 builds the validation. It takes the parity accumulator this chapter introduced as a datapath register and gives it meaning — the running XOR, its initial value, whether the received parity interval enters it, and the comparison against the expected relation for each mode. It validates the stop condition at the sample Chapter 6.2 schedules, and it settles the question that decides whether the status is usable at all: which frame owns each flag. That is where this module's most subtle defect lives, and it is one the implementation in Chapter 6.6 had to be corrected for.
Browse the full path on the UART tutorials index. For the bit-ordering requirement this chapter implements, read back to Chapter 3.2.
Continue learning
Related tutorials
- Related topic
Data Bits and LSB-First Transmission
Three different orderings get called bit order: numerical significance, array index, and time on the wire. UART fixes only the third, and the byte is never reversed — a verified byte-to-wire walkthrough, the shift-register invariant, and how to annotate a capture without fooling yourself.
- Related topic
Parity Generation, Checking and Error Detection
One interval, one XOR reduction, and a detection guarantee with a sharp edge: parity catches every corruption that flips an odd number of protected bits and provably misses every even-numbered one — demonstrated, not asserted.
- Related topic
Frame Configurations: 8N1 and the Configuration Space
8N1 names three of the four choices a UART link depends on and omits the one most likely to be wrong. Reading the shorthand, computing what each configuration costs in intervals and line time, and why a longer frame spends timing margin as well as throughput.
- Related topic
Integer Dividers and Baud-Rate Error
The ratio is a fraction and a counter holds an integer, so rounding is a design decision with a measurable cost. Three policies, the actual rate each produces, and why the error belongs in units of a bit period rather than as a bare percentage.
Where this fits
Part of the UART curriculum.
