Skip to content
VLSI Mentor

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.

QuestionAnswer
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 late1/M of a bit interval, 6.25% at M = 16, applied to every sample in every frame.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 tick

The 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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);
OVERSAMPLElargest valuePH_Wrange the register holdsexact fit?
8730–7yes
161540–15yes
323150–31yes
131240–15no — holds more than it uses

The last row is the reason the wrap must be an explicit comparison rather than natural overflow:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Conventionbit_idx_q == 0 meansIncrement relative to store
Adata bit 0 is about to be sampledstore, then increment
Bdata bit 0 was just storedincrement, then store
Czero data bits received so farequivalent 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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 ... end

Under 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
localparam int unsigned BI_W = (DATA_W <= 1) ? 1 : $clog2(DATA_W);
DATA_Wlargest indexBI_Wrange heldat the last increment
5430–7stops at 5
7630–7stops at 7
8730–7wraps to 0
9840–15stops 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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 writeRight shift
Logic per bita decoder from bit_idx_q to a write enablea fixed wire from bit n+1 to bit n
Scales with DATA_W asdecoder growsunchanged
Needs the bit counter?yes — for the write addressno — only the FSM needs it
Partial byte on early abortbits land in final positionsbyte 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 candidatepositionphase_q at samplestatebit_idx_qsampled bitshreg_q after
80.500 UI7S_START000x00
241.500 UI7S_DATA000x00
402.500 UI7S_DATA110x80
563.500 UI7S_DATA210xC0
724.500 UI7S_DATA300x60
885.500 UI7S_DATA400x30
1046.500 UI7S_DATA510x98
1207.500 UI7S_DATA600x4C
1368.500 UI7S_DATA710xA6
1529.500 UI7S_PARITY000xA6
16810.500 UI7S_STOP010xA6

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 cycles
A trace of eleven sample events during the reception of the byte A6 hexadecimal. The first event is the start qualification. The next eight events each store one data bit, arriving least significant bit first with values zero, one, one, zero, zero, one, zero, one. The shift register contents after each store progress from 00 to 80 to C0 to 60 to 30 to 98 to 4C and finally to A6 hexadecimal, at which point the complete byte is assembled. The final two events are the parity comparison and the stop validation, during which the shift register holds its value.8 data bits, LSB first8 data bits, LSB firstvalidation fieldsvalidation fieldsqualify — still spacequalify — still spacebyte complete = 0xA6byte complete = 0xA6bit_idx_q wrapped — unusedbit_idx_q wrapped — unusedfieldSTARTd0d1d2d3d4d5d6d7PARSTOPrx_sync_qbit_idx_q00123456700shreg_q000080C06030984CA6A6A6t0t1t2t3t4t5t6t7t8t9t10
Figure 1 — the shift register assembling 0xA6, one column per sample event. Columns are SAMPLE EVENTS, not fabric-clock cycles and not oversample ticks: each is one interval centre, 16 oversample ticks apart. The sampled bit enters at the most significant end and the previously-stored bits move one position toward bit 0, so the first-arrived bit reaches bit 0 exactly as the eighth is inserted.

8. The Complete Datapath RTL

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
end

Store 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.

DefectThe lineSymptom
Sample one tick latephase_q == CENTEREvery 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 wrapphase_q <= phase_q + 1Correct at power-of-two M, frame stretches at any other. Presents as a baud-rate error.
Terminal off-by-onebit_idx_q == DATA_WOne 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:

TransmittedReversedDetected by a byte comparison?
0x530xCAyes
0xA60x65yes
0x010x80yes
0x550xAAyes — but reverses into another test value
0xFF0xFFno — palindromic
0x000x00no — 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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
endproperty

The 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,10xA6 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

Where this fits

Part of the UART curriculum.