UART · Module 8
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.
Chapter 7.3 §6 raised an asymmetry and deliberately declined to resolve it:
| Receiver | Transmitter | |
|---|---|---|
| Ticks per bit interval | OVERSAMPLE — typically 16 | 1 |
| Why | must find a boundary it was never told about | defines the boundary |
Both halves need timing from the same fabric clock and the same nominal rate, and they need it at rates that differ by a factor of sixteen. That is the question this chapter settles.
It has an answer that is not obvious, and it is worth stating up front because it inverts the usual intuition: sharing one generator is the better architecture — but only if the shared generator is fractional. A shared integer base is dramatically worse than two independent integer dividers, for a reason that follows directly from Chapter 8.3 §3.
1. The Receiver's Rate
From Chapter 5.3, the receiver divides each bit interval into M positions, so it needs M enables per interval:
f_sample = BAUD_HZ × M [Hz] = [baud] × [dimensionless]At 115,200 baud and M = 16:
f_sample = 115,200 × 16 = 1,843,200 Hz1,843,200 sample enables per second, against the transmitter's 115,200. That number should look familiar: Chapter 4.2 showed 1.8432 MHz is itself a standard crystal frequency, which is exactly why 16× became conventional — a board with that crystal gets the receiver's tick with no division at all.
On a 100 MHz clock it is not free, and the divisor is where the trouble starts:
| baud | M | sample rate | ideal divisor @ 100 MHz | integer divisor | rate error |
|---|---|---|---|---|---|
| 9,600 | 16 | 153,600 | 651.0417 | 651 | +0.0064 % |
| 115,200 | 8 | 921,600 | 108.5069 | 109 | −0.4523 % |
| 115,200 | 16 | 1,843,200 | 54.2535 | 54 | +0.4694 % |
| 1,000,000 | 16 | 16,000,000 | 6.2500 | 6 | +4.1667 % |
The oversample divisor is M times smaller than the bit-rate divisor, so by Chapter 8.3 §3's law its relative rounding error is roughly M times larger. At 115,200 and 16× the divisor is 54 and the error is 0.47% — against 0.0064% for the bit-rate divisor of 868.
This is the fact the whole chapter turns on.
2. Three Architectures
A — independent generators. One for the receiver at M × BAUD, one for the transmitter at BAUD.
B — shared base. One generator at M × BAUD feeding the receiver directly, with a divide-by-M counter producing the transmitter's enable.
C — shared parameters, separate state. Two generators, but their divisors or increments derived from one set of parameters at elaboration.
They differ in resources, in coupling, and — the part that decides it — in accuracy.
3. The Accuracy Result
Machine-computed at 100 MHz and 115,200 baud, M = 16:
| Architecture | TX rate | TX error | vs independent |
|---|---|---|---|
A — independent integer dividers (N = 868) | 115,207.3733 | +0.0064 % | baseline |
B — shared integer base (N = 54, then ÷16) | 115,740.7407 | +0.4694 % | 73× worse |
B — shared fractional base, ACC_W = 16 | 115,203.8574 | +0.00335 % | 1.9× better |
B — shared fractional base, ACC_W = 24 | 115,200.1321 | +0.00011 % | 58× better |
4. What Sharing Does Not Fix
This is the distinction most easily blurred, and getting it wrong leads to a real design error.
A shared generator makes the receiver's and transmitter's enables coherent with each other. It does nothing whatever about the receiver's phase relative to the incoming data.
The receiver's sampling positions are anchored to the start edge it detects on the wire (Chapter 5.2 §4, Chapter 6.1 §4). That edge is produced by a different device with a different oscillator. No amount of internal generator sharing changes where it falls, how uncertain its observation is, or how the far end's frequency error accumulates across the frame:
| Quantity | Source | Does sharing help? |
|---|---|---|
δ_grid — placement onto the tick grid | our M | no — set by M, not by sharing |
δ_origin — quantisation of the observed edge | our clk period | no |
ε_rel — relative rate mismatch | the far end's oscillator | no |
our own contribution to ε_rel | our generator's accuracy | yes — this one |
Only the last row improves, and §3 quantifies by how much. Everything Chapter 5.5 showed about sampling drift across a frame is untouched — the receiver still re-anchors on every start bit, still places samples on a grid with 1/M resolution, and still accumulates displacement at n × ε_rel.
The practical error this prevents: concluding that because one generator now drives both halves, an internal loopback test proves the timing is correct. It proves the two halves agree with each other, which they would even if the shared generator were 5% off. Only a test against an independent far end, or a measurement of the actual tick rate, checks the thing that matters.
5. Choosing
| A — independent | B — shared base | C — shared parameters | |
|---|---|---|---|
| Generators | 2 | 1 + a small counter | 2 |
| Accuracy | each independent | excellent if fractional, poor if integer | each independent |
| RX and TX rates locked? | no — can drift by their rounding | yes, exactly | no |
| Different RX/TX rates possible? | yes | no — locked to M | yes |
| Resource cost | 2 adders/counters | 1 adder + $clog2(M) counter | 2 |
| Failure mode | two divisors to get right | one wrong base ruins both | parameters drift apart |
Architecture B with a fractional base is the recommendation for an ordinary UART, where both halves run at the same configured rate: it is the cheapest, the most accurate, and it guarantees the two enables cannot disagree.
Architecture A is right when the two halves may run at different rates — an uncommon but real configuration, and one a shared base structurally cannot support because the transmitter's rate is defined as the base divided by M.
Architecture C is the weakest of the three. It pays for two generators without buying the independence that justifies them, and it introduces a maintenance hazard: two derivations that must stay consistent, with nothing enforcing it.
6. The Composed RTL
// ---------------------------------------------------------------------------
// 8.4 — composed timing: one oversample base serving RX directly and TX
// through a divide-by-OVERSAMPLE. Illustrative composition, not a UART IP.
// ---------------------------------------------------------------------------
module uart_timing_gen #(
parameter int unsigned CLK_HZ = 100_000_000,
parameter int unsigned BAUD_HZ = 115_200,
parameter int unsigned OVERSAMPLE = 16
) (
input logic clk,
input logic rst_n,
output logic os_tick_o, // OVERSAMPLE per bit interval — RX
output logic baud_tick_o // 1 per bit interval — TX (a decode)
);
localparam int unsigned OS_W = (OVERSAMPLE <= 1) ? 1 : $clog2(OVERSAMPLE);
initial begin
if (OVERSAMPLE < 1)
$fatal(1, "uart_timing_gen: OVERSAMPLE must be >= 1");
if (longint'(BAUD_HZ) * OVERSAMPLE > longint'(CLK_HZ))
$fatal(1, "uart_timing_gen: %0d x %0d exceeds CLK_HZ %0d",
BAUD_HZ, OVERSAMPLE, CLK_HZ);
end
// The base runs at OVERSAMPLE x BAUD_HZ. RX consumes it directly.
uart_baud_tick_frac #(
.CLK_HZ (CLK_HZ),
.BAUD_HZ (BAUD_HZ * OVERSAMPLE),
.ACC_W (24)
) u_base (
.clk (clk), .rst_n (rst_n), .baud_tick_o (os_tick_o)
);
// TX derives its bit enable by counting OVERSAMPLE base ticks. This costs
// one small counter and guarantees the two enables cannot drift apart.
logic [OS_W-1:0] os_cnt_q;
// baud_tick_o is a DECODE of the oversample tick, not a register loaded
// by it. Registering it puts the transmit enable one clock AFTER the
// oversample tick it is supposed to coincide with, and the two enables
// then never align — see §8.
assign baud_tick_o = os_tick_o && (os_cnt_q == OS_W'(OVERSAMPLE - 1));
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n)
os_cnt_q <= '0;
else if (os_tick_o)
os_cnt_q <= (os_cnt_q == OS_W'(OVERSAMPLE - 1)) ? '0 : os_cnt_q + 1'b1;
end
endmoduleFour details are deliberate.
The base is the fractional generator, not the integer one. §3 is the whole argument. Instantiating Chapter 8.2's divider here would make the composition 73× less accurate than not sharing at all — the single most consequential line in the block.
BAUD_HZ * OVERSAMPLE is passed to the base, so the base generator needs no knowledge of oversampling. It produces a rate; the composition decides what that rate means.
The elaboration guard checks BAUD_HZ × OVERSAMPLE ≤ CLK_HZ in longint, because that product overflows 32 bits for high rates and large factors, and a wrapped comparison would pass a configuration that cannot work. At 1 Mbaud and 16× the required base is 16 MHz — a sixth of a 100 MHz clock, which is fine; at 8 Mbaud it would exceed it, and the guard says so rather than silently producing an unreachable rate.
The TX counter divides the base, it does not generate independently. That is what locks the two enables together: every sixteenth os_tick_o is a baud_tick_o — the same clock edge, not a related one. Simulation over three million fabric clocks: 55,296 oversample ticks and 3,456 baud ticks, a ratio of exactly 16.0000, with every one of those 3,456 baud ticks coinciding with an oversample tick and the baud interval measured at 868–869 clocks as the fractional base requires.
That last clause is not decoration, and §8 is the reason it is stated separately from the ratio.
7. Configuring Two Halves at One Rate
A UART link normally runs both directions at the same rate, which raises a design question the composition above answers implicitly.
One rate parameter, or two? With a shared base there is structurally only one — the transmitter's rate is the base divided by M, so there is nothing separate to configure. That removes an entire class of bug: a receiver and transmitter configured to different rates by a software error cannot happen, because there is no second register to disagree.
The cost is that they cannot be different when that is wanted. Split rates are unusual but not unheard of — a device streaming telemetry fast while accepting commands slowly. A design that must support it needs architecture A and two configurations, and it should then also verify the two independently rather than assuming a single loopback test covers both.
Whichever is chosen, the rate should be one field, not a divisor and an increment. Exposing N_DIV or INC directly to software leaks the generator's architecture into its interface: changing from an integer divider to a fractional one would then change the register map. Exposing a rate and deriving the generator value keeps that free. Where that derivation happens — elaboration or at runtime — is Chapter 8.5's subject, and the register interface itself belongs to Module 13.
8. The Phase Contract, and How It Was Broken
The ratio and the phase are two different claims, and only one of them is obvious.
The ratio says there are exactly OVERSAMPLE oversample ticks per baud tick.
The phase says each baud tick lands on the same clock edge as the
oversample tick that completed the count. A design can satisfy the first and
fail the second, and it will look completely correct while doing so.
This chapter's own RTL failed it. The listing in §6 originally read:
// WRONG — and the comment above it claimed the opposite.
if (os_tick_o) begin
if (os_cnt_q == OS_W'(OVERSAMPLE - 1)) begin
os_cnt_q <= '0;
baud_tick_o <= 1'b1; // lands ONE CLOCK LATER
end
endbaud_tick_o is assigned non-blocking inside a clocked process, so it becomes
visible on the following edge — by which time os_tick_o has gone low again.
Measured over three million fabric clocks:
os ticks : 55296
baud ticks : 3456 (ratio 16.0000)
baud ticks that COINCIDE with an os tick : 0
baud ticks that do NOT : 3456The ratio is perfect and the phase is wrong in every single interval. A rate measurement — the obvious test — passes. A frame sent through it decodes correctly. Nothing downstream in this UART misbehaves, because §4 already established that the receiver's phase comes from the detected start edge and not from this generator.
Every baud tick is also an oversample tick
10 cyclesRead the last two rows at cycle 12. Both are high on the same column. That single fact is the phase contract, and it is the one thing a rate measurement cannot see.
So why fix it? Because the comment in the RTL asserted a property the code did not have, and a reader integrating this block is entitled to rely on comments. The moment something does depend on the alignment — a shared sample-point decoder, a debug port that timestamps both enables, an assertion written by the next engineer — the design is wrong and the evidence for why it should have been right is sitting in the source.
The fix is to make the tick a decode rather than a register, which is what §6 now shows. It costs one comparator output on a fan-out of a few registers, and it makes the comment true.
9. The Composition in Three Languages
//===========================================================================
// uart_timing_gen_v — Synthesizable Verilog-2001
//
// The reference architecture: ONE accumulator runs at OVERSAMPLE x BAUD_HZ
// and drives the receiver's oversample enable directly; the transmitter's
// bit enable is derived by COUNTING OVERSAMPLE of those pulses.
//
// The phase contract that buys: every baud_tick_o coincides with an
// os_tick_o, so the two enables cannot drift apart by construction.
//===========================================================================
module uart_timing_gen_v #(
parameter CLK_HZ = 100000000,
parameter BAUD_HZ = 115200,
parameter OVERSAMPLE = 16,
parameter ACC_W = 24
) (
input wire clk,
input wire rst_n,
output wire os_tick_o, // OVERSAMPLE per bit interval — RX
output wire baud_tick_o // one per bit interval — TX
);
function integer clog2;
input integer value;
integer i;
begin
clog2 = 1;
for (i = value - 1; i > 1; i = i >> 1) clog2 = clog2 + 1;
end
endfunction
localparam OS_W = (OVERSAMPLE <= 1) ? 1 : clog2(OVERSAMPLE);
// INC = round(BAUD_HZ * OVERSAMPLE * 2^ACC_W / CLK_HZ).
//
// The intermediate MUST be computed 64 bits wide. BAUD_HZ*OVERSAMPLE is
// 1,843,200 here, and shifting that left by ACC_W=24 gives 3.09e13 --
// far outside 32 bits. Verilog-2001 sizes a constant expression from its
// context, so writing it inline invites a silent truncation that differs
// between tools. An explicit 64-bit constant function removes the
// ambiguity: every intermediate is declared, not inferred.
function [63:0] calc_inc;
input [63:0] baud_os;
input [63:0] clk_hz;
input integer acc_w;
reg [63:0] scaled;
begin
scaled = baud_os << acc_w; // 64-bit, explicit
calc_inc = (scaled + (clk_hz >> 1)) / clk_hz; // round to nearest
end
endfunction
localparam INC = calc_inc(BAUD_HZ * OVERSAMPLE, CLK_HZ, ACC_W);
uart_baud_nco_v #(.ACC_W(ACC_W), .INC(INC)) u_base (
.clk(clk), .rst_n(rst_n), .tick_o(os_tick_o));
reg [OS_W-1:0] os_cnt_q;
// baud_tick_o is a DECODE of the os tick, not a register loaded by it.
// A registered version would assert one clock later and the two enables
// would never coincide -- which is what the phase contract forbids, and
// what the testbench caught when this was written the obvious way.
assign baud_tick_o = os_tick_o && (os_cnt_q == (OVERSAMPLE[OS_W-1:0] - 1'b1));
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
os_cnt_q <= {OS_W{1'b0}};
end else if (os_tick_o) begin
if (os_cnt_q == (OVERSAMPLE[OS_W-1:0] - 1'b1))
os_cnt_q <= {OS_W{1'b0}};
else
os_cnt_q <= os_cnt_q + 1'b1;
end
end
endmodule//===========================================================================
// uart_timing_gen_sv — Synthesizable SystemVerilog
//
// ONE accumulator at OVERSAMPLE x BAUD_HZ drives the receiver's sample
// enable; the transmitter's bit enable is derived by counting OVERSAMPLE of
// those pulses.
//
// Phase contract: every baud_tick_o coincides with an os_tick_o. Two
// independent generators could not promise that.
//===========================================================================
module uart_timing_gen_sv #(
parameter int CLK_HZ = 100_000_000,
parameter int BAUD_HZ = 115_200,
parameter int OVERSAMPLE = 16,
parameter int ACC_W = 24
) (
input logic clk,
input logic rst_n,
output logic os_tick_o, // OVERSAMPLE per bit interval -- RX
output logic baud_tick_o // one per bit interval -- TX
);
// The intermediate is 3.09e13 for the default parameters. longint is
// explicit about the width so no tool has to guess; a 32-bit int here
// would truncate silently.
localparam longint unsigned SCALED =
longint'(BAUD_HZ) * longint'(OVERSAMPLE) * (longint'(1) << ACC_W);
localparam int INC = int'((SCALED + CLK_HZ/2) / CLK_HZ);
localparam int OS_W = (OVERSAMPLE <= 1) ? 1 : $clog2(OVERSAMPLE);
uart_baud_nco_sv #(.ACC_W(ACC_W), .INC(INC)) u_base (
.clk(clk), .rst_n(rst_n), .tick_o(os_tick_o));
logic [OS_W-1:0] os_cnt_q;
// A DECODE of the os tick, not a register loaded by it -- see the
// Verilog note. Registering it would put baud_tick_o one clock after
// the os tick it is supposed to coincide with.
assign baud_tick_o = os_tick_o && (os_cnt_q == OS_W'(OVERSAMPLE-1));
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n)
os_cnt_q <= '0;
else if (os_tick_o)
os_cnt_q <= (os_cnt_q == OS_W'(OVERSAMPLE-1)) ? '0 : os_cnt_q + 1'b1;
end
endmodule--==========================================================================
-- uart_timing_gen -- Synthesizable VHDL-2008
--
-- ONE accumulator at OVERSAMPLE x BAUD_HZ drives the receiver's sample
-- enable; the transmitter's bit enable is derived by counting OVERSAMPLE
-- of those pulses.
--
-- Phase contract: every baud_tick_o coincides with an os_tick_o.
--==========================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use ieee.math_real.all;
entity uart_timing_gen is
generic (
CLK_HZ : positive := 100_000_000;
BAUD_HZ : positive := 115_200;
OVERSAMPLE : positive := 16;
ACC_W : positive := 24
);
port (
clk : in std_logic;
rst_n : in std_logic;
os_tick_o : out std_logic; -- OVERSAMPLE per bit interval -- RX
baud_tick_o : out std_logic -- one per bit interval -- TX
);
end entity uart_timing_gen;
architecture rtl of uart_timing_gen is
-- VHDL's universal_integer is only guaranteed to 2**31-1, and the
-- numerator here is 3.09e13. real carries 53 bits of mantissa, which is
-- ample for a value that must only be correct to the nearest integer,
-- and it is evaluated entirely at elaboration.
function calc_inc (clk_hz, baud_hz, os, acc_w : positive) return positive is
variable scaled : real;
begin
scaled := real(baud_hz) * real(os) * 2.0 ** real(acc_w) / real(clk_hz);
return positive(round(scaled));
end function;
function cnt_width (n : positive) return positive is
begin
if n <= 2 then
return 1;
else
return positive(ceil(log2(real(n))));
end if;
end function;
constant INC : positive := calc_inc(CLK_HZ, BAUD_HZ, OVERSAMPLE, ACC_W);
constant OS_W : positive := cnt_width(OVERSAMPLE);
signal os_tick_s : std_logic;
signal os_cnt_q : unsigned(OS_W-1 downto 0);
begin
u_base : entity work.uart_baud_nco
generic map (ACC_W => ACC_W, INC => INC)
port map (clk => clk, rst_n => rst_n, tick_o => os_tick_s);
os_tick_o <= os_tick_s;
-- A DECODE of the os tick, not a register loaded by it. Registering it
-- would put baud_tick_o one clock after the os tick it must coincide
-- with, breaking the phase contract.
baud_tick_o <= '1' when os_tick_s = '1' and
os_cnt_q = to_unsigned(OVERSAMPLE - 1, OS_W)
else '0';
process (clk, rst_n)
begin
if rst_n = '0' then
os_cnt_q <= (others => '0');
elsif rising_edge(clk) then
if os_tick_s = '1' then
if os_cnt_q = to_unsigned(OVERSAMPLE - 1, OS_W) then
os_cnt_q <= (others => '0');
else
os_cnt_q <= os_cnt_q + 1;
end if;
end if;
end if;
end process;
end architecture rtl;The only genuine difference between the three is how INC is computed, and
it is a width problem in each of them. The numerator
BAUD_HZ x OVERSAMPLE x 2^ACC_W is 3.09 x 10^13 for the default
parameters — comfortably outside 32 bits — so every language needs to be told,
explicitly, to use something wider:
| how the wide intermediate is obtained | |
|---|---|
| Verilog-2001 | a constant function with declared [63:0] operands and locals |
| SystemVerilog | longint unsigned localparams |
| VHDL-2008 | real, then round — integer is only guaranteed to 2^31−1 |
None of the three can be written the obvious way. Verilog-2001 sizes a
constant expression from its context, so an inline
(BAUD_HZ * OVERSAMPLE) << ACC_W invites a truncation that differs between
tools; the constant function removes the ambiguity by declaring every
intermediate. All three were checked against exact rational arithmetic at four
configurations:
ACC_W=24 -> INC = 309238 expected 309238
ACC_W=16 -> INC = 1208 expected 1208
ACC_W=32 -> INC = 79164837 expected 79164837
50MHz/9600 os16 -> INC = 51540 expected 51540The composition, traced
OVERSAMPLE = 4, small enough to see the whole cycle. Printed by the
simulator:
TRACE-C uart_timing_gen_v, OVERSAMPLE=4 (the phase contract)
clock | os_tick | os_cnt_q | baud_tick
80 | 1 | 0 | 0
96 | 1 | 1 | 0
112 | 1 | 2 | 0
128 | 1 | 3 | 1 <- SAME edge, not the next one
144 | 1 | 0 | 0Every row with a baud tick also has an oversample tick. That is the phase contract, and it is visible in one column rather than inferred from two rates.
10. The Phase-Contract Testbench, in Three Languages
The composed generator makes two independent claims, and the testbench keeps them apart on purpose:
| Claim | Statement | Caught by |
|---|---|---|
| RATE | exactly OVERSAMPLE os ticks per baud tick | the per-interval count |
| PHASE | each baud tick is on the same edge as the os tick that completed the count | the coincidence check |
A design can satisfy RATE perfectly and fail PHASE in every interval — §8 measured exactly that. Separating them is what makes the second one findable.
Both configurations run simultaneously: 115,200 at 16× and 9,600 at 8×, so the oversampling factor is not accidentally hard-coded into the checks.
//===========================================================================
// tb_uart_timing_gen_v — self-checking Verilog-2001 testbench for the
// composed generator.
//
// The two claims this block makes are independent, and only one of them is
// obvious:
// RATE — there are exactly OVERSAMPLE os ticks per baud tick.
// PHASE — each baud tick lands on the SAME clock edge as the os tick
// that completed the count.
// A design can satisfy the first and fail the second while every rate
// measurement passes. The phase check below is the one that catches it.
//===========================================================================
`timescale 1ns/1ps
module tb_uart_timing_gen_v;
localparam RUN = 400000;
reg clk = 1'b0, rst_n = 1'b0;
always #5 clk = ~clk; // 100 MHz
integer checks = 0, failures = 0;
task check;
input cond;
input [8*56-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_int;
input integer got;
input integer want;
input [8*56-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
// Two configurations, deliberately different in both rate and factor.
wire os_a, bd_a, os_b, bd_b;
uart_timing_gen_v #(.CLK_HZ(100000000), .BAUD_HZ(115200),
.OVERSAMPLE(16), .ACC_W(24)) tg_a (
.clk(clk), .rst_n(rst_n), .os_tick_o(os_a), .baud_tick_o(bd_a));
uart_timing_gen_v #(.CLK_HZ(100000000), .BAUD_HZ(9600),
.OVERSAMPLE(8), .ACC_W(24)) tg_b (
.clk(clk), .rst_n(rst_n), .os_tick_o(os_b), .baud_tick_o(bd_b));
tick_probe p_os_a (.clk(clk), .rst_n(rst_n), .tick(os_a));
tick_probe p_bd_a (.clk(clk), .rst_n(rst_n), .tick(bd_a));
tick_probe p_os_b (.clk(clk), .rst_n(rst_n), .tick(os_b));
tick_probe p_bd_b (.clk(clk), .rst_n(rst_n), .tick(bd_b));
// THE PHASE CONTRACT, and the per-interval os count. Checked every
// clock for the whole run, not sampled at frame boundaries.
integer phase_viol_a = 0, phase_viol_b = 0;
integer between_a = 0, bad_count_a = 0;
integer between_b = 0, bad_count_b = 0;
always @(posedge clk) if (rst_n) begin
if (bd_a === 1'b1 && os_a !== 1'b1) phase_viol_a = phase_viol_a + 1;
if (bd_b === 1'b1 && os_b !== 1'b1) phase_viol_b = phase_viol_b + 1;
if (os_a === 1'b1) between_a = between_a + 1;
if (os_b === 1'b1) between_b = between_b + 1;
if (bd_a === 1'b1) begin
if (between_a != 16) bad_count_a = bad_count_a + 1;
between_a = 0;
end
if (bd_b === 1'b1) begin
if (between_b != 8) bad_count_b = bad_count_b + 1;
between_b = 0;
end
end
integer tick_in_reset = 0;
always @(posedge clk)
if (!rst_n && (os_a || bd_a || os_b || bd_b))
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 TIMING-GEN TESTS FAILED (timeout)");
$finish;
end
real ideal, got_avg, err_ppm;
initial begin
$display("== uart_timing_gen_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);
// --- the phase contract -------------------------------------------
check_int(phase_viol_a, 0, "115200/os16: every baud tick IS an os tick");
check_int(phase_viol_b, 0, "9600/os8: every baud tick IS an os tick");
// --- the rate contract --------------------------------------------
check_int(bad_count_a, 0, "115200/os16: exactly 16 os ticks per baud tick");
check_int(bad_count_b, 0, "9600/os8: exactly 8 os ticks per baud tick");
// --- pulse widths --------------------------------------------------
check_int(p_os_a.wide_pulse, 0, "115200: os tick one clock wide");
check_int(p_bd_a.wide_pulse, 0, "115200: baud tick one clock wide");
check_int(p_os_b.wide_pulse, 0, "9600: os tick one clock wide");
check_int(p_bd_b.wide_pulse, 0, "9600: baud tick one clock wide");
// --- interval spread: a fractional base gives two adjacent values --
check(p_bd_a.max_iv - p_bd_a.min_iv <= 1, "115200: baud interval spans <= 1 clock");
check(p_bd_b.max_iv - p_bd_b.min_iv <= 1, "9600: baud interval spans <= 1 clock");
// --- the number the module exists to produce -----------------------
got_avg = p_bd_a.sum_iv / (p_bd_a.n_ticks - 1);
ideal = 100000000.0 / 115200.0;
err_ppm = 1.0e6 * (got_avg - ideal) / ideal;
$display(" INFO 115200: %0.4f clocks/bit, ideal %0.4f, err %0.2f ppm",
got_avg, ideal, err_ppm);
check((err_ppm < 5.0) && (err_ppm > -5.0), "115200 measured rate within 5 ppm");
got_avg = p_bd_b.sum_iv / (p_bd_b.n_ticks - 1);
ideal = 100000000.0 / 9600.0;
err_ppm = 1.0e6 * (got_avg - ideal) / ideal;
$display(" INFO 9600: %0.4f clocks/bit, ideal %0.4f, err %0.2f ppm",
got_avg, ideal, err_ppm);
check((err_ppm < 20.0) && (err_ppm > -20.0), "9600 measured rate within 20 ppm");
// --- and the os stream is OVERSAMPLE times faster ------------------
// The run ends mid-interval, so between 0 and OVERSAMPLE-1 os ticks
// follow the last baud tick. Stating the bound is exact; asserting
// a bare 16x would be asserting that the run stopped on a boundary.
check((p_os_a.n_ticks - 16*p_bd_a.n_ticks >= 0) &&
(p_os_a.n_ticks - 16*p_bd_a.n_ticks <= 15),
"115200: os count = 16 x baud + r, 0 <= r <= 15");
check((p_os_b.n_ticks - 8*p_bd_b.n_ticks >= 0) &&
(p_os_b.n_ticks - 8*p_bd_b.n_ticks <= 7),
"9600: os count = 8 x baud + r, 0 <= r <= 7");
check(p_bd_a.n_ticks > 400, "115200: hundreds of bit intervals observed");
check(p_os_a.n_ticks > 6000, "115200: thousands of sample intervals observed");
// --- reset mid-run --------------------------------------------------
@(negedge clk) rst_n = 1'b0;
repeat (10) @(posedge clk);
check(os_a === 1'b0 && bd_a === 1'b0, "reset mid-run: both ticks return low");
@(negedge clk) rst_n = 1'b1;
repeat (20000) @(posedge clk);
check_int(phase_viol_a, 0, "phase contract still holds after re-reset");
$display("== %0d checks, %0d failures ==", checks, failures);
if (failures == 0) $display(" RESULT: ALL VERILOG TIMING-GEN TESTS PASSED");
else $display(" RESULT: VERILOG TIMING-GEN TESTS FAILED");
$finish;
end
endmodule//===========================================================================
// tb_uart_timing_gen_sv — self-checking SystemVerilog testbench for the
// composed generator.
//
// RATE and PHASE are checked as two separate claims, because a design can
// satisfy the first and fail the second while every rate measurement
// passes. Only the phase check catches a registered baud tick.
//===========================================================================
`timescale 1ns/1ps
module tb_uart_timing_gen_sv;
localparam int RUN = 400_000;
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 os_a, bd_a, os_b, bd_b;
uart_timing_gen_sv #(.CLK_HZ(100_000_000), .BAUD_HZ(115_200),
.OVERSAMPLE(16), .ACC_W(24)) tg_a (
.clk(clk), .rst_n(rst_n), .os_tick_o(os_a), .baud_tick_o(bd_a));
uart_timing_gen_sv #(.CLK_HZ(100_000_000), .BAUD_HZ(9_600),
.OVERSAMPLE(8), .ACC_W(24)) tg_b (
.clk(clk), .rst_n(rst_n), .os_tick_o(os_b), .baud_tick_o(bd_b));
tick_probe_sv p_os_a (.clk(clk), .rst_n(rst_n), .tick(os_a));
tick_probe_sv p_bd_a (.clk(clk), .rst_n(rst_n), .tick(bd_a));
tick_probe_sv p_os_b (.clk(clk), .rst_n(rst_n), .tick(os_b));
tick_probe_sv p_bd_b (.clk(clk), .rst_n(rst_n), .tick(bd_b));
int unsigned phase_viol_a = 0, phase_viol_b = 0;
int unsigned between_a = 0, bad_count_a = 0;
int unsigned between_b = 0, bad_count_b = 0;
always @(posedge clk) if (rst_n) begin
if (bd_a && !os_a) phase_viol_a++; // THE phase contract
if (bd_b && !os_b) phase_viol_b++;
if (os_a) between_a++;
if (os_b) between_b++;
if (bd_a) begin
if (between_a != 16) bad_count_a++;
between_a = 0;
end
if (bd_b) begin
if (between_b != 8) bad_count_b++;
between_b = 0;
end
end
int unsigned tick_in_reset = 0;
always @(posedge clk) if (!rst_n && (os_a || bd_a || os_b || bd_b)) 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 TIMING-GEN TESTS FAILED (timeout)");
$finish;
end
real ideal, got_avg, err_ppm;
initial begin
$display("== uart_timing_gen_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);
check_eq(phase_viol_a, 0, "115200/os16: every baud tick IS an os tick");
check_eq(phase_viol_b, 0, "9600/os8: every baud tick IS an os tick");
check_eq(bad_count_a, 0, "115200/os16: exactly 16 os ticks per baud tick");
check_eq(bad_count_b, 0, "9600/os8: exactly 8 os ticks per baud tick");
check_eq(p_os_a.wide_pulse, 0, "115200: os tick one clock wide");
check_eq(p_bd_a.wide_pulse, 0, "115200: baud tick one clock wide");
check_eq(p_os_b.wide_pulse, 0, "9600: os tick one clock wide");
check_eq(p_bd_b.wide_pulse, 0, "9600: baud tick one clock wide");
check(p_bd_a.max_iv - p_bd_a.min_iv <= 1, "115200: baud interval spans <= 1 clock");
check(p_bd_b.max_iv - p_bd_b.min_iv <= 1, "9600: baud interval spans <= 1 clock");
got_avg = p_bd_a.sum_iv / real'(p_bd_a.n_ticks - 1);
ideal = 100_000_000.0 / 115_200.0;
err_ppm = 1.0e6 * (got_avg - ideal) / ideal;
$display(" INFO 115200: %0.4f clocks/bit, ideal %0.4f, err %0.2f ppm", got_avg, ideal, err_ppm);
check(err_ppm < 5.0 && err_ppm > -5.0, "115200 measured rate within 5 ppm");
got_avg = p_bd_b.sum_iv / real'(p_bd_b.n_ticks - 1);
ideal = 100_000_000.0 / 9_600.0;
err_ppm = 1.0e6 * (got_avg - ideal) / ideal;
$display(" INFO 9600: %0.4f clocks/bit, ideal %0.4f, err %0.2f ppm", got_avg, ideal, err_ppm);
check(err_ppm < 20.0 && err_ppm > -20.0, "9600 measured rate within 20 ppm");
// The run ends mid-interval, so 0..OVERSAMPLE-1 os ticks follow the
// last baud tick. Asserting a bare 16x would assert that the run
// happened to stop on a boundary.
check(p_os_a.n_ticks - 16*p_bd_a.n_ticks <= 15,
"115200: os count = 16 x baud + r, 0 <= r <= 15");
check(p_os_b.n_ticks - 8*p_bd_b.n_ticks <= 7,
"9600: os count = 8 x baud + r, 0 <= r <= 7");
check(p_bd_a.n_ticks > 400, "115200: hundreds of bit intervals observed");
check(p_os_a.n_ticks > 6000, "115200: thousands of sample intervals observed");
@(negedge clk) rst_n = 1'b0;
repeat (10) @(posedge clk);
check(!os_a && !bd_a, "reset mid-run: both ticks return low");
@(negedge clk) rst_n = 1'b1;
repeat (20000) @(posedge clk);
check_eq(phase_viol_a, 0, "phase contract still holds after re-reset");
$display("== %0d checks, %0d failures ==", checks, failures);
if (failures == 0) $display(" RESULT: ALL SYSTEMVERILOG TIMING-GEN TESTS PASSED");
else $display(" RESULT: SYSTEMVERILOG TIMING-GEN TESTS FAILED");
$finish;
end
endmodule--==========================================================================
-- tb_uart_timing_gen -- self-checking VHDL-2008 testbench for the composed
-- generator.
--
-- RATE and PHASE are checked as two separate claims. Only the phase check
-- catches a registered baud tick, which is a defect with no other symptom.
--==========================================================================
library ieee;
use ieee.std_logic_1164.all;
use std.env.finish;
entity tb_uart_timing_gen is
end entity tb_uart_timing_gen;
architecture sim of tb_uart_timing_gen is
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 os_a, bd_a, os_b, bd_b : std_logic;
signal cc_osa, nt_osa, fl_osa, mn_osa, mx_osa, wd_osa : natural;
signal cc_bda, nt_bda, fl_bda, mn_bda, mx_bda, wd_bda : natural;
signal cc_osb, nt_osb, fl_osb, mn_osb, mx_osb, wd_osb : natural;
signal cc_bdb, nt_bdb, fl_bdb, mn_bdb, mx_bdb, wd_bdb : natural;
signal sm_osa, sm_bda, sm_osb, sm_bdb : real;
signal phase_viol_a, phase_viol_b : natural := 0;
signal bad_count_a, bad_count_b : natural := 0;
signal tick_in_reset : natural := 0;
begin
clk <= not clk after CLK_PERIOD / 2 when done = '0' else '0';
u_tg_a : entity work.uart_timing_gen
generic map (CLK_HZ => 100000000, BAUD_HZ => 115200,
OVERSAMPLE => 16, ACC_W => 24)
port map (clk => clk, rst_n => rst_n, os_tick_o => os_a, baud_tick_o => bd_a);
u_tg_b : entity work.uart_timing_gen
generic map (CLK_HZ => 100000000, BAUD_HZ => 9600,
OVERSAMPLE => 8, ACC_W => 24)
port map (clk => clk, rst_n => rst_n, os_tick_o => os_b, baud_tick_o => bd_b);
p_osa : entity work.tick_probe port map (clk, rst_n, os_a,
cc_osa, nt_osa, fl_osa, mn_osa, mx_osa, wd_osa, sm_osa);
p_bda : entity work.tick_probe port map (clk, rst_n, bd_a,
cc_bda, nt_bda, fl_bda, mn_bda, mx_bda, wd_bda, sm_bda);
p_osb : entity work.tick_probe port map (clk, rst_n, os_b,
cc_osb, nt_osb, fl_osb, mn_osb, mx_osb, wd_osb, sm_osb);
p_bdb : entity work.tick_probe port map (clk, rst_n, bd_b,
cc_bdb, nt_bdb, fl_bdb, mn_bdb, mx_bdb, wd_bdb, sm_bdb);
-- One process, one driver per signal.
mon : process (clk)
variable pva, pvb, bca, bcb, bwa, bwb, inr : natural := 0;
begin
if rising_edge(clk) then
if rst_n = '0' then
if os_a = '1' or bd_a = '1' or os_b = '1' or bd_b = '1' then
inr := inr + 1;
end if;
else
if bd_a = '1' and os_a /= '1' then pva := pva + 1; end if;
if bd_b = '1' and os_b /= '1' then pvb := pvb + 1; end if;
if os_a = '1' then bwa := bwa + 1; end if;
if os_b = '1' then bwb := bwb + 1; end if;
if bd_a = '1' then
if bwa /= 16 then bca := bca + 1; end if;
bwa := 0;
end if;
if bd_b = '1' then
if bwb /= 8 then bcb := bcb + 1; end if;
bwb := 0;
end if;
end if;
phase_viol_a <= pva;
phase_viol_b <= pvb;
bad_count_a <= bca;
bad_count_b <= bcb;
tick_in_reset <= inr;
end if;
end process mon;
watchdog : process
begin
wait for 40 ms;
report "watchdog: simulation did not finish" severity error;
report " RESULT: VHDL TIMING-GEN 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;
variable ideal, got_avg, err_ppm : real;
begin
report "== uart_timing_gen (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;
check_eq(phase_viol_a, 0, "115200/os16: every baud tick IS an os tick");
check_eq(phase_viol_b, 0, "9600/os8: every baud tick IS an os tick");
check_eq(bad_count_a, 0, "115200/os16: exactly 16 os ticks per baud tick");
check_eq(bad_count_b, 0, "9600/os8: exactly 8 os ticks per baud tick");
check_eq(wd_osa, 0, "115200: os tick one clock wide");
check_eq(wd_bda, 0, "115200: baud tick one clock wide");
check_eq(wd_osb, 0, "9600: os tick one clock wide");
check_eq(wd_bdb, 0, "9600: baud tick one clock wide");
check(mx_bda - mn_bda <= 1, "115200: baud interval spans <= 1 clock");
check(mx_bdb - mn_bdb <= 1, "9600: baud interval spans <= 1 clock");
got_avg := sm_bda / real(nt_bda - 1);
ideal := 100000000.0 / 115200.0;
err_ppm := 1.0e6 * (got_avg - ideal) / ideal;
report " INFO 115200: " & real'image(got_avg) & " clocks/bit, ideal " &
real'image(ideal) & ", err " & real'image(err_ppm) & " ppm";
check(abs(err_ppm) < 5.0, "115200 measured rate within 5 ppm");
got_avg := sm_bdb / real(nt_bdb - 1);
ideal := 100000000.0 / 9600.0;
err_ppm := 1.0e6 * (got_avg - ideal) / ideal;
report " INFO 9600: " & real'image(got_avg) & " clocks/bit, ideal " &
real'image(ideal) & ", err " & real'image(err_ppm) & " ppm";
check(abs(err_ppm) < 20.0, "9600 measured rate within 20 ppm");
-- The run ends mid-interval, so 0..OVERSAMPLE-1 os ticks follow the
-- last baud tick. A bare 16x would assert the run stopped on a
-- boundary, which it has no reason to do.
check(nt_osa - 16*nt_bda <= 15, "115200: os count = 16 x baud + r, 0 <= r <= 15");
check(nt_osb - 8*nt_bdb <= 7, "9600: os count = 8 x baud + r, 0 <= r <= 7");
check(nt_bda > 400, "115200: hundreds of bit intervals observed");
check(nt_osa > 6000, "115200: thousands of sample intervals observed");
rst_n <= '0';
for i in 0 to 9 loop wait until rising_edge(clk); end loop;
wait for 1 ns;
check(os_a = '0' and bd_a = '0', "reset mid-run: both ticks return low");
rst_n <= '1';
for i in 0 to 19999 loop wait until rising_edge(clk); end loop;
wait for 1 ns;
check_eq(phase_viol_a, 0, "phase contract still holds after re-reset");
report "== " & integer'image(checks) & " checks, " &
integer'image(failures) & " failures ==";
if failures = 0 then
report " RESULT: ALL VHDL TIMING-GEN TESTS PASSED";
else
report " RESULT: VHDL TIMING-GEN TESTS FAILED" severity error;
end if;
done <= '1';
finish;
end process stim;
end architecture sim;$ iverilog -g2001 -o sim_tg_v uart_baud_nco_v.v uart_timing_gen_v.v \
tick_probe_v.v tb_uart_timing_gen_v.v
$ ./sim_tg_v
== uart_timing_gen_v : self-checking Verilog testbench ==
PASS reset: no ticks while rst_n low
PASS 115200/os16: every baud tick IS an os tick (0)
PASS 9600/os8: every baud tick IS an os tick (0)
PASS 115200/os16: exactly 16 os ticks per baud tick (0)
PASS 9600/os8: exactly 8 os ticks per baud tick (0)
PASS 115200: baud interval spans <= 1 clock
INFO 115200: 868.0545 clocks/bit, ideal 868.0556, err -1.25 ppm
PASS 115200 measured rate within 5 ppm
INFO 9600: 10416.5946 clocks/bit, ideal 10416.6667, err -6.92 ppm
PASS 9600 measured rate within 20 ppm
PASS 115200: os count = 16 x baud + r, 0 <= r <= 15
PASS phase contract still holds after re-reset (0)
== 19 checks, 0 failures ==
RESULT: ALL VERILOG TIMING-GEN TESTS PASSEDAll three languages run 19 checks with 0 failures — the same 19.
Proving these checks can fail
| Mutation | What it breaks | Failures |
|---|---|---|
TX divide-down off by one (OVERSAMPLE, not -1) | §6's ratio | 6 |
| baud tick registered, not decoded | §8's phase contract | 3 |
The second row is the module's most instructive number. Three checks catch it, and all three are phase checks — the two coincidence checks and the post-reset repeat. Every rate check, every interval check, every count check and every pulse-width check still passes, because all of them are still true. A property that nothing observes is a property nothing protects.
11. Verification
Assert the ratio, exactly. Every M-th oversample tick must be a bit tick, with no drift over any run length. Counting both and checking the quotient is a stronger check than measuring either rate:
composed : os_tick=55312 baud_tick=3457 baud interval 868..869 clksFrom simulation over three million fabric clocks — a ratio of exactly 16, and baud intervals in the two-value set the fractional base predicts.
Check the base rate, not the derived rate. An error in the base propagates to both consumers, so measuring the transmitter's rate alone can mask which generator is wrong. Measure os_tick_o directly and compare against BAUD_HZ × M.
Do not verify timing by internal loopback alone. §4's point stated as a test procedure: a loopback exercises the receiver against the transmitter, both driven by the same generator, so it passes whatever that generator's rate happens to be. It is an excellent test of framing, bit order and handshake, and it is worthless as a test of rate accuracy. Rate must be checked against an independent reference or measured directly.
Sweep OVERSAMPLE. The divide-by-M counter's width is derived, and values that are not powers of two exercise the terminal comparison rather than a natural wrap — the same hazard Chapter 6.3 §2 identified in the receiver's phase counter.
// Assertion — a bit tick never occurs without a coincident oversample tick,
// which is what "subsample" means structurally.
property p_baud_implies_os;
@(posedge clk) disable iff (!rst_n)
baud_tick_o |-> os_tick_o;
endproperty
assert property (p_baud_implies_os);
// Assertion — the subsample counter stays within its modulus.
property p_os_count_in_range;
@(posedge clk) disable iff (!rst_n)
os_cnt_q <= OS_W'(OVERSAMPLE - 1);
endproperty
assert property (p_os_count_in_range);The first is worth writing because it fails on the most likely restructuring error: someone replacing the subsample counter with an independent generator "to decouple the two", which silently reintroduces the drift the composition existed to prevent.
12. What This Means on an FPGA
The sharing is nearly free. One 25-bit adder for the base and a 4-bit counter for the subsample, against two generators in the independent architecture. The saving is small in absolute terms and the accuracy gain is the real reason to prefer it.
The oversample enable has higher fanout and a higher toggle rate. It pulses 1,843,200 times per second rather than 115,200 and reaches every timing-dependent register in the receiver. At UART scale this is still unremarkable — a few dozen endpoints toggling at under 2 MHz — but it is the one signal in the design worth glancing at if a power or fanout report looks unexpected.
Probe os_tick_o and baud_tick_o together. The ratio is directly visible and is the first check on a composition: sixteen of one per one of the other, always. A ratio that is right but a rate that is wrong points at the base generator; a ratio that is wrong points at the subsample counter.
Size the base generator from the highest rate the design supports, not the nominal one. The guard catches BAUD_HZ × OVERSAMPLE > CLK_HZ at elaboration, and for a runtime-configurable rate the same relationship must hold for the fastest configuration — which is the composition's equivalent of Chapter 8.2's DIV_MAX argument, inverted.
13. Understanding Check
14. Summary
The receiver needs M ticks per bit interval and the transmitter needs one, so a shared generator must run at BAUD_HZ × M — 1,843,200 Hz at 115,200 baud and 16×, which is itself a standard crystal frequency and the reason 16× became conventional.
The oversample divisor is M times smaller than the bit-rate divisor, so its relative rounding error is roughly M times larger: 54 instead of 868, 0.47% instead of 0.0064%.
That single fact decides the architecture, and it points both ways:
| TX error at 100 MHz / 115,200 | vs independent | |
|---|---|---|
| independent integer dividers | +0.0064 % | baseline |
| shared integer base | +0.4694 % | 73× worse |
| shared fractional base, 24-bit | +0.00011 % | 58× better |
Share the base — with a fractional generator. It is cheaper, far more accurate, and it locks the two enables together so they cannot disagree. A shared integer base is the worst of the three options.
Sharing does not touch the receiver's phase uncertainty. The sampling phase comes from the start edge on the wire, produced by another device's oscillator. Sharing improves our own contribution to the rate mismatch and nothing else — and the practical consequence is that an internal loopback test cannot validate rate accuracy, because both halves are wrong together.
Verified in simulation: 55,312 oversample ticks to 3,457 bit ticks — exactly 16 — over three million fabric clocks.
15. What Comes Next
Four chapters have produced working generators and an architecture to compose them. What they have not done is state a contract: which parameter values are legal, what happens when an illegal one is supplied, what the generator does during and immediately after reset, and how a rate change should reach it safely.
Chapter 8.5 closes the module with exactly that. It defines the legal range and hardware consequence of every parameter; separates elaboration-time errors from runtime programming errors from accuracy warnings, which are three different things routinely conflated; specifies reset behaviour down to the first tick; and weighs compile-time specialisation against runtime programmability rather than building both and calling it flexible.
Browse the full path on the UART tutorials index. For the oversampling grid this chapter feeds, read back to Chapter 5.3; for the transmitter's single enable, Chapter 7.3.
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
Fractional Accumulator Baud Generation
A phase accumulator keeps the remainder an integer divider discards, so the average rate converges while individual intervals alternate between two lengths — and a too-narrow accumulator is worse than the divider it replaced.
- Related topic
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.
