Skip to content
VLSI Mentor

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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
every fabric clock:    phase  <-  phase + INC
when phase overflows:  emit a tick, and keep the remainder

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
                 INC
f_tick  =  f_clk × ─────────           [Hz] = [Hz] × [dimensionless]
                   2^ACC_W

Setting f_tick to the target baud and solving:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
                 BAUD_HZ × 2^ACC_W
INC  =  round( ───────────────────── )      [dimensionless]
                      CLK_HZ

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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_Wideal INCINC usedactual bauderrorppm
1675.497575114,440.9180−0.65893 %−6,589
201,207.95961,208115,203.8574+0.00335 %+33.5
2419,327.352819,327115,197.8970−0.00183 %−18.3
324,947,802.32504,947,802115,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_clkbaudACC_WINCactual bauderrorppm
50 MHz115,20016151115,203.8574+0.00335 %+33.5
50 MHz115,2002438,655115,200.8772+0.00076 %+7.6
100 MHz1,000,00016655999,450.6836−0.05493 %−549.3
100 MHz1,000,00024167,772999,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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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_WINCideal interval2^W / INCobserved intervalsdistribution
2419,327868.0556868.0714868, 86992.83 % / 7.17 %
201,208868.0556868.0265868, 86997.33 % / 2.67 %
1675868.0556873.8133873, 87418.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 dividerFractional accumulator
Every intervalidentical, exactly N clocksone of two values
Phase error within a framegrows linearlybounded by one clock
Long-term frequency errorfixed by Nfar smaller, set by ACC_W
Which one a UART frame cares aboutfrequency — 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------------
//  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
endmodule
A fractional baud generator built as a phase accumulator. A phase register holds the current position within the bit interval. Each fabric clock its value is added to a constant increment derived at elaboration from the target baud rate, the clock frequency and the accumulator width. The addition is performed at one bit wider than the accumulator so that the carry out of the top bit is preserved rather than discarded. The bits below that carry are written back into the phase register, which is arithmetically the same as subtracting the accumulator modulus and is what preserves the fractional remainder between intervals. The carry bit itself is registered and becomes the one-cycle baud tick output. No comparator appears anywhere in the structure.phase_qACC_W bitsadderACC_W+1 bitssumthe extra bitbaud_tick_oregistered carryINClocalparamclkevery cyclesum[ACC_W-1:0]remainder keptback to phase= subtract 2^ACC_Wphaseconstantsum[ACC_W]next phase12
Figure 1 — the accumulator's structure. There is no comparator anywhere: the phase register feeds an adder one bit wider than itself, the carry out of that adder is the timing event, and the bits below the carry become the next phase. That extra bit is the whole design — without it the carry has nowhere to go and the generator emits nothing.

The width hazard that silently discards the carry

The single most important line is the declaration of sum:

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

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

cyclephase_q+ INCsumcarryphase_q afterbaud_tick_o
1066060
266120120
312618120
4268081
586140140
614620140
746100101
810616100
9066061

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 cycles
A trace of nine fabric clock cycles from a phase accumulator with a four-bit accumulator and an increment of six. The phase register advances by six each cycle, holding zero, six, twelve, two, eight, fourteen, four, ten and zero. Whenever the extended sum reaches or exceeds sixteen a carry is produced, which happens at cycles three, six and eight. The registered tick output pulses one cycle after each carry, at cycles four, seven and nine, giving interval lengths of three and two clocks which bracket the ideal interval of two point six six seven.sum 18 >= 16 — carrysum 18 >= 16 — carrysum exactly 16 — still a carrysum exactly 16 — still acarryclkphase_q061228144100sum612188142010166carrybaud_tick_ot0t1t2t3t4t5t6t7t8
Figure 2 — the accumulator wrapping, from simulation. Columns are FABRIC CLOCK CYCLES. The phase advances by INC every cycle and the carry out of the extended sum is the timing event; the tick appears one cycle later because it is registered. With ACC_W = 4 and INC = 6 the intervals are 3 and 2 clocks — the two integers bracketing 2^4/6 = 2.667 — which is the deterministic modulation of §4 at a scale small enough to read.

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
//===========================================================================
//  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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
//===========================================================================
//  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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
--==========================================================================
--  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;
PropertyVerilog-2001SystemVerilogVHDL-2008Agree?
accumulator widthACC_WACC_WACC_Wyes
sum widthACC_W+1ACC_W+1ACC_W+1yes
how the carry is made{1'b0, phase_q}{1'b0, phase_q}'0' & phase_qyes
how INC is widened{1'b0, INC[ACC_W-1:0]}cast to ACC_W+1to_unsigned(INC, ACC_W+1)yes
wrapdrop the carry bitsamesameyes
tickregistered carrysamesameyes
reset phase000yes
range guardnone$fatalassertno — 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:

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

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

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

  1. the reference model above, compared every clock — catches any arithmetic error, including ones that produce a plausible-looking tick stream;
  2. tick_probe from 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
//===========================================================================
//  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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
//===========================================================================
//  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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
--==========================================================================
--  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;
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
$ 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 PASSED

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

CheckWhy 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 / INCand 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):

MutationWhat it breaksFailures
accumulator discards the remainder on every tick§1's whole mechanism19
increment off by one§2's rate equation10
accumulator resets half-full, not to 0reproducibility (8.5 §4)9
carry width: INC[ACC_W:0]§7's real defect4

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:

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

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
expected ticks  =  floor(C × INC / 2^ACC_W)      ± 1 for the start-up phase

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

CheckRun length neededWhy
every interval is in the two-value setanya per-interval law — one violation is a defect
tick count against the derived expressionthousands of ticksthe ±1 start-up term must become negligible
the distribution between the two lengthsone full periodshorter 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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 dividerFractional accumulator
Hardwarecounter, comparator, incrementerphase register, wide adder, carry
Adder width$clog2(DIV_MAX) — ~10–16 bitsACC_W + 1 — 17–33 bits
Rate accuracyset by N; 64 ppm at 100 MHz / 115,200set by ACC_W; −18 ppm at 24 bits, −6,589 ppm at 16
Every intervalidenticalone of two values
Phase error in a framegrows linearlybounded by one clock
Verificationexact spacing, no toleranceexact interval set, no tolerance
Runtime reconfigurationwrite a divisorwrite an increment — same mechanism
Fails badly whenDIV_MAX sized from the fastest rateACC_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.

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

Where this fits

Part of the UART curriculum.