Skip to content
VLSI Mentor

UART · Module 8

Integer Divider RTL

The counter that turns a fabric clock into a bit-interval enable — terminal count derived rather than asserted, width from the parameters, and a runtime-programmable divisor with a commit policy that cannot corrupt an interval in flight.

Chapter 4.3 did the arithmetic. It chose between floor, ceiling and nearest divisors, computed the resulting rate error, and published a working divider — explicitly labelled as the elaboration-time version, with a note naming what this chapter owns:

The production generator — runtime reconfiguration, separate RX/TX timing, error reporting — is Module 8.

So this chapter does not re-derive the divisor. It builds the block: the counter's semantics stated before its code, the terminal count derived, the width taken from the parameters, and the runtime-programmable divisor that a UART whose rate is set by software actually needs — together with the commit policy that keeps a rate change from corrupting the interval in progress.

1. Counter Semantics, Before Any Code

Six questions, following the discipline Chapter 6.3 §1 established for the receiver's counters.

QuestionAnswer
What does it count?Fabric clock cycles, every cycle, unconditionally.
What is its range?0 … N−1, where N is the divisor.
What resets it to zero?Reset, and reaching its terminal value.
Which value is terminal?N−1, not N — §2.
How many cycles per output tick?Exactly N.
When is the tick observable?One cycle after the terminal value, because the tick is registered — §5.

The fourth and fifth rows are the same statement seen twice, and the second form is the one to hold on to: a counter walking 0 … N−1 and wrapping takes N cycles per lap. Everything else follows.

2. The Off-By-One, Derived

The tempting comparison is against the divisor itself. Here is what each produces, walked edge by edge at N = 4:

edgecnt before== N−1 (3)?tickcnt aftercnt before== N (4)?tickcnt after
10no10no1
21no21no2
32no32no3
43yestick03no4
50no14yestick0
61no20no1
72no31no2
83yestick02no3

Correct form: ticks at edges 4 and 8 — spacing 4 = N. Wrong form: ticks at edges 5 and 10 — spacing 5 = N + 1.

The counter that compares against N visits N + 1 distinct values, so every bit interval is one fabric cycle too long. At 100 MHz and 115,200 baud:

divisoractual bauderrormagnitude
868 — correct115,207.3733+0.0064 %64 ppm
869 — off-by-one115,074.7986−0.1087 %1,087 ppm

Seventeen times worse, and in the opposite direction. The sign flip matters: a design tuned against a far end that was slightly slow now compounds rather than cancels. And 869 is exactly the ceiling divisor from Chapter 4.3 §1 — so the bug silently substitutes a different rounding policy, which is why the symptom looks like a configuration choice rather than a defect.

Terminal count and tick, N = 4

11 cycles
A trace of eleven fabric clock cycles from an integer baud divider configured with a divisor of four. The counter holds zero, one, two and three in successive cycles then wraps back to zero, repeating every four cycles. The baud tick output is high for exactly one cycle at edges four and eight, which is one cycle after the counter showed its terminal value of three, because the tick output is registered. The spacing between successive ticks is exactly four cycles, matching the divisor.one bit interval — exactly N cyclesone bit interval — exactly N cyclesterminal valueterminal valuetick — registered, one cycle latertick — registered, onecycle laterspacing = 4 = Nspacing = 4 = Nclkcnt_q01230123012cnt_q == N-1baud_tick_ot0t1t2t3t4t5t6t7t8t9t10
Figure 1 — the divider at N = 4, extracted from simulation. Columns are FABRIC CLOCK CYCLES; the values shown are what each register holds entering that edge. The counter walks 0 to 3 and wraps, giving four cycles per lap. The tick appears one cycle after the counter shows its terminal value because baud_tick_o is registered — a latency paid once at start-up, not per interval: successive ticks are exactly four cycles apart.

3. Counter Width

Derived from the parameters, never written as a convenient [31:0]:

