UART · Module 8
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.
Chapter 8.2 built a divider that produces f_clk / N for integer N, and nothing else. At 100 MHz and 115,200 baud the ideal divisor is 868.0556, so the hardware runs at 868 and the rate is 0.0064% fast — 64 ppm. Small, and systematic: every interval is the same wrong length, so the error never cancels.
Chapter 4.4 asked what happens if the remainder is kept rather than discarded, worked the arithmetic, and published a conceptual block explicitly deferring to this chapter:
The production generator is Chapter 8.3: reset and start-up behaviour, runtime reconfiguration, whether RX and TX share an accumulator, and how the residual error is reported all belong there.
So this chapter builds the hardware. It also does something Chapter 4.4 could not, because it had no accumulator width to speak of: it asks how wide the accumulator must be, and finds a case where a fractional generator is worse than the integer divider it replaced.
1. The Model
An integer divider counts to a whole number of cycles. A phase accumulator instead tracks how far through the current interval it is, as a fraction, and lets that fraction be as precise as its register width allows.
every fabric clock: phase <- phase + INC
when phase overflows: emit a tick, and keep the remainderThe overflow is not a side effect to be handled — the carry out of the accumulator is the timing event. That is the whole mechanism, and it is why the block has no comparator.
Think of phase as a position on a circle of circumference 2^ACC_W. Each clock advances it by INC. Every time it passes the origin, one bit interval has elapsed. Because the remainder is carried forward rather than thrown away, the position is never rounded — only the moment of crossing is quantised to a clock edge.
2. The Increment Equation
The accumulator wraps once per 2^ACC_W / INC clocks, so the tick rate is:
INC
f_tick = f_clk × ───────── [Hz] = [Hz] × [dimensionless]
2^ACC_WSetting f_tick to the target baud and solving:
BAUD_HZ × 2^ACC_W
INC = round( ───────────────────── ) [dimensionless]
CLK_HZThe rounding is the entire quantisation. An integer divider quantises the interval length to a whole number of cycles; the accumulator quantises the increment instead — and since INC is a number in the tens of thousands rather than a number like 868, the relative error of rounding it is far smaller. That is the whole reason the architecture exists.
At 100 MHz, 115,200 baud, ACC_W = 24:
INC = round(115,200 × 16,777,216 / 100,000,000) = round(19,327.3528) = 19,327
f_tick = 100,000,000 × 19,327 / 16,777,216 = 115,197.8970 baud → −0.00183 %against the integer divider's +0.0064%. A 3.5× improvement, and in the opposite direction.
3. The Width Question — and a Result Worth Knowing
ACC_W is the only real design choice, and the temptation is to pick a round number. Machine-computed at 100 MHz and 115,200 baud:
ACC_W | ideal INC | INC used | actual baud | error | ppm |
|---|---|---|---|---|---|
| 16 | 75.4975 | 75 | 114,440.9180 | −0.65893 % | −6,589 |
| 20 | 1,207.9596 | 1,208 | 115,203.8574 | +0.00335 % | +33.5 |
| 24 | 19,327.3528 | 19,327 | 115,197.8970 | −0.00183 % | −18.3 |
| 32 | 4,947,802.3250 | 4,947,802 | 115,199.9924 | −0.00001 % | −0.1 |
Compare the integer divider from Chapter 8.2: +0.0064%, or +64 ppm.
The same table at other clock/rate pairs, showing that the crossover is not a constant:
f_clk | baud | ACC_W | INC | actual baud | error | ppm |
|---|---|---|---|---|---|---|
| 50 MHz | 115,200 | 16 | 151 | 115,203.8574 | +0.00335 % | +33.5 |
| 50 MHz | 115,200 | 24 | 38,655 | 115,200.8772 | +0.00076 % | +7.6 |
| 100 MHz | 1,000,000 | 16 | 655 | 999,450.6836 | −0.05493 % | −549.3 |
| 100 MHz | 1,000,000 | 24 | 167,772 | 999,999.0463 | −0.00010 % | −1.0 |
At 50 MHz a 16-bit accumulator is already fine — INC = 151 for the same rate, because the ratio BAUD/CLK doubled. The width needed is a property of the pair, not of the baud rate.
4. What the Intervals Actually Look Like
The average rate converges. Individual intervals do not — and the precise statement matters.
An accumulator with increment INC wraps every 2^ACC_W / INC clocks on average, but a clock edge is an integer, so each interval must be one of the two integers bracketing that value:
interval ∈ { floor(2^ACC_W / INC), ceil(2^ACC_W / INC) }Note what that expression is not. It is not the floor and ceiling of the ideal interval CLK_HZ / BAUD_HZ. The difference between the two is exactly the residual rate error of §3, and at a narrow width they are different numbers entirely. Simulation over three million fabric clocks:
ACC_W | INC | ideal interval | 2^W / INC | observed intervals | distribution |
|---|---|---|---|---|---|
| 24 | 19,327 | 868.0556 | 868.0714 | 868, 869 | 92.83 % / 7.17 % |
| 20 | 1,208 | 868.0556 | 868.0265 | 868, 869 | 97.33 % / 2.67 % |
| 16 | 75 | 868.0556 | 873.8133 | 873, 874 | 18.67 % / 81.33 % |
The 16-bit row is the §3 warning made physical: the intervals are not 868 and 869 at all, they are 873 and 874. The generator is not producing a slightly imperfect 115,200 baud — it is producing a clean 114,441 baud.
5. Frequency Error and Phase Error Are Different
Worth separating explicitly, because the fractional generator is the design where they diverge.
Frequency error is rate mismatch: the generator's long-term average tick rate against the target. At ACC_W = 24 it is −18.3 ppm, and it is what accumulates across a frame in Chapter 5.5's model.
Phase error is the displacement of an individual boundary from where a perfect continuous-time generator would have put it. For the accumulator it is bounded by one fabric clock — the interval is either the floor or the ceiling, never further — and it does not accumulate, because the remainder is carried.
An integer divider has the opposite profile: zero phase variation between intervals, and a frequency error that is therefore expressed entirely as a steadily growing displacement.
| Integer divider | Fractional accumulator | |
|---|---|---|
| Every interval | identical, exactly N clocks | one of two values |
| Phase error within a frame | grows linearly | bounded by one clock |
| Long-term frequency error | fixed by N | far smaller, set by ACC_W |
| Which one a UART frame cares about | frequency — Chapter 4.4 §5 |
Chapter 4.4 §5 already settled which matters for a UART: the receiver samples at interval centres and a one-clock displacement of a boundary is a rounding error against a bit period of 868 clocks, while a frequency error accumulates across all ten intervals of a frame. The fractional generator improves the term that matters and slightly worsens the term that does not, which is why it is worth its adder.
6. The RTL
// ---------------------------------------------------------------------------
// 8.3 — fractional (NCO / phase-accumulator) generator.
// f_tick = f_clk x INC / 2^ACC_W
// ---------------------------------------------------------------------------
module uart_baud_tick_frac #(
parameter int unsigned CLK_HZ = 100_000_000,
parameter int unsigned BAUD_HZ = 115_200,
parameter int unsigned ACC_W = 24
) (
input logic clk,
input logic rst_n,
output logic baud_tick_o
);
// INC = round(BAUD_HZ x 2^ACC_W / CLK_HZ), in integer arithmetic.
// The 64-bit intermediate matters: BAUD_HZ << ACC_W overflows 32 bits for
// any ordinary rate at ACC_W >= 16.
localparam longint unsigned SCALED = (longint'(BAUD_HZ) <<< ACC_W);
localparam int unsigned INC =
int'((SCALED + (longint'(CLK_HZ) / 2)) / longint'(CLK_HZ));
initial begin
if (CLK_HZ == 0 || BAUD_HZ == 0)
$fatal(1, "uart_baud_tick_frac: CLK_HZ and BAUD_HZ must be non-zero");
if (ACC_W < 2 || ACC_W > 32)
$fatal(1, "uart_baud_tick_frac: ACC_W = %0d outside 2..32", ACC_W);
if (INC == 0)
$fatal(1, "uart_baud_tick_frac: increment rounds to 0 — ACC_W too narrow");
// longint on BOTH sides. is 32-bit arithmetic and
// evaluates to 0 at ACC_W = 32, so the plain form rejects every
// legal increment at the widest supported width — see §7.
if (longint'(INC) >= (longint'(1) <<< ACC_W))
$fatal(1, "uart_baud_tick_frac: increment %0d >= 2^ACC_W — BAUD_HZ too close to CLK_HZ", INC);
end
logic [ACC_W-1:0] phase_q;
// ONE bit wider than the accumulator, so the carry out of the top bit is
// captured rather than silently discarded. This is the defect §37 warns of.
logic [ACC_W:0] sum;
assign sum = {1'b0, phase_q} + (ACC_W+1)'(INC);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
phase_q <= '0;
baud_tick_o <= 1'b0;
end else begin
phase_q <= sum[ACC_W-1:0]; // wrap = subtract 2^ACC_W
baud_tick_o <= sum[ACC_W]; // the carry IS the timing event
end
end
endmoduleThe width hazard that silently discards the carry
The single most important line is the declaration of sum:
logic [ACC_W:0] sum; // ACC_W+1 bits
assign sum = {1'b0, phase_q} + (ACC_W+1)'(INC);One bit wider than the accumulator, and the operand explicitly extended to that width. Written the obvious way instead:
// WRONG — the carry has nowhere to go.
logic [ACC_W-1:0] sum;
assign sum = phase_q + INC;SystemVerilog sizes the addition to the width of the widest operand in the expression context, which here is the ACC_W-bit assignment target. The addition therefore happens in ACC_W bits, the overflow is discarded, and sum[ACC_W] does not exist to be read.
The failure mode is total and silent. The accumulator still wraps correctly — losing the carry is arithmetically the same as subtracting 2^ACC_W, which is exactly what the wrap should do — so phase_q behaves perfectly. What vanishes is the tick. The generator produces a correct phase sequence and no output at all, and the RTL looks right.
This is a general SystemVerilog hazard rather than a UART one, and it is worth carrying: when a carry is the thing you want, the sum must be declared wide enough to hold it. Relying on the expression to widen itself works only when some operand is already wide enough, and a same-width accumulator and increment never are.
The longint in the INC computation is the same hazard at elaboration. BAUD_HZ << ACC_W for 115,200 and ACC_W = 24 is about 1.93 × 10¹², which overflows a 32-bit integer. Computing it in longint and narrowing afterwards is what makes ACC_W = 24 and 32 produce correct increments rather than wrapped nonsense.
The trace, from simulation
A deliberately small instance makes the arithmetic readable: ACC_W = 4, INC = 6, so 2⁴/6 = 2.667 and intervals must be 2 or 3.
| cycle | phase_q | + INC | sum | carry | phase_q after | baud_tick_o |
|---|---|---|---|---|---|---|
| 1 | 0 | 6 | 6 | 0 | 6 | 0 |
| 2 | 6 | 6 | 12 | 0 | 12 | 0 |
| 3 | 12 | 6 | 18 | 1 | 2 | 0 |
| 4 | 2 | 6 | 8 | 0 | 8 | 1 |
| 5 | 8 | 6 | 14 | 0 | 14 | 0 |
| 6 | 14 | 6 | 20 | 1 | 4 | 0 |
| 7 | 4 | 6 | 10 | 0 | 10 | 1 |
| 8 | 10 | 6 | 16 | 1 | 0 | 0 |
| 9 | 0 | 6 | 6 | 0 | 6 | 1 |
Carries at cycles 3, 6 and 8; ticks at 4, 7 and 9 — one cycle later, because the tick is registered, exactly as in Chapter 8.2 §2. The tick spacing is 3, 2 — both members of the predicted {2, 3} set, averaging 2.5 over this window and converging to 2.667 over a longer one.
Note cycle 8: sum is exactly 16, so phase_q returns to 0 and the carry fires. Equality is a carry, not a near miss — which is why the test is a carry bit rather than a comparison.
Phase accumulator, ACC_W = 4, INC = 6
9 cycles7. The Same Accumulator in Three Languages
The structure is four lines of arithmetic, and the interesting part is that each language makes a different one of them the easy mistake.
//===========================================================================
// uart_baud_nco_v — Synthesizable Verilog-2001
//
// Direct phase accumulator. Each clock the phase advances by INC; the CARRY
// out of the accumulator is the timing event. Average rate is exactly
// f_clk * INC / 2^ACC_W; individual intervals alternate between two adjacent
// integers. Chapter 8.3 measures both.
//===========================================================================
module uart_baud_nco_v #(
parameter ACC_W = 24,
parameter INC = 19327
) (
input wire clk,
input wire rst_n,
output reg tick_o
);
reg [ACC_W-1:0] phase_q;
// ONE bit wider than the accumulator, so the carry out of the top bit is
// captured rather than silently discarded — the defect §54 warns about.
// Both operands must be ACC_W+1 bits. Writing INC[ACC_W:0] looks
// symmetric with the concatenation above and is wrong: a parameter holds
// a 32-bit value, so at ACC_W=32 bit 32 does not exist and the select
// returns x -- poisoning the carry and every tick with it. Select
// ACC_W bits, then widen by concatenation, exactly as phase_q is widened.
wire [ACC_W:0] sum = {1'b0, phase_q} + {1'b0, INC[ACC_W-1:0]};
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
phase_q <= {ACC_W{1'b0}};
tick_o <= 1'b0;
end else begin
phase_q <= sum[ACC_W-1:0]; // wrapping = subtracting 2^ACC_W
tick_o <= sum[ACC_W]; // the carry IS the timing event
end
end
endmodule//===========================================================================
// uart_baud_nco_sv — Synthesizable SystemVerilog
//
// A numerically controlled oscillator. phase_q accumulates INC every clock
// and wraps modulo 2^ACC_W; the CARRY OUT of that addition is the tick.
//
// Average rate = f_clk * INC / 2^ACC_W, exactly -- no rounding anywhere in
// the hardware. Individual intervals alternate between the two integers
// either side of the ideal; that difference is the jitter, and chapter 8.3
// section 8 bounds it at strictly less than one clock period.
//===========================================================================
module uart_baud_nco_sv #(
parameter int ACC_W = 24,
parameter int INC = 19327
) (
input logic clk,
input logic rst_n,
output logic tick_o
);
logic [ACC_W-1:0] phase_q;
// ACC_W+1 bits wide: bit ACC_W is the carry, and it must not be lost.
logic [ACC_W:0] sum;
assign sum = {1'b0, phase_q} + (ACC_W+1)'(INC);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
phase_q <= '0;
tick_o <= 1'b0;
end else begin
phase_q <= sum[ACC_W-1:0]; // wrapping == subtracting 2^ACC_W
tick_o <= sum[ACC_W]; // the carry IS the timing event
end
end
initial begin
if (ACC_W < 2 || ACC_W > 32)
$fatal(1, "uart_baud_nco_sv: ACC_W must be 2..32, got %0d", ACC_W);
// longint, not int. `1 << ACC_W` is 32-bit arithmetic: at ACC_W=32
// it evaluates to 0 and the guard rejects every legal INC. A check
// written to catch parameter-width mistakes is exactly where one
// hides -- this one fired on the first ACC_W=32 run.
if (INC <= 0 || longint'(INC) >= (longint'(1) << ACC_W))
$fatal(1, "uart_baud_nco_sv: INC must be 1..2^ACC_W-1, got %0d", INC);
end
endmodule--==========================================================================
-- uart_baud_nco -- Synthesizable VHDL-2008
--
-- A numerically controlled oscillator. phase_q accumulates INC every clock
-- and wraps modulo 2^ACC_W; the CARRY OUT of that addition is the tick.
--
-- Average rate = f_clk * INC / 2^ACC_W, exactly. Individual intervals
-- alternate between the two integers either side of the ideal.
--==========================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity uart_baud_nco is
generic (
ACC_W : positive := 24;
INC : positive := 19327
);
port (
clk : in std_logic;
rst_n : in std_logic;
tick_o : out std_logic
);
end entity uart_baud_nco;
architecture rtl of uart_baud_nco is
signal phase_q : unsigned(ACC_W-1 downto 0);
-- ACC_W+1 bits wide. VHDL will not silently widen for you, so the carry
-- position has to be created deliberately -- which is arguably clearer
-- than the Verilog concatenation that does the same thing.
signal sum : unsigned(ACC_W downto 0);
begin
sum <= ('0' & phase_q) + to_unsigned(INC, ACC_W + 1);
process (clk, rst_n)
begin
if rst_n = '0' then
phase_q <= (others => '0');
tick_o <= '0';
elsif rising_edge(clk) then
phase_q <= sum(ACC_W-1 downto 0); -- wrap == subtract 2^ACC_W
tick_o <= sum(ACC_W); -- the carry IS the event
end if;
end process;
-- Contract check. A too-large INC makes the generator tick every clock,
-- and the symptom looks like a baud-rate problem rather than a parameter
-- problem -- so it is worth catching at elaboration.
--
-- Written in real, deliberately. `INC < 2**ACC_W` is the obvious form
-- and it is unusable: VHDL's integer is only guaranteed to 2**31-1, so
-- at ACC_W = 32 the guard itself raises "result of 2 ** 32 cannot be
-- represented as INTEGER" and kills the run it was written to protect.
-- Guarding it with `(ACC_W >= 31) or (...)` is not a fix either --
-- whether a tool actually skips the right operand of `or` is not
-- something to bet a parameter check on, and NVC 1.23.0 evaluated it
-- here regardless. The rule: an overflowing expression must not appear
-- in the guard at all. real holds every value in range exactly.
assert real(INC) < 2.0 ** ACC_W
report "uart_baud_nco: INC must be < 2**ACC_W"
severity failure;
end architecture rtl;| Property | Verilog-2001 | SystemVerilog | VHDL-2008 | Agree? |
|---|---|---|---|---|
| accumulator width | ACC_W | ACC_W | ACC_W | yes |
| sum width | ACC_W+1 | ACC_W+1 | ACC_W+1 | yes |
| how the carry is made | {1'b0, phase_q} | {1'b0, phase_q} | '0' & phase_q | yes |
how INC is widened | {1'b0, INC[ACC_W-1:0]} | cast to ACC_W+1 | to_unsigned(INC, ACC_W+1) | yes |
| wrap | drop the carry bit | same | same | yes |
| tick | registered carry | same | same | yes |
| reset phase | 0 | 0 | 0 | yes |
| range guard | none | $fatal | assert | no — 8.5 §5 |
8. The Accumulator, Traced
Hand-drawn accumulator tables are usually wrong in the same place, so this one
is printed by the simulator. ACC_W = 4 (modulus 16) and INC = 5 — small
enough to read, and it has the same structure as the 24-bit version:
TRACE-B uart_baud_nco_v, ACC_W=4 (modulus 16), INC=5
clock | phase_q | sum | carry=tick_o
13 | 1 | 6 | 1
14 | 6 | 11 | 0
15 | 11 | 16 | 0 <- sum reaches the modulus: carry set
16 | 0 | 5 | 1 <- ...and appears here, registered
17 | 5 | 10 | 0
18 | 10 | 15 | 0
19 | 15 | 20 | 0 <- sum = 20, carry set, remainder 4 KEPT
20 | 4 | 9 | 1
21 | 9 | 14 | 0
22 | 14 | 19 | 0
23 | 3 | 8 | 1
24 | 8 | 13 | 0
25 | 13 | 18 | 0
26 | 2 | 7 | 1
27 | 7 | 12 | 0
28 | 12 | 17 | 0
29 | 1 | 6 | 1 <- phase 1 again: the cycle has closedThree things in that table are the whole mechanism.
The remainder is kept, and that is the entire reason this works. At clock 19 the sum is 20 and the modulus is 16. The phase becomes 4, not 0 — the overshoot is carried into the next interval, so the next tick arrives sooner. A generator that zeroed the phase on every tick would throw that remainder away and degrade to an integer divider with worse jitter.
The intervals are not equal, and there are exactly two of them. Ticks land at clocks 13, 16, 20, 23, 26, 29: intervals of 3, 4, 3, 3, 3. The ideal is 16/5 = 3.2, and 3 and 4 are the integers bracketing it — §4's claim, observed.
Five ticks in sixteen clocks, exactly. From clock 13 to clock 29 the phase
returns to 1, and the pattern repeats forever. That is §4's period formula with
gcd(5, 16) = 1: P = 16 clocks containing 5 ticks. The average
rate is exact even though not one individual interval is.
9. The Independent Reference Model
The divider of Chapter 8.2 could be checked
by arithmetic alone: every interval is DIV_N, and a number either equals
DIV_N or does not. The NCO cannot, because its intervals are
deliberately unequal and the correct sequence of interval lengths is not
something a human writes down.
So the NCO gets a second oracle: a behavioural model that computes the same tick stream by different arithmetic, compared cycle by cycle.
//===========================================================================
// nco_ref — an INDEPENDENT behavioural model of the NCO contract.
//
// It is written from the specification sentence
// "accumulate INC each clock modulo 2^ACC_W and emit a tick on every
// wrap"
// using COMPARE-AND-SUBTRACT. The design under test extracts a CARRY OUT
// instead. Two different pieces of arithmetic that must agree on every
// clock -- which is the point: a model that copied the design's carry
// trick would agree with a broken design just as happily.
//
// 64 bits throughout so that ACC_W = 32 needs no special case.
//===========================================================================
module nco_ref #(
parameter ACC_W = 24,
parameter INC = 19327
) (
input wire clk,
input wire rst_n,
output reg tick_o
);
reg [63:0] acc;
reg [63:0] modulus;
initial modulus = (64'd1 << ACC_W);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
acc <= 64'd0;
tick_o <= 1'b0;
end else if ((acc + INC) >= modulus) begin
acc <= acc + INC - modulus; // subtract, do not drop a carry
tick_o <= 1'b1;
end else begin
acc <= acc + INC;
tick_o <= 1'b0;
end
end
endmodule//===========================================================================
// nco_ref_sv — the same independent model, in SystemVerilog.
//
// Compare-and-subtract, not carry extraction. SystemVerilog's longint
// makes the 64-bit intermediate explicit rather than implied, which is
// what lets ACC_W = 32 be ordinary arithmetic instead of a special case.
//===========================================================================
module nco_ref_sv #(
parameter int ACC_W = 24,
parameter int INC = 19327
) (
input logic clk,
input logic rst_n,
output logic tick_o
);
localparam longint unsigned MODULUS = (longint'(1) << ACC_W);
longint unsigned acc;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
acc <= '0;
tick_o <= 1'b0;
end else if (acc + INC >= MODULUS) begin
acc <= acc + INC - MODULUS;
tick_o <= 1'b1;
end else begin
acc <= acc + INC;
tick_o <= 1'b0;
end
end
endmodule--==========================================================================
-- nco_ref -- the same independent model, in VHDL-2008.
--
-- Compare-and-subtract on a 64-bit unsigned, not carry extraction. The
-- design under test drops a carry out of an ACC_W+1 bit adder; this model
-- never forms a carry at all. Two different pieces of arithmetic that must
-- agree on every clock.
--==========================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity nco_ref is
generic (
ACC_W : positive := 24;
INC : positive := 19327
);
port (
clk : in std_logic;
rst_n : in std_logic;
tick_o : out std_logic
);
end entity nco_ref;
architecture bhv of nco_ref is
-- 64 bits so that ACC_W = 32 is arithmetic, not a special case.
constant MODULUS : unsigned(63 downto 0) := shift_left(to_unsigned(1, 64), ACC_W);
constant INC_U : unsigned(63 downto 0) := to_unsigned(INC, 64);
signal acc : unsigned(63 downto 0) := (others => '0');
begin
process (clk, rst_n)
begin
if rst_n = '0' then
acc <= (others => '0');
tick_o <= '0';
elsif rising_edge(clk) then
if acc + INC_U >= MODULUS then
acc <= acc + INC_U - MODULUS;
tick_o <= '1';
else
acc <= acc + INC_U;
tick_o <= '0';
end if;
end if;
end process;
end architecture bhv;10. The NCO Testbench, in Three Languages
Each testbench runs two independent oracles at once:
- the reference model above, compared every clock — catches any arithmetic error, including ones that produce a plausible-looking tick stream;
tick_probefrom Chapter 8.2 §8 plus exact integer predictions — catches a design and a model that are wrong in the same way.
Neither reads phase_q.
//===========================================================================
// tb_uart_baud_nco_v — self-checking Verilog-2001 testbench for the NCO.
//
// Two independent oracles, deliberately:
// 1. nco_ref, compared cycle by cycle -- catches any arithmetic error.
// 2. tick_probe plus exact integer predictions -- catches a model and a
// design that are wrong in the same way.
//
// Neither reads phase_q.
//===========================================================================
`timescale 1ns/1ps
module tb_uart_baud_nco_v;
localparam RUN = 400000;
reg clk = 1'b0, rst_n = 1'b0;
always #5 clk = ~clk; // 100 MHz
integer checks = 0, failures = 0;
reg [63:0] want64;
task check;
input cond;
input [8*54-1:0] name;
begin
checks = checks + 1;
if (cond) $display(" PASS %0s", name);
else begin failures = failures + 1; $display(" FAIL %0s", name); end
end
endtask
task check_i64;
input integer got;
input [63:0] want;
input [8*54-1:0] name;
begin
checks = checks + 1;
if (got === want) $display(" PASS %0s (%0d)", name, got);
else begin
failures = failures + 1;
$display(" FAIL %0s: got %0d, want %0d", name, got, want);
end
end
endtask
// Four widths. 16 is the width Chapter 8.3 shows is WORSE than an
// integer divider; 32 is the width where the carry-width defect lives.
wire t16, t20, t24, t32;
wire r16, r20, r24, r32;
uart_baud_nco_v #(.ACC_W(16), .INC(75)) n16 (.clk(clk), .rst_n(rst_n), .tick_o(t16));
uart_baud_nco_v #(.ACC_W(20), .INC(1208)) n20 (.clk(clk), .rst_n(rst_n), .tick_o(t20));
uart_baud_nco_v #(.ACC_W(24), .INC(19327)) n24 (.clk(clk), .rst_n(rst_n), .tick_o(t24));
uart_baud_nco_v #(.ACC_W(32), .INC(4947802)) n32 (.clk(clk), .rst_n(rst_n), .tick_o(t32));
nco_ref #(.ACC_W(16), .INC(75)) m16 (.clk(clk), .rst_n(rst_n), .tick_o(r16));
nco_ref #(.ACC_W(20), .INC(1208)) m20 (.clk(clk), .rst_n(rst_n), .tick_o(r20));
nco_ref #(.ACC_W(24), .INC(19327)) m24 (.clk(clk), .rst_n(rst_n), .tick_o(r24));
nco_ref #(.ACC_W(32), .INC(4947802)) m32 (.clk(clk), .rst_n(rst_n), .tick_o(r32));
tick_probe p16 (.clk(clk), .rst_n(rst_n), .tick(t16));
tick_probe p20 (.clk(clk), .rst_n(rst_n), .tick(t20));
tick_probe p24 (.clk(clk), .rst_n(rst_n), .tick(t24));
tick_probe p32 (.clk(clk), .rst_n(rst_n), .tick(t32));
integer mm16 = 0, mm20 = 0, mm24 = 0, mm32 = 0;
always @(posedge clk) if (rst_n) begin
if (t16 !== r16) mm16 = mm16 + 1;
if (t20 !== r20) mm20 = mm20 + 1;
if (t24 !== r24) mm24 = mm24 + 1;
if (t32 !== r32) mm32 = mm32 + 1;
end
integer tick_in_reset = 0;
always @(posedge clk)
if (!rst_n && (t16 || t20 || t24 || t32))
tick_in_reset = tick_in_reset + 1;
initial begin
#40_000_000;
$display(" FAIL watchdog: simulation did not finish");
$display("== %0d checks, %0d failures ==", checks+1, failures+1);
$display(" RESULT: VERILOG NCO TESTS FAILED (timeout)");
$finish;
end
real ideal, got_avg, err_ppm;
initial begin
$display("== uart_baud_nco_v : self-checking Verilog testbench ==");
rst_n = 1'b0;
repeat (20) @(posedge clk);
check(tick_in_reset == 0, "reset: no ticks while rst_n low");
@(negedge clk) rst_n = 1'b1;
repeat (RUN) @(posedge clk);
// --- oracle 1: the independent model, every clock -----------------
check(mm16 == 0, "ACC_W=16 matches reference model every clock");
check(mm20 == 0, "ACC_W=20 matches reference model every clock");
check(mm24 == 0, "ACC_W=24 matches reference model every clock");
check(mm32 == 0, "ACC_W=32 matches reference model every clock");
// --- pulse width --------------------------------------------------
check(p16.wide_pulse == 0, "ACC_W=16 pulse one clock wide");
check(p20.wide_pulse == 0, "ACC_W=20 pulse one clock wide");
check(p24.wide_pulse == 0, "ACC_W=24 pulse one clock wide");
check(p32.wide_pulse == 0, "ACC_W=32 pulse one clock wide");
// --- the defining NCO property: exactly TWO interval lengths, and
// they are adjacent integers. This is the jitter, stated exactly
// rather than bounded by a tolerance.
check(p16.max_iv - p16.min_iv == 1, "ACC_W=16 intervals span exactly one clock");
check(p20.max_iv - p20.min_iv == 1, "ACC_W=20 intervals span exactly one clock");
check(p24.max_iv - p24.min_iv == 1, "ACC_W=24 intervals span exactly one clock");
check(p32.max_iv - p32.min_iv == 1, "ACC_W=32 intervals span exactly one clock");
// --- and they straddle 2^ACC_W / INC, NOT CLK_HZ / BAUD_HZ --------
ideal = 65536.0 / 75.0;
check(p16.min_iv <= ideal && ideal <= p16.max_iv, "ACC_W=16 intervals straddle 2^W/INC");
ideal = 1048576.0 / 1208.0;
check(p20.min_iv <= ideal && ideal <= p20.max_iv, "ACC_W=20 intervals straddle 2^W/INC");
ideal = 16777216.0 / 19327.0;
check(p24.min_iv <= ideal && ideal <= p24.max_iv, "ACC_W=24 intervals straddle 2^W/INC");
ideal = 4294967296.0 / 4947802.0;
check(p32.min_iv <= ideal && ideal <= p32.max_iv, "ACC_W=32 intervals straddle 2^W/INC");
// --- oracle 2: exact tick count, floor((cycles-1) * INC / 2^W) ----
want64 = ((p16.clk_cnt-1) * 64'd75) >> 16; check_i64(p16.n_ticks, want64, "ACC_W=16 exact tick count");
want64 = ((p20.clk_cnt-1) * 64'd1208) >> 20; check_i64(p20.n_ticks, want64, "ACC_W=20 exact tick count");
want64 = ((p24.clk_cnt-1) * 64'd19327) >> 24; check_i64(p24.n_ticks, want64, "ACC_W=24 exact tick count");
want64 = ((p32.clk_cnt-1) * 64'd4947802) >> 32; check_i64(p32.n_ticks, want64, "ACC_W=32 exact tick count");
// --- the mean rate, reported and bounded --------------------------
got_avg = p24.sum_iv / (p24.n_ticks - 1);
ideal = 16777216.0 / 19327.0;
err_ppm = 1.0e6 * (got_avg - ideal) / ideal;
$display(" INFO ACC_W=24 mean interval %0.6f clocks, ideal %0.6f, err %0.3f ppm",
got_avg, ideal, err_ppm);
check((err_ppm < 5.0) && (err_ppm > -5.0), "ACC_W=24 mean rate matches INC/2^ACC_W");
check(p24.n_ticks > 400, "ACC_W=24 hundreds of intervals observed");
// --- reset mid-run -------------------------------------------------
@(negedge clk) rst_n = 1'b0;
repeat (10) @(posedge clk);
check(t24 === 1'b0 && t32 === 1'b0, "reset mid-run: ticks return low");
@(negedge clk) rst_n = 1'b1;
repeat (5000) @(posedge clk);
check(mm24 == 0, "still matches the model after re-reset");
$display("== %0d checks, %0d failures ==", checks, failures);
if (failures == 0) $display(" RESULT: ALL VERILOG NCO TESTS PASSED");
else $display(" RESULT: VERILOG NCO TESTS FAILED");
$finish;
end
endmodule//===========================================================================
// tb_uart_baud_nco_sv — self-checking SystemVerilog testbench for the NCO.
//
// Two independent oracles: the compare-and-subtract reference model
// compared every clock, and exact integer predictions from tick_probe.
// Neither reads phase_q.
//===========================================================================
`timescale 1ns/1ps
module tb_uart_baud_nco_sv;
localparam int RUN = 400_000;
localparam int NNCO = 4;
// Icarus Verilog 13.0 supports no array parameters; in a tool that does
// these are localparam int ACCWS [NNCO] = '{16, 20, 24, 32};
function automatic int accws(input int g);
case (g) 0: accws = 16; 1: accws = 20; 2: accws = 24; default: accws = 32; endcase
endfunction
function automatic int incs(input int g);
case (g) 0: incs = 75; 1: incs = 1208; 2: incs = 19327; default: incs = 4947802; endcase
endfunction
logic clk = 1'b0, rst_n = 1'b0;
always #5ns clk = ~clk;
int unsigned checks = 0, failures = 0;
task automatic check(input bit cond, input string name);
checks++;
if (cond) $display(" PASS %0s", name);
else begin failures++; $display(" FAIL %0s", name); end
endtask
task automatic check_eq(input longint got, input longint want, input string name);
checks++;
if (got == want) $display(" PASS %0s (%0d)", name, got);
else begin failures++; $display(" FAIL %0s: got %0d, want %0d", name, got, want); end
endtask
logic [NNCO-1:0] ntick, rtick;
generate
for (genvar g = 0; g < NNCO; g++) begin : g_nco
localparam int W = accws(g);
localparam int I = incs(g);
uart_baud_nco_sv #(.ACC_W(W), .INC(I)) u_dut (
.clk(clk), .rst_n(rst_n), .tick_o(ntick[g]));
nco_ref_sv #(.ACC_W(W), .INC(I)) u_ref (
.clk(clk), .rst_n(rst_n), .tick_o(rtick[g]));
tick_probe_sv u_probe (.clk(clk), .rst_n(rst_n), .tick(ntick[g]));
int unsigned mism = 0;
always @(posedge clk) if (rst_n && ntick[g] !== rtick[g]) mism++;
task automatic run_checks();
longint want;
real ideal;
check_eq(mism, 0, $sformatf("ACC_W=%0d matches reference every clock", W));
check_eq(u_probe.wide_pulse, 0, $sformatf("ACC_W=%0d pulse one clock wide", W));
check(u_probe.max_iv - u_probe.min_iv == 1,
$sformatf("ACC_W=%0d intervals span exactly one clock", W));
ideal = (2.0 ** W) / real'(I);
check(real'(u_probe.min_iv) <= ideal && ideal <= real'(u_probe.max_iv),
$sformatf("ACC_W=%0d intervals straddle 2^W/INC", W));
// Exact: floor((cycles-1) * INC / 2^ACC_W). No tolerance.
want = (longint'(u_probe.clk_cnt - 1) * I) >> W;
check_eq(u_probe.n_ticks, want, $sformatf("ACC_W=%0d exact tick count", W));
endtask
end
endgenerate
int unsigned tick_in_reset = 0;
always @(posedge clk) if (!rst_n && |ntick) tick_in_reset++;
initial begin
#40ms;
$display(" FAIL watchdog: simulation did not finish");
$display("== %0d checks, %0d failures ==", checks+1, failures+1);
$display(" RESULT: SYSTEMVERILOG NCO TESTS FAILED (timeout)");
$finish;
end
real ideal, got_avg, err_ppm;
initial begin
$display("== uart_baud_nco_sv : self-checking SystemVerilog testbench ==");
rst_n = 1'b0;
repeat (20) @(posedge clk);
check_eq(tick_in_reset, 0, "reset: no ticks while rst_n low");
@(negedge clk) rst_n = 1'b1;
repeat (RUN) @(posedge clk);
g_nco[0].run_checks(); g_nco[1].run_checks();
g_nco[2].run_checks(); g_nco[3].run_checks();
got_avg = g_nco[2].u_probe.sum_iv / real'(g_nco[2].u_probe.n_ticks - 1);
ideal = 16777216.0 / 19327.0;
err_ppm = 1.0e6 * (got_avg - ideal) / ideal;
$display(" INFO ACC_W=24 mean interval %0.6f clocks, ideal %0.6f, err %0.3f ppm",
got_avg, ideal, err_ppm);
check(err_ppm < 5.0 && err_ppm > -5.0, "ACC_W=24 mean rate matches INC/2^ACC_W");
check(g_nco[2].u_probe.n_ticks > 400, "ACC_W=24 hundreds of intervals observed");
@(negedge clk) rst_n = 1'b0;
repeat (10) @(posedge clk);
check(ntick == '0, "reset mid-run: ticks return low");
@(negedge clk) rst_n = 1'b1;
repeat (5000) @(posedge clk);
check_eq(g_nco[2].mism, 0, "still matches the model after re-reset");
$display("== %0d checks, %0d failures ==", checks, failures);
if (failures == 0) $display(" RESULT: ALL SYSTEMVERILOG NCO TESTS PASSED");
else $display(" RESULT: SYSTEMVERILOG NCO TESTS FAILED");
$finish;
end
endmodule--==========================================================================
-- tb_uart_baud_nco -- self-checking VHDL-2008 testbench for the NCO.
--
-- Two independent oracles: nco_ref compared every clock, and exact
-- integer predictions from tick_probe. Neither reads phase_q.
--==========================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use std.env.finish;
entity tb_uart_baud_nco is
end entity tb_uart_baud_nco;
architecture sim of tb_uart_baud_nco is
type int_vec is array (natural range <>) of integer;
type nat_vec is array (natural range <>) of natural;
type real_vec is array (natural range <>) of real;
constant ACCWS : int_vec := (16, 20, 24, 32);
constant INCS : int_vec := (75, 1208, 19327, 4947802);
constant CLK_PERIOD : time := 10 ns;
constant RUN : natural := 400000;
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal done : std_logic := '0';
signal ntick, rtick : std_logic_vector(ACCWS'range);
signal clk_cnt : nat_vec(ACCWS'range);
signal n_ticks : nat_vec(ACCWS'range);
signal first_l : nat_vec(ACCWS'range);
signal min_iv : nat_vec(ACCWS'range);
signal max_iv : nat_vec(ACCWS'range);
signal wide : nat_vec(ACCWS'range);
signal sum_iv : real_vec(ACCWS'range);
signal mism : nat_vec(ACCWS'range) := (others => 0);
signal tick_in_reset : natural := 0;
begin
clk <= not clk after CLK_PERIOD / 2 when done = '0' else '0';
gen_nco : for g in ACCWS'range generate
u_dut : entity work.uart_baud_nco
generic map (ACC_W => ACCWS(g), INC => INCS(g))
port map (clk => clk, rst_n => rst_n, tick_o => ntick(g));
u_ref : entity work.nco_ref
generic map (ACC_W => ACCWS(g), INC => INCS(g))
port map (clk => clk, rst_n => rst_n, tick_o => rtick(g));
u_probe : entity work.tick_probe
port map (clk => clk, rst_n => rst_n, tick => ntick(g),
clk_cnt_o => clk_cnt(g), n_ticks_o => n_ticks(g),
first_lat_o => first_l(g), min_iv_o => min_iv(g),
max_iv_o => max_iv(g), wide_o => wide(g),
sum_iv_o => sum_iv(g));
-- One driver per element. nat_vec is unresolved, so a second
-- process writing the same element is an elaboration error.
cmp : process (clk)
variable m : natural := 0;
begin
if rising_edge(clk) and rst_n = '1' then
if ntick(g) /= rtick(g) then
m := m + 1;
end if;
mism(g) <= m;
end if;
end process cmp;
end generate gen_nco;
rst_mon : process (clk)
variable n : natural := 0;
begin
if rising_edge(clk) and rst_n = '0' then
if ntick /= (ntick'range => '0') then
n := n + 1;
end if;
tick_in_reset <= n;
end if;
end process rst_mon;
watchdog : process
begin
wait for 40 ms;
report "watchdog: simulation did not finish" severity error;
report " RESULT: VHDL NCO TESTS FAILED (timeout)" severity failure;
wait;
end process watchdog;
stim : process
variable checks : natural := 0;
variable failures : natural := 0;
procedure check(cond : boolean; name : string) is
begin
checks := checks + 1;
if cond then report " PASS " & name;
else failures := failures + 1; report " FAIL " & name severity error;
end if;
end procedure;
procedure check_eq(got, want : natural; name : string) is
begin
checks := checks + 1;
if got = want then
report " PASS " & name & " (" & integer'image(got) & ")";
else
failures := failures + 1;
report " FAIL " & name & ": got " & integer'image(got) &
", want " & integer'image(want) severity error;
end if;
end procedure;
-- floor((cycles-1) * INC / 2**ACC_W), in 96 bits so ACC_W = 32
-- needs no special case.
function exact_ticks(cycles, inc, acc_w : natural) return natural is
variable prod : unsigned(95 downto 0);
begin
prod := to_unsigned(cycles - 1, 48) * to_unsigned(inc, 48);
return to_integer(shift_right(prod, acc_w)(31 downto 0));
end function;
variable ideal, got_avg, err_ppm : real;
begin
report "== uart_baud_nco (VHDL) : self-checking testbench ==";
rst_n <= '0';
for i in 0 to 19 loop wait until rising_edge(clk); end loop;
wait for 1 ns;
check_eq(tick_in_reset, 0, "reset: no ticks while rst_n low");
rst_n <= '1';
for i in 0 to RUN - 1 loop wait until rising_edge(clk); end loop;
wait for 1 ns;
for g in ACCWS'range loop
check_eq(mism(g), 0,
"ACC_W=" & integer'image(ACCWS(g)) & " matches reference every clock");
check_eq(wide(g), 0,
"ACC_W=" & integer'image(ACCWS(g)) & " pulse one clock wide");
check(max_iv(g) - min_iv(g) = 1,
"ACC_W=" & integer'image(ACCWS(g)) & " intervals span exactly one clock");
ideal := 2.0 ** ACCWS(g) / real(INCS(g));
check(real(min_iv(g)) <= ideal and ideal <= real(max_iv(g)),
"ACC_W=" & integer'image(ACCWS(g)) & " intervals straddle 2^W/INC");
check_eq(n_ticks(g), exact_ticks(clk_cnt(g), INCS(g), ACCWS(g)),
"ACC_W=" & integer'image(ACCWS(g)) & " exact tick count");
end loop;
got_avg := sum_iv(2) / real(n_ticks(2) - 1);
ideal := 16777216.0 / 19327.0;
err_ppm := 1.0e6 * (got_avg - ideal) / ideal;
report " INFO ACC_W=24 mean interval " & real'image(got_avg) &
" clocks, ideal " & real'image(ideal) &
", err " & real'image(err_ppm) & " ppm";
check(abs(err_ppm) < 5.0, "ACC_W=24 mean rate matches INC/2^ACC_W");
check(n_ticks(2) > 400, "ACC_W=24 hundreds of intervals observed");
rst_n <= '0';
for i in 0 to 9 loop wait until rising_edge(clk); end loop;
wait for 1 ns;
check(ntick = (ntick'range => '0'), "reset mid-run: ticks return low");
rst_n <= '1';
for i in 0 to 4999 loop wait until rising_edge(clk); end loop;
wait for 1 ns;
check_eq(mism(2), 0, "still matches the model after re-reset");
report "== " & integer'image(checks) & " checks, " &
integer'image(failures) & " failures ==";
if failures = 0 then
report " RESULT: ALL VHDL NCO TESTS PASSED";
else
report " RESULT: VHDL NCO TESTS FAILED" severity error;
end if;
done <= '1';
finish;
end process stim;
end architecture sim;$ iverilog -g2001 -o sim_nco_v uart_baud_nco_v.v nco_ref_v.v tick_probe_v.v tb_uart_baud_nco_v.v
$ ./sim_nco_v
== uart_baud_nco_v : self-checking Verilog testbench ==
PASS reset: no ticks while rst_n low
PASS ACC_W=16 matches reference model every clock
PASS ACC_W=20 matches reference model every clock
PASS ACC_W=24 matches reference model every clock
PASS ACC_W=32 matches reference model every clock
PASS ACC_W=24 intervals span exactly one clock
PASS ACC_W=24 intervals straddle 2^W/INC
PASS ACC_W=24 exact tick count (460)
INFO ACC_W=24 mean interval 868.069717 clocks, ideal 868.071403, err -1.942 ppm
PASS ACC_W=24 mean rate matches INC/2^ACC_W
PASS still matches the model after re-reset
== 25 checks, 0 failures ==
RESULT: ALL VERILOG NCO TESTS PASSEDAll three languages run 25 checks with 0 failures — the same 25.
Three of those checks are worth singling out, because they state the NCO's defining properties exactly rather than approximately:
| Check | Why it is the right statement |
|---|---|
| intervals span exactly one clock | §4's claim that there are precisely two interval lengths, not a spread |
| intervals straddle 2^W / INC | and not CLK_HZ/BAUD_HZ — §4 shows these differ, and at ACC_W=16 they are different numbers entirely |
exact tick count floor((cycles-1) × INC / 2^W) | integer arithmetic with no tolerance; a slow drift fails it and passes any average |
The mean-rate check has a 5 ppm window, and that window is a property of the measurement rather than of the design. Over 400,000 clocks the accumulator does not return to phase zero, so the observed mean carries a residual worth up to one interval — about 2.5 ppm here. Claiming tighter would be claiming precision the run does not have. The exact-count check is what makes the tight statement; the ppm figure is reported because it is the number an engineer recognises.
Proving these checks can fail
Four mutations, each against the unmodified testbench above (baseline 25 checks / 0 failures):
| Mutation | What it breaks | Failures |
|---|---|---|
| accumulator discards the remainder on every tick | §1's whole mechanism | 19 |
| increment off by one | §2's rate equation | 10 |
| accumulator resets half-full, not to 0 | reproducibility (8.5 §4) | 9 |
carry width: INC[ACC_W:0] | §7's real defect | 4 |
The first row is the one to read. Zeroing the phase on every tick turns the
NCO back into an integer divider with worse jitter — the exact mistake §1 warns
against — and 19 of 25 checks notice. The last row is the quietest, because
it only misbehaves at ACC_W = 32: three of its four failures are the
ACC_W=32 checks, and the fourth is the model comparison. A test matrix
that stopped at ACC_W = 24 would have found none of it.
11. Verification
Assert the interval set, with no tolerance. §4 established that every interval must be one of exactly two known integers. This is stronger than an average-rate check and it fails immediately on a width error, a lost carry or a wrong increment:
ACC_W=16 : 3433 intervals 873:641 874:2792
ACC_W=20 : 3456 intervals 868:3365 869:91
ACC_W=24 : 3455 intervals 868:3209 869:246From simulation over three million fabric clocks. Zero intervals outside the predicted set in all three cases — the check is count_outside == 0, not a percentage.
Derive the expected tick count rather than guessing a tolerance. Over C fabric clocks the accumulator advances C × INC, so it wraps:
expected ticks = floor(C × INC / 2^ACC_W) ± 1 for the start-up phaseThe ±1 is not slack — it is the single tick whose position depends on where the accumulator happened to start. Simulation over 3,000,000 clocks at ACC_W = 24: 3,456 observed against 3,455 expected, within the derived bound. A test using a loose percentage band would pass on a design with a materially wrong increment.
Know which checks need a long run and which do not. These are different, and conflating them produces either wasted simulation or an unsupported claim:
| Check | Run length needed | Why |
|---|---|---|
| every interval is in the two-value set | any | a per-interval law — one violation is a defect |
| tick count against the derived expression | thousands of ticks | the ±1 start-up term must become negligible |
| the distribution between the two lengths | one full period | shorter samples a biased slice |
The three-million-clock run above covers 45.8 full periods at ACC_W = 16 and 22.9 at 20, but only 0.179 of one period at 24 — where the period is 16,777,216 clocks. So the 92.83 % / 7.17 % split reported for the 24-bit case is a sample, not the settled distribution, and it is quoted here as an observation rather than as a characterisation. The interval-set and tick-count checks are unaffected, which is why those are the ones the suite asserts.
Compare against a software model, cycle by cycle. The accumulator is trivially modelled — phase = (phase + INC) mod 2^W, tick on wrap — so a reference model can predict every tick position exactly rather than only the aggregate rate. That is what caught the interval-set law being about 2^W / INC rather than the ideal interval while this chapter was being written.
Test the narrow width deliberately. ACC_W = 16 at 100 MHz is not a corner case to avoid; it is a configuration someone will choose, and the test should demonstrate that it produces 873/874 rather than 868/869. A verification suite that only exercises the width the designer intended cannot report that a different width is unusable.
// Assertion — the tick is one cycle wide, as Chapter 8.1 requires.
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 tick is exactly the carry, one cycle delayed. This is the
// property that fails when the sum is declared too narrow and the carry is
// silently discarded: baud_tick_o would simply never assert.
property p_tick_follows_carry;
@(posedge clk) disable iff (!rst_n)
baud_tick_o == $past(sum[ACC_W]);
endproperty
assert property (p_tick_follows_carry);
// Assertion — the phase register never exceeds the accumulator's modulus.
// Trivially true by width, and stated so a later widening cannot break it.
property p_phase_in_range;
@(posedge clk) disable iff (!rst_n)
phase_q < (1 <<< ACC_W);
endproperty
assert property (p_phase_in_range);12. Integer or Fractional?
Neither wins universally, and the comparison below is what the decision should be made from.
| Integer divider | Fractional accumulator | |
|---|---|---|
| Hardware | counter, comparator, incrementer | phase register, wide adder, carry |
| Adder width | $clog2(DIV_MAX) — ~10–16 bits | ACC_W + 1 — 17–33 bits |
| Rate accuracy | set by N; 64 ppm at 100 MHz / 115,200 | set by ACC_W; −18 ppm at 24 bits, −6,589 ppm at 16 |
| Every interval | identical | one of two values |
| Phase error in a frame | grows linearly | bounded by one clock |
| Verification | exact spacing, no tolerance | exact interval set, no tolerance |
| Runtime reconfiguration | write a divisor | write an increment — same mechanism |
| Fails badly when | DIV_MAX sized from the fastest rate | ACC_W chosen too narrow |
Choose the integer divider when the residual error already fits the budget Chapter 4.5 computes for the link — which at 100 MHz and 115,200 baud it comfortably does, since 64 ppm against a far end specified at ±0.5% is 1.3% of the endpoint's own error. It is the right default, and Chapter 4.3 §3 already made that argument arithmetically.
Choose the accumulator when the clock and rate are genuinely awkward — an ungenerous crystal, a high rate where the divisor is small and its relative rounding error correspondingly large, or a design that must support many rates from one clock and cannot tune each. Note the second case is the one where the integer divider degrades: a divisor of 100 has a far worse relative rounding error than a divisor of 868.
And check INC before committing to the accumulator. If it is not comfortably larger than the integer divisor it replaces, the fractional form is buying nothing — and at ACC_W = 16 on this clock it is actively losing.
13. What This Means on an FPGA
The adder is the cost, and it is modest. A 25-bit adder at ACC_W = 24 is a few carry-chain slices on any modern device. It runs at the fabric clock, which for a UART is the easiest timing path in the design.
Width costs are linear and small — choose from the error, not from caution. Going from 20 to 24 bits costs four flip-flops and four adder bits and improves the residual from +33.5 to −18.3 ppm. Going from 24 to 32 costs eight more and reaches −0.1 ppm, which is far below the far end's own oscillator tolerance and therefore buys nothing a link can use. 32 bits is not the safe choice; it is the unexamined one.
Constant folding does most of the work. INC is a localparam, so the adder has one constant operand and synthesis simplifies accordingly. That is a reason to prefer the elaboration-time form where runtime reconfiguration is not required — a point Chapter 8.5 develops.
Probe the tick and count it against a known window. The accumulator's internal phase is not useful on a logic analyser — it changes every cycle and means nothing in isolation. Counting ticks over a known number of fabric clocks measures the thing that matters, and the expected count is the derived expression from §7.
14. Debugging
15. Understanding Check
16. Summary
A phase accumulator advances by INC each fabric clock on a circle of circumference 2^ACC_W, and the carry out of the accumulator is the timing event. The remainder below the carry is the next interval's fractional part, preserved rather than discarded.
f_tick = f_clk × INC / 2^ACC_W INC = round(BAUD_HZ × 2^ACC_W / CLK_HZ)The quantisation moves from the interval length to the increment, and that is the whole benefit — INC = 19,327 rounds far more precisely than N = 868.
But only if the accumulator is wide enough. At ACC_W = 16 on a 100 MHz clock the increment is 75, the error is −0.659%, and the intervals are 873 and 874 rather than 868 and 869 — a hundred times worse than the integer divider it replaced, in a block that looks more sophisticated. The needed width is a property of the clock/rate pair: at 50 MHz the same rate is fine at 16 bits.
Intervals are always one of exactly two integers, bracketing 2^ACC_W / INC — not the ideal interval; the gap between those two is the residual error. The sequence is deterministic, repeating with a known period, so the right check has no tolerance band at all.
Frequency error and phase error are different things. The accumulator improves the frequency error that accumulates across a frame and slightly worsens the phase error that does not, which is why it is worth its adder.
The sum must be one bit wider than the accumulator. Otherwise the carry is discarded, the phase sequence remains perfectly correct, and the generator emits nothing.
Verified in simulation: three widths, three million fabric clocks, zero intervals outside the predicted sets, tick counts within the derived ±1 bound.
17. What Comes Next
Both generators produce one tick per bit interval. The transmitter of Module 7 wants exactly that. The receiver of Module 6 wants OVERSAMPLE of them — sixteen times as many — because Chapter 5.3 needed a grid of positions inside each interval to find a boundary it was never told about.
Chapter 8.4 resolves that asymmetry. It derives the receiver's required tick rate, compares independent generators against a shared high-rate base with a divider for the transmitter, and settles the question Chapter 7.3 §4 deliberately left open. It also draws a line that is easy to blur: sharing a generator does not reduce the receiver's phase uncertainty at all, because that uncertainty comes from the far end's clock and not from ours.
Browse the full path on the UART tutorials index. For the remainder-keeping arithmetic this chapter implements, read back to Chapter 4.4.
Continue learning
Related tutorials
- Related topic
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.
- 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.
