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.
| Question | Answer |
|---|---|
| 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:
| edge | cnt before | == N−1 (3)? | tick | cnt after | ‖ | cnt before | == N (4)? | tick | cnt after |
|---|---|---|---|---|---|---|---|---|---|
| 1 | 0 | no | — | 1 | ‖ | 0 | no | — | 1 |
| 2 | 1 | no | — | 2 | ‖ | 1 | no | — | 2 |
| 3 | 2 | no | — | 3 | ‖ | 2 | no | — | 3 |
| 4 | 3 | yes | tick | 0 | ‖ | 3 | no | — | 4 |
| 5 | 0 | no | — | 1 | ‖ | 4 | yes | tick | 0 |
| 6 | 1 | no | — | 2 | ‖ | 0 | no | — | 1 |
| 7 | 2 | no | — | 3 | ‖ | 1 | no | — | 2 |
| 8 | 3 | yes | tick | 0 | ‖ | 2 | no | — | 3 |
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:
| divisor | actual baud | error | magnitude |
|---|---|---|---|
| 868 — correct | 115,207.3733 | +0.0064 % | 64 ppm |
| 869 — off-by-one | 115,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 cycles3. Counter Width
Derived from the parameters, never written as a convenient [31:0]:
N | max value (N−1) | $clog2(N) | register holds | note |
|---|---|---|---|---|
| 1 | 0 | 0 | — | $clog2(1) is 0; a zero-width vector is illegal — guard to 1 |
| 4 | 3 | 2 | 0–3 | exact power of two |
| 434 | 433 | 9 | 0–511 | |
| 868 | 867 | 10 | 0–1023 | |
| 10,417 | 10,416 | 14 | 0–16,383 | |
| 65,536 | 65,535 | 16 | 0–65,535 | exact power of two |
// ---------------------------------------------------------------------------
// 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
endmoduleSix 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 test | first interval after the write | steady state |
|---|---|---|
>= | 602 cycles — recovers at once | 100 |
== | 65,636 cycles — 2^CNT_W + N | 100 |
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.
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:
integer : 3456 intervals, all exactly 868 clksThat 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.
// 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
Related tutorials
- Related topic
Fractional Accumulator Baud Generation
A phase accumulator keeps the remainder an integer divider discards, so the average rate converges while individual intervals alternate between two lengths — and a too-narrow accumulator is worse than the divider it replaced.
- Related topic
RX and TX Baud Timing: One Generator or Two?
A receiver needs sixteen ticks per bit interval and a transmitter needs one. Sharing a base is excellent when that base is fractional and 73 times worse when it is not — and it does nothing at all for the receiver's phase uncertainty.
- 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.
Where this fits
Part of the UART curriculum.