Nmax value (N−1)$clog2(N)register holdsnote
100$clog2(1) is 0; a zero-width vector is illegal — guard to 1
4320–3exact power of two
43443390–511
868867100–1023
10,41710,416140–16,383
65,53665,535160–65,535exact power of two
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------------
//  8.2 — integer divider, production form: runtime-programmable divisor with
//  a safe update policy. Chapter 4.3 published the elaboration-only version.
// ---------------------------------------------------------------------------
module uart_baud_tick_int #(
    parameter int unsigned CLK_HZ  = 100_000_000,
    parameter int unsigned BAUD_HZ = 115_200,
    // Widest runtime divisor the counter must hold. Derived by the integrator
    // from the slowest rate required, not guessed.
    parameter int unsigned DIV_MAX = 65_536
) (
    input  logic clk,
    input  logic rst_n,
    input  logic                    cfg_we_i,      // load a new divisor
    input  logic [$clog2(DIV_MAX)-1:0] cfg_div_i,  // N, not N-1
    output logic                    cfg_err_o,     // latched: illegal divisor
    output logic                    baud_tick_o
);
    localparam int unsigned CNT_W = (DIV_MAX <= 1) ? 1 : $clog2(DIV_MAX);

    // Elaboration-time default, using the nearest policy of Chapter 4.3 §1.
    localparam int unsigned N_DEF = (2*CLK_HZ + BAUD_HZ) / (2*BAUD_HZ);

    initial begin
        if (CLK_HZ == 0 || BAUD_HZ == 0)
            $fatal(1, "uart_baud_tick_int: CLK_HZ and BAUD_HZ must be non-zero");
        if (BAUD_HZ > CLK_HZ)
            $fatal(1, "uart_baud_tick_int: BAUD_HZ (%0d) exceeds CLK_HZ (%0d)", BAUD_HZ, CLK_HZ);
        if (N_DEF < 1)
            $fatal(1, "uart_baud_tick_int: derived divisor %0d is below 1", N_DEF);
        if (N_DEF > DIV_MAX)
            $fatal(1, "uart_baud_tick_int: divisor %0d exceeds DIV_MAX %0d", N_DEF, DIV_MAX);
    end

    logic [CNT_W-1:0] cnt_q;
    logic [CNT_W-1:0] div_q;      // ACTIVE divisor
    logic [CNT_W-1:0] div_shadow_q;
    logic             pend_q;

    // A divisor of 0 is illegal: N-1 would underflow and the counter would
    // never reach its terminal value. Rejected, and the old value retained.
    wire cfg_bad = cfg_we_i && (cfg_div_i == '0);

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            cnt_q        <= '0;
            div_q        <= CNT_W'(N_DEF);
            div_shadow_q <= CNT_W'(N_DEF);
            pend_q       <= 1'b0;
            cfg_err_o    <= 1'b0;
            baud_tick_o  <= 1'b0;
        end else begin
            baud_tick_o <= 1'b0;                   // default: one-cycle pulse

            if (cfg_bad) begin
                cfg_err_o <= 1'b1;                 // sticky until reset
            end else if (cfg_we_i) begin
                // SHADOWED: the new divisor takes effect at the next interval
                // boundary, so the interval in progress keeps its length.
                div_shadow_q <= cfg_div_i;
                pend_q       <= 1'b1;
            end

            if (cnt_q >= div_q - 1) begin
                cnt_q       <= '0;
                baud_tick_o <= 1'b1;
                if (pend_q) begin
                    div_q  <= div_shadow_q;        // commit, on a boundary only
                    pend_q <= 1'b0;
                end
            end else begin
                cnt_q <= cnt_q + 1'b1;
            end
        end
    end
endmodule

Six details are deliberate.

cnt_q >= div_q - 1, not == — and the reason is not the obvious one. The intuition is that a divisor written below the counter's current value would never satisfy an equality test. That intuition is wrong for this design, and it was worth checking rather than asserting: because §4's commit zeroes the counter on the very edge the divisor changes, the counter can never be left above the new terminal value, and == behaves identically. Simulation of both forms against this block confirms it — both produce 100-cycle intervals after a change to divisor 100.

The inequality is defence in depth, and its value is measurable the moment the commit policy is bypassed. With the divisor applied immediately instead of shadowed, writing 100 while the counter held 600:

terminal testfirst interval after the writesteady state
>=602 cycles — recovers at once100
==65,636 cycles2^CNT_W + N100

The == form recovers, but only after the counter has run all the way to 2^16 and wrapped. Two independent mechanisms are protecting the same failure, and knowing which one is actually load-bearing matters: remove the shadow and the inequality saves the design; remove the inequality and the shadow saves it. Removing both is the defect.

baud_tick_o is registered, giving the one-cycle start-up latency §1 recorded. A combinational tick would save that cycle and put a comparator output directly onto a high-fanout enable; registering it keeps the enable clean and its timing trivial.

The default assignment baud_tick_o <= 1'b0 comes first, so every path that does not explicitly set it produces a one-cycle pulse. This is the same pattern the receiver used for sample_now in Chapter 6.3.

cfg_err_o is sticky until reset. A rejected write is a programming error, and the natural consumer is software reading a status bit some time later. A one-cycle pulse would be unobservable to it. Chapter 8.5 classifies the error kinds and revisits the lifetime.

A divisor of zero is rejected, not clamped. Clamping to 1 would silently run the link at 100 Mbaud; retaining the previous divisor keeps the link working while the error bit reports the write. Neither is universally right, and the choice is documented rather than implied.

DIV_MAX sizes the counter, and elaboration checks the default fits. A default divisor larger than DIV_MAX is a parameter error caught at elaboration rather than a silently truncated counter.

An integer baud divider. A free-running counter increments every fabric clock cycle and is compared against the active divisor minus one. When the comparison is satisfied the counter is cleared and a registered tick output is pulsed for one cycle. Separately, a configuration write path validates the incoming divisor, rejecting zero and latching a sticky error flag, and otherwise stores the value into a shadow register while setting a pending flag. The commit of the shadow register into the active divisor register is gated by the same terminal-count signal that emits the tick, so a new divisor can only take effect at an interval boundary and never in the middle of an interval.cnt_q0 .. N-1, every clkcompare>= div_q - 1terminalthe shared gatebaud_tick_oregistered, 1 cyclecfg_we_isoftware writesvalidatezero -> cfg_err_odiv_shadow_q+ pend_qdiv_qactive divisorcountmatchpulsedivisorif legalcommitdiv_q12
Figure 2 — the divider's structure. The counting path is a counter, a comparator against the active divisor and an incrementer; the configuration path is a shadow register and a pending flag whose commit is gated by the same terminal-count signal that emits the tick. That shared gate is what guarantees a divisor can only change on an interval boundary.

6. Verification

The integer divider is the one block in this module with no tolerance. Every steady-state interval must be exactly N fabric clocks. Not approximately, not on average — a divider whose intervals vary by a cycle is broken, and the test should say so:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
integer  : 3456 intervals, all exactly 868 clks

That line is from simulation of the block above, over three million fabric clocks. The check is min == max == N, with no band.

Measure from the wire, not the counter. Counting cycles between successive baud_tick_o pulses tests the contract. Inspecting cnt_q tests an implementation detail and will pass on a design whose tick logic is wrong.

Test DIVISOR = 1 explicitly, because it is the corner where the width guard and the terminal test meet. With N = 1 the terminal condition cnt_q >= 0 is true on every cycle, so the tick is asserted continuously — which is the correct meaning of one tick per cycle and is verified in simulation.

Test reconfiguration at a deliberately awkward moment, mid-interval, and assert that the interval in progress completes at its old length and the next uses the new one. A test that reconfigures while idle cannot fail §4's defect.

Test the rejected write, and assert both that cfg_err_o sets and that the previous divisor is still in use. Confirmed in simulation: after a rejected write of zero, intervals stayed at 434.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Assertion — the tick is exactly one cycle wide.
property p_tick_one_cycle;
    @(posedge clk) disable iff (!rst_n)
        baud_tick_o |=> !baud_tick_o;
endproperty
assert property (p_tick_one_cycle);

// Assertion — the counter never exceeds the active divisor's terminal value.
// Stated as a contract because the register is wide enough to violate it.
property p_counter_in_range;
    @(posedge clk) disable iff (!rst_n)
        cnt_q <= div_q - 1;
endproperty
assert property (p_counter_in_range);

// Assertion — a commit happens only on a tick, never mid-interval.
property p_commit_on_boundary_only;
    @(posedge clk) disable iff (!rst_n)
        $changed(div_q) |-> $past(baud_tick_o) || $past(!rst_n);
endproperty
assert property (p_commit_on_boundary_only);

The third is §4's policy stated as a property, and it is the one that fails on the naive immediate-apply implementation.

7. What This Means on an FPGA

The block is a counter, a comparator and an incrementer. At DIV_MAX = 65,536 that is 16 flip-flops for the counter, 16 more for each of the active and shadow divisors, and a 16-bit comparator — under sixty flip-flops and a small amount of carry logic. Nothing here is worth optimising.

A fixed divisor folds away entirely. If the divisor is a parameter rather than an input, synthesis replaces the comparator with a constant compare and often shrinks it substantially. That is the resource argument for the elaboration-time form of Chapter 4.3, and Chapter 8.5 weighs it against runtime programmability properly.

Size DIV_MAX from the slowest rate, once. A design supporting 9,600 baud at 100 MHz needs a divisor of 10,417 and therefore 14 bits. Sizing for 115,200 only and discovering the limit when someone configures 9,600 is a recurring and entirely avoidable bug.

Probe baud_tick_o against a known interval. The measurement technique is Chapter 4.3 §6's: count many intervals rather than one, because the relative precision improves as 1/N and a 0.1% error is invisible on a single interval.

8. Debugging

9. Understanding Check

10. Summary

The counter walks 0 … N−1 and wraps, giving exactly N fabric cycles per bit interval. The terminal value is N−1; comparing against N visits one extra value and makes every interval one cycle too long — at 100 MHz and 115,200 baud that is +0.0064% becoming −0.1087%, seventeen times worse and opposite in sign, landing exactly on the ceiling divisor so the symptom reads as a rounding choice.

The tick is registered, so it is observable one cycle after the terminal value — a start-up latency paid once, with interval spacing still exactly N.

Width comes from DIV_MAX, not the default divisor, and DIV_MAX comes from the slowest supported rate. $clog2(1) is 0, so the degenerate case needs an explicit guard.

The >= terminal test is defence in depth, not the load-bearing safeguard. With the commit policy in place == behaves identically, because the counter is zeroed on the commit edge — verified in simulation. Bypass the commit and the difference appears: 602 cycles to recover with >=, 65,636 with ==.

Reconfiguration is shadowed and committed on a boundary, so the interval in progress always completes at its original length. Applying immediately truncates it and corrupts the frame in flight, with the failure correlating with configuration writes rather than with traffic.

A zero divisor is rejected and flagged, not clamped, and the error bit is sticky because its consumer is software reading later.

Verified in simulation: 3,456 intervals, all exactly 868 cycles, over three million fabric clocks — the one block in this module that gets no tolerance.

11. What Comes Next

An integer divider can only produce a rate of f_clk / N for integer N. Chapter 4.3 showed the residual is small at ordinary rates — 64 ppm for the 868 case — but it is systematic: every interval is the same wrong length, so the error never cancels.

Chapter 8.3 builds the alternative. A phase accumulator keeps the fractional part instead of discarding it, so the average rate converges on the target while individual intervals alternate between two lengths. It derives the increment equation, shows why the carry out of the accumulator is the timing event, and confronts the width question — including a case where a too-narrow accumulator is worse than the integer divider it replaced.

Browse the full path on the UART tutorials index. For the divisor arithmetic this chapter implements, read back to Chapter 4.3.

Continue learning

Where this fits

Part of the UART curriculum.