UART · Module 17
Baud Mismatch and Sampling-Error Signatures
Why sampling error accumulates across a frame and corrupts the high bits first, the arithmetic that fixes the tolerance at 5.26 percent, and the measured drift table for five receiver dividers sharing one wire.
A UART has no clock line. The receiver finds one edge — the start bit — and from then on it is navigating by dead reckoning, stepping forward by its own idea of a bit period and hoping it stays inside the transmitter's. Every bit it samples is further from the last known-good reference than the one before.
That is why a baud mismatch has such a specific signature, and why the signature is so often misread. The corruption is not random and it is not uniform: it concentrates in the high bits of the byte, it depends on the payload, and it can leave some bytes completely intact while destroying others. This chapter measures all three effects with five receivers running on one wire.
1. The Lever Arm
A mid-bit-sampling receiver that has detected a falling edge at time zero samples data bit k at
t(k) = DIV_RX / 2 + (k + 1) * DIV_RXand the stop bit at DIV_RX/2 + 9 * DIV_RX. In bit periods, that is 1.5, 2.5, 3.5 … up to 8.5 for d7 and 9.5 for the stop bit.
Now suppose the receiver's bit period is wrong by a fraction e. Its sample for d7 lands 8.5 * e bit periods away from where it should, and the stop sample lands 9.5 * e away. The error the receiver makes is not e — it is e multiplied by how far into the frame it has walked.
A sample stays inside its intended bit as long as it has not drifted by half a bit period. So the constraint is
9.5 * |e| < 0.5 => |e| < 5.26%That is the budget for this receiver, at 8N1, and it is shared between both ends. It is also why UART framing is what it is: the start bit re-synchronises the receiver on every frame, which resets the lever arm to zero and stops the error accumulating across bytes. Without that, a 1% error would destroy the link within a hundred bits.
2. Where Dividers Actually Go Wrong
The divider is round(f_clk / baud), and the rounding is almost never the problem:
f_clk MHz baud ideal div div truncation error
--------- ------ --------- ----- ----------------
50 115200 434.03 434 -0.006%
100 115200 868.06 868 -0.006%
12 115200 104.17 104 -0.160%
16 9600 1666.67 1667 +0.020%
25 460800 54.25 54 -0.467%
48 921600 52.08 52 -0.160%Every one of those is comfortably inside 5.26%. Integer truncation on a sane clock is a fraction of a percent, and it is not what breaks links.
What breaks links is the assumed clock frequency being wrong. A board respun with a 24 MHz oscillator instead of 25 MHz, a PLL that did not lock and left the design on its reference, a divider constant copied from a project with a different clock — these produce errors of 4%, 20%, 100%, not 0.1%. When a UART is corrupting data, the divider constant is worth checking, but the actual clock frequency is worth measuring.
3. Five Receivers, One Wire
The experiment puts five instances of the same receiver on a single line driven at DIV_TX = 32 clocks per bit. Nothing about any receiver is faulty; they differ only in the divider each was told to use.
instance DIV_RX divider error
-------- ------ -------------
rx32 32 0.0%
rx31 31 -3.1%
rx33 33 +3.1%
rx30 30 -6.2%
rx34 34 +6.2%The ±3.1% pair sits inside the 5.26% budget and must work. The ±6.2% pair sits outside it and must not. Here is the receiver:
Verilog
// ---------------------------------------------------------------------------
// uart_skew_rx -- an ordinary mid-bit-sampling UART receiver, parameterised on
// its OWN divider so it can be run deliberately mismatched against the
// transmitter that is driving it.
//
// Nothing here is faulty. This is what every UART receiver does: find the
// falling edge, wait half a bit to land mid-start, then step one bit period at
// a time. The failure signature of a baud mismatch is produced entirely by the
// arithmetic of that stepping -- the sample point drifts by (DIV - DIV_TX)
// clocks per bit, and the drift ACCUMULATES. Bit 7 is sampled nine and a half
// receiver bit periods after the edge, so it carries nineteen times the error
// of the start bit. That is why the high bits of a byte corrupt first.
// ---------------------------------------------------------------------------
module uart_skew_rx #(
parameter DIV = 32 // this receiver's clocks per bit
)(
input wire clk,
input wire rst_n,
input wire line_i,
output reg [7:0] data_o, // last byte assembled
output reg valid_o, // one-clock strobe: a frame completed
output reg frame_err_o, // stop bit was not MARK
output reg false_start_o // start bit vanished before mid-bit
);
localparam S_IDLE = 2'd0,
S_START = 2'd1,
S_DATA = 2'd2,
S_STOP = 2'd3;
reg [1:0] state;
reg [15:0] cnt;
reg [3:0] idx;
reg line_q;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= S_IDLE;
cnt <= 16'd0;
idx <= 4'd0;
line_q <= 1'b1;
data_o <= 8'd0;
valid_o <= 1'b0;
frame_err_o <= 1'b0;
false_start_o <= 1'b0;
end else begin
line_q <= line_i;
valid_o <= 1'b0; // strobes are one clock wide
false_start_o <= 1'b0;
case (state)
S_IDLE: begin
if (line_q && !line_i) begin // falling edge = candidate start
state <= S_START;
cnt <= 16'd0;
end
end
S_START: begin
if (cnt == (DIV/2) - 1) begin
// Mid-start. If the line has already recovered, this
// was never a start bit.
if (line_i) begin
state <= S_IDLE;
false_start_o <= 1'b1;
end else begin
state <= S_DATA;
cnt <= 16'd0;
idx <= 4'd0;
end
end else begin
cnt <= cnt + 16'd1;
end
end
S_DATA: begin
if (cnt == DIV - 1) begin
data_o <= {line_i, data_o[7:1]}; // LSB first
cnt <= 16'd0;
if (idx == 4'd7) state <= S_STOP;
else idx <= idx + 4'd1;
end else begin
cnt <= cnt + 16'd1;
end
end
S_STOP: begin
if (cnt == DIV - 1) begin
valid_o <= 1'b1;
frame_err_o <= ~line_i; // stop must be MARK
state <= S_IDLE;
cnt <= 16'd0;
end else begin
cnt <= cnt + 16'd1;
end
end
default: state <= S_IDLE;
endcase
end
end
endmoduleSystemVerilog
// ---------------------------------------------------------------------------
// uart_skew_rx -- an ordinary mid-bit-sampling UART receiver, parameterised on
// its OWN divider so it can be run deliberately mismatched against the
// transmitter that is driving it.
//
// Nothing here is faulty. This is what every UART receiver does: find the
// falling edge, wait half a bit to land mid-start, then step one bit period at
// a time. The failure signature of a baud mismatch is produced entirely by the
// arithmetic of that stepping -- the sample point drifts by (DIV - DIV_TX)
// clocks per bit, and the drift ACCUMULATES. Bit 7 is sampled nine and a half
// receiver bit periods after the edge, so it carries nineteen times the error
// of the start bit. That is why the high bits of a byte corrupt first.
// ---------------------------------------------------------------------------
module uart_skew_rx #(
parameter int DIV = 32 // this receiver's clocks per bit
)(
input logic clk,
input logic rst_n,
input logic line_i,
output logic [7:0] data_o, // last byte assembled
output logic valid_o, // one-clock strobe: a frame completed
output logic frame_err_o, // stop bit was not MARK
output logic false_start_o // start bit vanished before mid-bit
);
localparam S_IDLE = 2'd0,
S_START = 2'd1,
S_DATA = 2'd2,
S_STOP = 2'd3;
logic [1:0] state;
logic [15:0] cnt;
logic [3:0] idx;
logic line_q;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= S_IDLE;
cnt <= '0;
idx <= '0;
line_q <= 1'b1;
data_o <= '0;
valid_o <= 1'b0;
frame_err_o <= 1'b0;
false_start_o <= 1'b0;
end else begin
line_q <= line_i;
valid_o <= 1'b0; // strobes are one clock wide
false_start_o <= 1'b0;
case (state)
S_IDLE: begin
if (line_q && !line_i) begin // falling edge = candidate start
state <= S_START;
cnt <= '0;
end
end
S_START: begin
if (cnt == (DIV/2) - 1) begin
// Mid-start. If the line has already recovered, this
// was never a start bit.
if (line_i) begin
state <= S_IDLE;
false_start_o <= 1'b1;
end else begin
state <= S_DATA;
cnt <= '0;
idx <= '0;
end
end else begin
cnt <= cnt + 1'b1;
end
end
S_DATA: begin
if (cnt == DIV - 1) begin
data_o <= {line_i, data_o[7:1]}; // LSB first
cnt <= '0;
if (idx == 4'd7) state <= S_STOP;
else idx <= idx + 1'b1;
end else begin
cnt <= cnt + 1'b1;
end
end
S_STOP: begin
if (cnt == DIV - 1) begin
valid_o <= 1'b1;
frame_err_o <= ~line_i; // stop must be MARK
state <= S_IDLE;
cnt <= '0;
end else begin
cnt <= cnt + 1'b1;
end
end
default: state <= S_IDLE;
endcase
end
end
endmoduleVHDL
-- ---------------------------------------------------------------------------
-- uart_skew_rx -- an ordinary mid-bit-sampling UART receiver, parameterised on
-- its OWN divider so it can be run deliberately mismatched against the
-- transmitter that is driving it.
--
-- Nothing here is faulty. This is what every UART receiver does: find the
-- falling edge, wait half a bit to land mid-start, then step one bit period at
-- a time. The failure signature of a baud mismatch is produced entirely by the
-- arithmetic of that stepping -- the sample point drifts by (DIV - DIV_TX)
-- clocks per bit, and the drift ACCUMULATES. Bit 7 is sampled nine and a half
-- receiver bit periods after the edge, so it carries nineteen times the error
-- of the start bit. That is why the high bits of a byte corrupt first.
-- ---------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity uart_skew_rx is
generic (
DIV : natural := 32 -- this receiver's clocks per bit
);
port (
clk : in std_logic;
rst_n : in std_logic;
line_i : in std_logic;
data_o : out std_logic_vector(7 downto 0); -- last byte assembled
valid_o : out std_logic; -- frame-complete strobe
frame_err_o : out std_logic; -- stop bit was not MARK
false_start_o : out std_logic -- start vanished by mid-bit
);
end entity uart_skew_rx;
architecture rtl of uart_skew_rx is
type state_t is (S_IDLE, S_START, S_DATA, S_STOP);
signal state : state_t := S_IDLE;
signal cnt : unsigned(15 downto 0) := (others => '0');
signal idx : unsigned(3 downto 0) := (others => '0');
signal line_q : std_logic := '1';
-- outputs are mirrored internally: an entity may not read its own outputs
signal data_r : std_logic_vector(7 downto 0) := (others => '0');
signal valid_r : std_logic := '0';
signal ferr_r : std_logic := '0';
signal fstart_r: std_logic := '0';
begin
data_o <= data_r;
valid_o <= valid_r;
frame_err_o <= ferr_r;
false_start_o <= fstart_r;
process (clk, rst_n)
begin
if rst_n = '0' then
state <= S_IDLE;
cnt <= (others => '0');
idx <= (others => '0');
line_q <= '1';
data_r <= (others => '0');
valid_r <= '0';
ferr_r <= '0';
fstart_r <= '0';
elsif rising_edge(clk) then
line_q <= line_i;
valid_r <= '0'; -- strobes are one clock wide
fstart_r <= '0';
case state is
when S_IDLE =>
if line_q = '1' and line_i = '0' then -- candidate start
state <= S_START;
cnt <= (others => '0');
end if;
when S_START =>
if cnt = to_unsigned((DIV/2) - 1, cnt'length) then
-- Mid-start. If the line has already recovered, this
-- was never a start bit.
if line_i = '1' then
state <= S_IDLE;
fstart_r <= '1';
else
state <= S_DATA;
cnt <= (others => '0');
idx <= (others => '0');
end if;
else
cnt <= cnt + 1;
end if;
when S_DATA =>
if cnt = to_unsigned(DIV - 1, cnt'length) then
data_r <= line_i & data_r(7 downto 1); -- LSB first
cnt <= (others => '0');
if idx = to_unsigned(7, idx'length) then
state <= S_STOP;
else
idx <= idx + 1;
end if;
else
cnt <= cnt + 1;
end if;
when S_STOP =>
if cnt = to_unsigned(DIV - 1, cnt'length) then
valid_r <= '1';
ferr_r <= not line_i; -- stop must be MARK
state <= S_IDLE;
cnt <= (others => '0');
else
cnt <= cnt + 1;
end if;
end case;
end if;
end process;
end architecture rtl;4. The Measured Drift Table
Before looking at any data, the testbench prints where each receiver's sample points actually land relative to the true bit centres. A bit is mis-sampled once the drift reaches half a bit period, which at DIV_TX = 32 is 16 clocks:
Sample-point drift from the true bit centre, in clocks
(DIV_TX = 32; a bit is mis-sampled once |drift| reaches 16)
bit : d0 d1 d2 d3 d4 d5 d6 d7 stop
-3.1% (DIV=31): -2 -3 -4 -5 -6 -7 -8 -9 -10
+3.1% (DIV=33): 1 2 3 4 5 6 7 8 9
-6.2% (DIV=30): -3 -5 -7 -9 -11 -13 -15 -17 -19
+6.2% (DIV=34): 3 5 7 9 11 13 15 17 19Read the rows left to right and the lever arm of §1 is visible as a straight line: the drift grows by a constant amount per bit, because each bit adds one more DIV_RX - DIV_TX of error.
Read the columns and the tolerance budget appears as a measurement rather than a formula. The ±3.1% rows reach 9 and 10 clocks at the stop bit — inside 16, so those links work. The ±6.2% rows cross 16 between d6 and d7, reaching 17 at d7 and 19 at the stop bit. The arithmetic of §1 predicted failure beyond 5.26%; the table shows where in the frame it arrives.
Sample-point drift at -6.2% divider error
11 cycles5. The Corruption Sweep
Three payloads, each sent once, read simultaneously by all five receivers:
sent DIV=32 DIV=31 DIV=33 DIV=30 DIV=34
---- ------ ------ ------ ------ ------
0x55 0x55 0x55 0x55 0xd5 0xd5
0xAA 0xaa 0xaa 0xaa 0x2a 0xaa
0x3C 0x3c 0x3c 0x3c 0x3c 0xbcThe ±3.1% columns are clean on every payload, as the budget requires. The ±6.2% columns are where it gets interesting, and there are two separate lessons in that small table.
The corruption is in the top bit. 0x55 became 0xd5 under both ±6.2% receivers: bit 7 flipped from 0 to 1, and nothing else changed. The testbench computes the index of the lowest differing bit and reports 7 for both. This is the lever arm again — d7 is the only data bit whose drift exceeded half a period.
The same error does not corrupt every payload. 0xAA survived +6.2% intact but became 0x2a at -6.2%. 0x3C did the exact opposite: intact at -6.2%, corrupted to 0xbc at +6.2%.
6. The Oracle Is a Model of Time, Not a Second Receiver
Testing a receiver against another receiver would prove only that two implementations of the same idea agree. The oracle here is instead a closed-form model of when a mid-bit sampler looks:
sample clock of data bit k = DIV_RX/2 + (k+1) * DIV_RX
transmitted symbol at t = floor(t / DIV_TX)The predicted bit is simply whichever symbol is on the wire at that instant. Because the second line maps an arbitrary time to a transmitted symbol, the model naturally predicts reading a neighbouring bit — which is exactly the failure under study — without containing any notion of "drift" or "error" at all. It shares no logic with the design.
Verilog
`timescale 1ns/1ps
// ---------------------------------------------------------------------------
// Testbench for uart_skew_rx -- the baud-mismatch signature, measured.
//
// Five receivers share one wire. The wire is driven at DIV_TX = 32 clocks per
// bit; the receivers run at 32, 31, 33, 30 and 34 -- that is 0%, -3.1%, +3.1%,
// -6.25% and +6.25% divider error.
//
// The oracle is NOT a second receiver. It is a closed-form model of WHEN a
// mid-bit sampler looks:
//
// sample clock of data bit k = DIV_RX/2 + (k+1)*DIV_RX (after the edge)
// transmitted symbol at t = floor(t / DIV_TX)
//
// so the predicted bit is simply whichever symbol happens to be on the wire at
// that instant. This predicts reading a NEIGHBOURING bit, which is exactly what
// a baud mismatch does, and it does so without sharing a line of logic with
// the DUT.
// ---------------------------------------------------------------------------
module tb_uart_skew_rx;
localparam DIV_TX = 32;
reg clk = 1'b0;
reg rst_n = 1'b0;
reg line = 1'b1;
integer checks = 0;
integer fails = 0;
always #5 clk = ~clk;
// ---- five receivers, one wire ----
wire [7:0] d_32, d_31, d_33, d_30, d_34;
wire v_32, v_31, v_33, v_30, v_34;
wire e_32, e_31, e_33, e_30, e_34;
wire f_32;
uart_skew_rx #(.DIV(32)) rx32 (.clk(clk), .rst_n(rst_n), .line_i(line),
.data_o(d_32), .valid_o(v_32), .frame_err_o(e_32), .false_start_o(f_32));
uart_skew_rx #(.DIV(31)) rx31 (.clk(clk), .rst_n(rst_n), .line_i(line),
.data_o(d_31), .valid_o(v_31), .frame_err_o(e_31), .false_start_o());
uart_skew_rx #(.DIV(33)) rx33 (.clk(clk), .rst_n(rst_n), .line_i(line),
.data_o(d_33), .valid_o(v_33), .frame_err_o(e_33), .false_start_o());
uart_skew_rx #(.DIV(30)) rx30 (.clk(clk), .rst_n(rst_n), .line_i(line),
.data_o(d_30), .valid_o(v_30), .frame_err_o(e_30), .false_start_o());
uart_skew_rx #(.DIV(34)) rx34 (.clk(clk), .rst_n(rst_n), .line_i(line),
.data_o(d_34), .valid_o(v_34), .frame_err_o(e_34), .false_start_o());
// ---- the transmitted symbol sequence: 0=start, 1..8=data, 9=stop ------
reg [7:0] tx_byte;
function sym_level;
input integer j;
begin
if (j == 0) sym_level = 1'b0; // start
else if (j >= 1 && j <= 8) sym_level = tx_byte[j-1]; // LSB first
else sym_level = 1'b1; // stop / idle
end
endfunction
// ---- closed-form oracle: what WILL this divider read? ----------------
function [7:0] oracle_data;
input integer div_rx;
integer k, t;
begin
oracle_data = 8'd0;
for (k = 0; k < 8; k = k + 1) begin
t = (div_rx/2) + (k+1)*div_rx;
oracle_data[k] = sym_level(t / DIV_TX);
end
end
endfunction
function oracle_stop;
input integer div_rx;
integer t;
begin
t = (div_rx/2) + 9*div_rx;
oracle_stop = sym_level(t / DIV_TX);
end
endfunction
// drift of bit k's sample point away from the true bit centre, in clocks
function integer drift_at;
input integer div_rx;
input integer k;
begin
drift_at = ((div_rx/2) + (k+1)*div_rx) - ((DIV_TX/2) + (k+1)*DIV_TX);
end
endfunction
function integer first_diff;
input [7:0] a;
input [7:0] b;
integer k, r;
begin
r = -1;
for (k = 7; k >= 0; k = k - 1) if (a[k] !== b[k]) r = k;
first_diff = r;
end
endfunction
task chk;
input [255:0] name;
input integer got;
input integer exp;
begin
checks = checks + 1;
if (got !== exp) begin
fails = fails + 1;
$display(" FAIL %0s: got %0d expected %0d", name, got, exp);
end
end
endtask
// Drive one 8N1 frame at the TRANSMITTER's bit period.
task send_byte;
input [7:0] b;
integer j, i;
begin
tx_byte = b;
for (j = 0; j < 10; j = j + 1) begin
@(negedge clk);
line = sym_level(j);
for (i = 0; i < DIV_TX; i = i + 1) @(posedge clk);
end
@(negedge clk); line = 1'b1;
repeat (DIV_TX) @(posedge clk); // one idle bit-time
end
endtask
task do_reset;
begin
line = 1'b1; rst_n = 1'b0;
repeat (4) @(posedge clk);
@(negedge clk); rst_n = 1'b1;
repeat (DIV_TX) @(posedge clk);
end
endtask
integer k;
initial begin
do_reset;
// ---------------- the drift table ---------------------------------
$display("Sample-point drift from the true bit centre, in clocks");
$display(" (DIV_TX = %0d; a bit is mis-sampled once |drift| reaches %0d)", DIV_TX, DIV_TX/2);
$display(" bit : d0 d1 d2 d3 d4 d5 d6 d7 stop");
$write(" -3.1%% (DIV=31):");
for (k = 0; k < 9; k = k + 1) $write("%5d", drift_at(31, k));
$display("");
$write(" +3.1%% (DIV=33):");
for (k = 0; k < 9; k = k + 1) $write("%5d", drift_at(33, k));
$display("");
$write(" -6.2%% (DIV=30):");
for (k = 0; k < 9; k = k + 1) $write("%5d", drift_at(30, k));
$display("");
$write(" +6.2%% (DIV=34):");
for (k = 0; k < 9; k = k + 1) $write("%5d", drift_at(34, k));
$display("");
$display("");
// ---------------- T1: matched divider is exact --------------------
send_byte(8'h55);
chk("T1 0x55 matched data", d_32, 8'h55);
chk("T1 0x55 matched no framing error", e_32, 0);
send_byte(8'hAA);
chk("T1 0xAA matched data", d_32, 8'hAA);
send_byte(8'h3C);
chk("T1 0x3C matched data", d_32, 8'h3C);
chk("T1 0x3C matched no framing error", e_32, 0);
// ---------------- T2/T3/T4: the sweep, three payloads -------------
send_byte(8'h55);
$display("0x55 sent: DIV=32 -> %02h 31 -> %02h 33 -> %02h 30 -> %02h 34 -> %02h",
d_32, d_31, d_33, d_30, d_34);
chk("T2 DIV=32 matches oracle", d_32, oracle_data(32));
chk("T2 DIV=31 matches oracle", d_31, oracle_data(31));
chk("T2 DIV=33 matches oracle", d_33, oracle_data(33));
chk("T2 DIV=30 matches oracle", d_30, oracle_data(30));
chk("T2 DIV=34 matches oracle", d_34, oracle_data(34));
chk("T2 DIV=30 stop matches oracle", e_30, oracle_stop(30) ? 0 : 1);
chk("T2 DIV=34 stop matches oracle", e_34, oracle_stop(34) ? 0 : 1);
send_byte(8'hAA);
$display("0xAA sent: DIV=32 -> %02h 31 -> %02h 33 -> %02h 30 -> %02h 34 -> %02h",
d_32, d_31, d_33, d_30, d_34);
chk("T3 DIV=32 matches oracle", d_32, oracle_data(32));
chk("T3 DIV=31 matches oracle", d_31, oracle_data(31));
chk("T3 DIV=33 matches oracle", d_33, oracle_data(33));
chk("T3 DIV=30 matches oracle", d_30, oracle_data(30));
chk("T3 DIV=34 matches oracle", d_34, oracle_data(34));
send_byte(8'h3C);
$display("0x3C sent: DIV=32 -> %02h 31 -> %02h 33 -> %02h 30 -> %02h 34 -> %02h",
d_32, d_31, d_33, d_30, d_34);
chk("T4 DIV=32 matches oracle", d_32, oracle_data(32));
chk("T4 DIV=31 matches oracle", d_31, oracle_data(31));
chk("T4 DIV=33 matches oracle", d_33, oracle_data(33));
chk("T4 DIV=30 matches oracle", d_30, oracle_data(30));
chk("T4 DIV=34 matches oracle", d_34, oracle_data(34));
// ---------------- T5: the tolerance boundary ----------------------
// Theory: the stop bit is sampled 9.5 receiver bit periods after the
// edge, so the error budget is 0.5/9.5 = 5.26%. 3.1% must survive;
// 6.25% must not.
send_byte(8'h55);
chk("T5 -3.1% survives 0x55", (d_31 == 8'h55) ? 1 : 0, 1);
chk("T5 +3.1% survives 0x55", (d_33 == 8'h55) ? 1 : 0, 1);
chk("T5 -6.2% corrupts 0x55", (d_30 != 8'h55) ? 1 : 0, 1);
chk("T5 +6.2% corrupts 0x55", (d_34 != 8'h55) ? 1 : 0, 1);
// ---------------- T6: it is the HIGH bits that go first ------------
$display("first corrupted bit index: DIV=30 -> %0d DIV=34 -> %0d",
first_diff(d_30, 8'h55), first_diff(d_34, 8'h55));
chk("T6 DIV=30 first failure is a high bit", (first_diff(d_30, 8'h55) >= 5) ? 1 : 0, 1);
chk("T6 DIV=34 first failure is a high bit", (first_diff(d_34, 8'h55) >= 5) ? 1 : 0, 1);
// ---------------- T7: a glitch is not a start bit ------------------
do_reset;
@(negedge clk); line = 1'b0;
repeat (DIV_TX/4) @(posedge clk); // a quarter-bit spike
@(negedge clk); line = 1'b1;
repeat (DIV_TX) @(posedge clk);
chk("T7 spike raised false_start", f_32, 0); // strobe already passed
chk("T7 spike produced no frame", v_32, 0);
$display("");
$display("== %0d checks, %0d failures ==", checks, fails);
if (fails == 0) $display(" RESULT: ALL VERILOG SKEW-RX TESTS PASSED");
else $display(" RESULT: %0d FAILURE(S)", fails);
$finish;
end
endmoduleSystemVerilog
`timescale 1ns/1ps
// ---------------------------------------------------------------------------
// Testbench for uart_skew_rx -- the baud-mismatch signature, measured.
//
// Five receivers share one wire. The logic is driven at DIV_TX = 32 clocks per
// bit; the receivers run at 32, 31, 33, 30 and 34 -- that is 0%, -3.1%, +3.1%,
// -6.25% and +6.25% divider error.
//
// The oracle is NOT a second receiver. It is a closed-form model of WHEN a
// mid-bit sampler looks:
//
// sample clock of data bit k = DIV_RX/2 + (k+1)*DIV_RX (after the edge)
// transmitted symbol at t = floor(t / DIV_TX)
//
// so the predicted bit is simply whichever symbol happens to be on the logic at
// that instant. This predicts reading a NEIGHBOURING bit, which is exactly what
// a baud mismatch does, and it does so without sharing a line of logic with
// the DUT.
// ---------------------------------------------------------------------------
module tb_uart_skew_rx;
localparam DIV_TX = 32;
logic clk = 1'b0;
logic rst_n = 1'b0;
logic line = 1'b1;
integer checks = 0;
integer fails = 0;
always #5 clk = ~clk;
// ---- five receivers, one logic ----
logic [7:0] d_32, d_31, d_33, d_30, d_34;
logic v_32, v_31, v_33, v_30, v_34;
logic e_32, e_31, e_33, e_30, e_34;
logic f_32;
uart_skew_rx #(.DIV(32)) rx32 (.clk(clk), .rst_n(rst_n), .line_i(line),
.data_o(d_32), .valid_o(v_32), .frame_err_o(e_32), .false_start_o(f_32));
uart_skew_rx #(.DIV(31)) rx31 (.clk(clk), .rst_n(rst_n), .line_i(line),
.data_o(d_31), .valid_o(v_31), .frame_err_o(e_31), .false_start_o());
uart_skew_rx #(.DIV(33)) rx33 (.clk(clk), .rst_n(rst_n), .line_i(line),
.data_o(d_33), .valid_o(v_33), .frame_err_o(e_33), .false_start_o());
uart_skew_rx #(.DIV(30)) rx30 (.clk(clk), .rst_n(rst_n), .line_i(line),
.data_o(d_30), .valid_o(v_30), .frame_err_o(e_30), .false_start_o());
uart_skew_rx #(.DIV(34)) rx34 (.clk(clk), .rst_n(rst_n), .line_i(line),
.data_o(d_34), .valid_o(v_34), .frame_err_o(e_34), .false_start_o());
// ---- the transmitted symbol sequence: 0=start, 1..8=data, 9=stop ------
logic [7:0] tx_byte;
function automatic logic sym_level(input int j);
begin
if (j == 0) sym_level = 1'b0; // start
else if (j >= 1 && j <= 8) sym_level = tx_byte[j-1]; // LSB first
else sym_level = 1'b1; // stop / idle
end
endfunction
// ---- closed-form oracle: what WILL this divider read? ----------------
function automatic logic [7:0] oracle_data(input int div_rx);
int k, t;
begin
oracle_data = 8'd0;
for (k = 0; k < 8; k = k + 1) begin
t = (div_rx/2) + (k+1)*div_rx;
oracle_data[k] = sym_level(t / DIV_TX);
end
end
endfunction
function automatic logic oracle_stop(input int div_rx);
int t;
begin
t = (div_rx/2) + 9*div_rx;
oracle_stop = sym_level(t / DIV_TX);
end
endfunction
// drift of bit k's sample point away from the true bit centre, in clocks
function automatic int drift_at(input int div_rx, input int k);
begin
drift_at = ((div_rx/2) + (k+1)*div_rx) - ((DIV_TX/2) + (k+1)*DIV_TX);
end
endfunction
function automatic int first_diff(input logic [7:0] a, input logic [7:0] b);
int k, r;
begin
r = -1;
for (k = 7; k >= 0; k = k - 1) if (a[k] !== b[k]) r = k;
first_diff = r;
end
endfunction
task automatic chk(input string name, input int got, input int exp);
begin
checks = checks + 1;
if (got !== exp) begin
fails = fails + 1;
$display(" FAIL %0s: got %0d expected %0d", name, got, exp);
end
end
endtask
// Drive one 8N1 frame at the TRANSMITTER's bit period.
task automatic send_byte(input logic [7:0] b);
int j, i;
begin
tx_byte = b;
for (j = 0; j < 10; j = j + 1) begin
@(negedge clk);
line = sym_level(j);
for (i = 0; i < DIV_TX; i = i + 1) @(posedge clk);
end
@(negedge clk); line = 1'b1;
repeat (DIV_TX) @(posedge clk); // one idle bit-time
end
endtask
task automatic do_reset();
begin
line = 1'b1; rst_n = 1'b0;
repeat (4) @(posedge clk);
@(negedge clk); rst_n = 1'b1;
repeat (DIV_TX) @(posedge clk);
end
endtask
integer k;
initial begin
do_reset;
// ---------------- the drift table ---------------------------------
$display("Sample-point drift from the true bit centre, in clocks");
$display(" (DIV_TX = %0d; a bit is mis-sampled once |drift| reaches %0d)", DIV_TX, DIV_TX/2);
$display(" bit : d0 d1 d2 d3 d4 d5 d6 d7 stop");
$write(" -3.1%% (DIV=31):");
for (k = 0; k < 9; k = k + 1) $write("%5d", drift_at(31, k));
$display("");
$write(" +3.1%% (DIV=33):");
for (k = 0; k < 9; k = k + 1) $write("%5d", drift_at(33, k));
$display("");
$write(" -6.2%% (DIV=30):");
for (k = 0; k < 9; k = k + 1) $write("%5d", drift_at(30, k));
$display("");
$write(" +6.2%% (DIV=34):");
for (k = 0; k < 9; k = k + 1) $write("%5d", drift_at(34, k));
$display("");
$display("");
// ---------------- T1: matched divider is exact --------------------
send_byte(8'h55);
chk("T1 0x55 matched data", d_32, 8'h55);
chk("T1 0x55 matched no framing error", e_32, 0);
send_byte(8'hAA);
chk("T1 0xAA matched data", d_32, 8'hAA);
send_byte(8'h3C);
chk("T1 0x3C matched data", d_32, 8'h3C);
chk("T1 0x3C matched no framing error", e_32, 0);
// ---------------- T2/T3/T4: the sweep, three payloads -------------
send_byte(8'h55);
$display("0x55 sent: DIV=32 -> %02h 31 -> %02h 33 -> %02h 30 -> %02h 34 -> %02h",
d_32, d_31, d_33, d_30, d_34);
chk("T2 DIV=32 matches oracle", d_32, oracle_data(32));
chk("T2 DIV=31 matches oracle", d_31, oracle_data(31));
chk("T2 DIV=33 matches oracle", d_33, oracle_data(33));
chk("T2 DIV=30 matches oracle", d_30, oracle_data(30));
chk("T2 DIV=34 matches oracle", d_34, oracle_data(34));
chk("T2 DIV=30 stop matches oracle", e_30, oracle_stop(30) ? 0 : 1);
chk("T2 DIV=34 stop matches oracle", e_34, oracle_stop(34) ? 0 : 1);
send_byte(8'hAA);
$display("0xAA sent: DIV=32 -> %02h 31 -> %02h 33 -> %02h 30 -> %02h 34 -> %02h",
d_32, d_31, d_33, d_30, d_34);
chk("T3 DIV=32 matches oracle", d_32, oracle_data(32));
chk("T3 DIV=31 matches oracle", d_31, oracle_data(31));
chk("T3 DIV=33 matches oracle", d_33, oracle_data(33));
chk("T3 DIV=30 matches oracle", d_30, oracle_data(30));
chk("T3 DIV=34 matches oracle", d_34, oracle_data(34));
send_byte(8'h3C);
$display("0x3C sent: DIV=32 -> %02h 31 -> %02h 33 -> %02h 30 -> %02h 34 -> %02h",
d_32, d_31, d_33, d_30, d_34);
chk("T4 DIV=32 matches oracle", d_32, oracle_data(32));
chk("T4 DIV=31 matches oracle", d_31, oracle_data(31));
chk("T4 DIV=33 matches oracle", d_33, oracle_data(33));
chk("T4 DIV=30 matches oracle", d_30, oracle_data(30));
chk("T4 DIV=34 matches oracle", d_34, oracle_data(34));
// ---------------- T5: the tolerance boundary ----------------------
// Theory: the stop bit is sampled 9.5 receiver bit periods after the
// edge, so the error budget is 0.5/9.5 = 5.26%. 3.1% must survive;
// 6.25% must not.
send_byte(8'h55);
chk("T5 -3.1% survives 0x55", (d_31 == 8'h55) ? 1 : 0, 1);
chk("T5 +3.1% survives 0x55", (d_33 == 8'h55) ? 1 : 0, 1);
chk("T5 -6.2% corrupts 0x55", (d_30 != 8'h55) ? 1 : 0, 1);
chk("T5 +6.2% corrupts 0x55", (d_34 != 8'h55) ? 1 : 0, 1);
// ---------------- T6: it is the HIGH bits that go first ------------
$display("first corrupted bit index: DIV=30 -> %0d DIV=34 -> %0d",
first_diff(d_30, 8'h55), first_diff(d_34, 8'h55));
chk("T6 DIV=30 first failure is a high bit", (first_diff(d_30, 8'h55) >= 5) ? 1 : 0, 1);
chk("T6 DIV=34 first failure is a high bit", (first_diff(d_34, 8'h55) >= 5) ? 1 : 0, 1);
// ---------------- T7: a glitch is not a start bit ------------------
do_reset;
@(negedge clk); line = 1'b0;
repeat (DIV_TX/4) @(posedge clk); // a quarter-bit spike
@(negedge clk); line = 1'b1;
repeat (DIV_TX) @(posedge clk);
chk("T7 spike raised false_start", f_32, 0); // strobe already passed
chk("T7 spike produced no frame", v_32, 0);
$display("");
$display("== %0d checks, %0d failures ==", checks, fails);
if (fails == 0) $display(" RESULT: ALL SYSTEMVERILOG SKEW-RX TESTS PASSED");
else $display(" RESULT: %0d FAILURE(S)", fails);
$finish;
end
endmoduleVHDL
-- ---------------------------------------------------------------------------
-- Testbench for uart_skew_rx -- the baud-mismatch signature, measured.
--
-- Five receivers share one wire. The wire is driven at DIV_TX = 32 clocks per
-- bit; the receivers run at 32, 31, 33, 30 and 34 -- that is 0%, -3.1%, +3.1%,
-- -6.25% and +6.25% divider error.
--
-- The oracle is NOT a second receiver. It is a closed-form model of WHEN a
-- mid-bit sampler looks:
--
-- sample clock of data bit k = DIV_RX/2 + (k+1)*DIV_RX (after the edge)
-- transmitted symbol at t = t / DIV_TX (integer divide)
--
-- so the predicted bit is simply whichever symbol happens to be on the wire at
-- that instant. This predicts reading a NEIGHBOURING bit, which is exactly what
-- a baud mismatch does, and it does so without sharing a line of logic with
-- the DUT.
-- ---------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity tb_uart_skew_rx is
end entity tb_uart_skew_rx;
architecture sim of tb_uart_skew_rx is
constant DIV_TX : natural := 32;
constant TCLK : time := 10 ns;
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal line : std_logic := '1';
signal done : boolean := false;
signal d_32, d_31, d_33, d_30, d_34 : std_logic_vector(7 downto 0);
signal v_32, v_31, v_33, v_30, v_34 : std_logic;
signal e_32, e_31, e_33, e_30, e_34 : std_logic;
signal f_32 : std_logic;
signal open_fs : std_logic;
-- ---- the transmitted symbol sequence: 0=start, 1..8=data, 9=stop ------
function sym_level (j : integer; data : std_logic_vector(7 downto 0))
return std_logic is
begin
if j = 0 then
return '0'; -- start
elsif j >= 1 and j <= 8 then
return data(j-1); -- LSB first
else
return '1'; -- stop / idle
end if;
end function;
-- ---- closed-form oracle: what WILL this divider read? ----------------
function oracle_data (div_rx : integer; data : std_logic_vector(7 downto 0))
return std_logic_vector is
variable r : std_logic_vector(7 downto 0) := (others => '0');
variable t : integer;
begin
for k in 0 to 7 loop
t := (div_rx/2) + (k+1)*div_rx;
r(k) := sym_level(t / DIV_TX, data);
end loop;
return r;
end function;
function oracle_stop (div_rx : integer; data : std_logic_vector(7 downto 0))
return std_logic is
variable t : integer;
begin
t := (div_rx/2) + 9*div_rx;
return sym_level(t / DIV_TX, data);
end function;
-- drift of bit k's sample point away from the true bit centre, in clocks
function drift_at (div_rx : integer; k : integer) return integer is
begin
return ((div_rx/2) + (k+1)*div_rx) - ((DIV_TX/2) + (k+1)*DIV_TX);
end function;
function first_diff (a : std_logic_vector(7 downto 0);
b : std_logic_vector(7 downto 0)) return integer is
variable r : integer := -1;
begin
for k in 7 downto 0 loop
if a(k) /= b(k) then r := k; end if;
end loop;
return r;
end function;
function hex2 (v : std_logic_vector(7 downto 0)) return string is
constant D : string(1 to 16) := "0123456789abcdef";
variable u : integer := to_integer(unsigned(v));
begin
return D(u/16 + 1) & D(u mod 16 + 1);
end function;
function pad5 (n : integer) return string is
variable s : string(1 to 5) := (others => ' ');
variable t : string(1 to 20);
variable l : integer;
begin
t := (others => ' ');
l := integer'image(n)'length;
s(6-l to 5) := integer'image(n);
return s;
end function;
begin
clk <= '0' when done else not clk after TCLK/2;
rx32 : entity work.uart_skew_rx generic map (DIV => 32)
port map (clk => clk, rst_n => rst_n, line_i => line, data_o => d_32,
valid_o => v_32, frame_err_o => e_32, false_start_o => f_32);
rx31 : entity work.uart_skew_rx generic map (DIV => 31)
port map (clk => clk, rst_n => rst_n, line_i => line, data_o => d_31,
valid_o => v_31, frame_err_o => e_31, false_start_o => open);
rx33 : entity work.uart_skew_rx generic map (DIV => 33)
port map (clk => clk, rst_n => rst_n, line_i => line, data_o => d_33,
valid_o => v_33, frame_err_o => e_33, false_start_o => open);
rx30 : entity work.uart_skew_rx generic map (DIV => 30)
port map (clk => clk, rst_n => rst_n, line_i => line, data_o => d_30,
valid_o => v_30, frame_err_o => e_30, false_start_o => open);
rx34 : entity work.uart_skew_rx generic map (DIV => 34)
port map (clk => clk, rst_n => rst_n, line_i => line, data_o => d_34,
valid_o => v_34, frame_err_o => e_34, false_start_o => open);
stim : process
variable checks, fails : integer := 0;
procedure chk (name : string; got : integer; exp : integer) is
begin
checks := checks + 1;
if got /= exp then
fails := fails + 1;
report " FAIL " & name & ": got " & integer'image(got) &
" expected " & integer'image(exp) severity error;
end if;
end procedure;
procedure chkv (name : string; got : std_logic_vector(7 downto 0);
exp : std_logic_vector(7 downto 0)) is
begin
checks := checks + 1;
if got /= exp then
fails := fails + 1;
report " FAIL " & name & ": got " & hex2(got) &
" expected " & hex2(exp) severity error;
end if;
end procedure;
-- Drive one 8N1 frame at the TRANSMITTER's bit period.
procedure send_byte (b : std_logic_vector(7 downto 0)) is
begin
for j in 0 to 9 loop
wait until falling_edge(clk);
line <= sym_level(j, b);
for i in 0 to DIV_TX-1 loop
wait until rising_edge(clk);
end loop;
end loop;
wait until falling_edge(clk);
line <= '1';
for i in 0 to DIV_TX-1 loop
wait until rising_edge(clk);
end loop;
end procedure;
procedure do_reset is
begin
line <= '1'; rst_n <= '0';
for i in 0 to 3 loop wait until rising_edge(clk); end loop;
wait until falling_edge(clk);
rst_n <= '1';
for i in 0 to DIV_TX-1 loop wait until rising_edge(clk); end loop;
end procedure;
variable row : string(1 to 45);
begin
do_reset;
-- ---------------- the drift table ---------------------------------
report "Sample-point drift from the true bit centre, in clocks";
report " (DIV_TX = " & integer'image(DIV_TX) &
"; a bit is mis-sampled once |drift| reaches " &
integer'image(DIV_TX/2) & ")";
report " bit : d0 d1 d2 d3 d4 d5 d6 d7 stop";
for sel in 0 to 3 loop
row := (others => ' ');
for k in 0 to 8 loop
case sel is
when 0 => row(k*5+1 to k*5+5) := pad5(drift_at(31, k));
when 1 => row(k*5+1 to k*5+5) := pad5(drift_at(33, k));
when 2 => row(k*5+1 to k*5+5) := pad5(drift_at(30, k));
when others => row(k*5+1 to k*5+5) := pad5(drift_at(34, k));
end case;
end loop;
case sel is
when 0 => report " -3.1% (DIV=31):" & row;
when 1 => report " +3.1% (DIV=33):" & row;
when 2 => report " -6.2% (DIV=30):" & row;
when others => report " +6.2% (DIV=34):" & row;
end case;
end loop;
-- ---------------- T1: matched divider is exact --------------------
send_byte(x"55");
chkv("T1 0x55 matched data", d_32, x"55");
chk ("T1 0x55 matched no framing error", to_integer(unsigned'("" & e_32)), 0);
send_byte(x"AA");
chkv("T1 0xAA matched data", d_32, x"AA");
send_byte(x"3C");
chkv("T1 0x3C matched data", d_32, x"3C");
chk ("T1 0x3C matched no framing error", to_integer(unsigned'("" & e_32)), 0);
-- ---------------- T2/T3/T4: the sweep, three payloads -------------
send_byte(x"55");
report "0x55 sent: DIV=32 -> " & hex2(d_32) & " 31 -> " & hex2(d_31) &
" 33 -> " & hex2(d_33) & " 30 -> " & hex2(d_30) &
" 34 -> " & hex2(d_34);
chkv("T2 DIV=32 matches oracle", d_32, oracle_data(32, x"55"));
chkv("T2 DIV=31 matches oracle", d_31, oracle_data(31, x"55"));
chkv("T2 DIV=33 matches oracle", d_33, oracle_data(33, x"55"));
chkv("T2 DIV=30 matches oracle", d_30, oracle_data(30, x"55"));
chkv("T2 DIV=34 matches oracle", d_34, oracle_data(34, x"55"));
if oracle_stop(30, x"55") = '1' then
chk("T2 DIV=30 stop matches oracle", to_integer(unsigned'("" & e_30)), 0);
else
chk("T2 DIV=30 stop matches oracle", to_integer(unsigned'("" & e_30)), 1);
end if;
if oracle_stop(34, x"55") = '1' then
chk("T2 DIV=34 stop matches oracle", to_integer(unsigned'("" & e_34)), 0);
else
chk("T2 DIV=34 stop matches oracle", to_integer(unsigned'("" & e_34)), 1);
end if;
send_byte(x"AA");
report "0xAA sent: DIV=32 -> " & hex2(d_32) & " 31 -> " & hex2(d_31) &
" 33 -> " & hex2(d_33) & " 30 -> " & hex2(d_30) &
" 34 -> " & hex2(d_34);
chkv("T3 DIV=32 matches oracle", d_32, oracle_data(32, x"AA"));
chkv("T3 DIV=31 matches oracle", d_31, oracle_data(31, x"AA"));
chkv("T3 DIV=33 matches oracle", d_33, oracle_data(33, x"AA"));
chkv("T3 DIV=30 matches oracle", d_30, oracle_data(30, x"AA"));
chkv("T3 DIV=34 matches oracle", d_34, oracle_data(34, x"AA"));
send_byte(x"3C");
report "0x3C sent: DIV=32 -> " & hex2(d_32) & " 31 -> " & hex2(d_31) &
" 33 -> " & hex2(d_33) & " 30 -> " & hex2(d_30) &
" 34 -> " & hex2(d_34);
chkv("T4 DIV=32 matches oracle", d_32, oracle_data(32, x"3C"));
chkv("T4 DIV=31 matches oracle", d_31, oracle_data(31, x"3C"));
chkv("T4 DIV=33 matches oracle", d_33, oracle_data(33, x"3C"));
chkv("T4 DIV=30 matches oracle", d_30, oracle_data(30, x"3C"));
chkv("T4 DIV=34 matches oracle", d_34, oracle_data(34, x"3C"));
-- ---------------- T5: the tolerance boundary ----------------------
-- Theory: the stop bit is sampled 9.5 receiver bit periods after the
-- edge, so the error budget is 0.5/9.5 = 5.26%. 3.1% must survive;
-- 6.25% must not.
send_byte(x"55");
if d_31 = x"55" then chk("T5 -3.1% survives 0x55", 1, 1);
else chk("T5 -3.1% survives 0x55", 0, 1); end if;
if d_33 = x"55" then chk("T5 +3.1% survives 0x55", 1, 1);
else chk("T5 +3.1% survives 0x55", 0, 1); end if;
if d_30 /= x"55" then chk("T5 -6.2% corrupts 0x55", 1, 1);
else chk("T5 -6.2% corrupts 0x55", 0, 1); end if;
if d_34 /= x"55" then chk("T5 +6.2% corrupts 0x55", 1, 1);
else chk("T5 +6.2% corrupts 0x55", 0, 1); end if;
-- ---------------- T6: it is the HIGH bits that go first ------------
report "first corrupted bit index: DIV=30 -> " &
integer'image(first_diff(d_30, x"55")) & " DIV=34 -> " &
integer'image(first_diff(d_34, x"55"));
if first_diff(d_30, x"55") >= 5 then
chk("T6 DIV=30 first failure is a high bit", 1, 1);
else
chk("T6 DIV=30 first failure is a high bit", 0, 1);
end if;
if first_diff(d_34, x"55") >= 5 then
chk("T6 DIV=34 first failure is a high bit", 1, 1);
else
chk("T6 DIV=34 first failure is a high bit", 0, 1);
end if;
-- ---------------- T7: a glitch is not a start bit ------------------
do_reset;
wait until falling_edge(clk); line <= '0';
for i in 0 to DIV_TX/4 - 1 loop wait until rising_edge(clk); end loop;
wait until falling_edge(clk); line <= '1';
for i in 0 to DIV_TX-1 loop wait until rising_edge(clk); end loop;
chk("T7 spike raised false_start", to_integer(unsigned'("" & f_32)), 0);
chk("T7 spike produced no frame", to_integer(unsigned'("" & v_32)), 0);
report "";
report "== " & integer'image(checks) & " checks, " &
integer'image(fails) & " failures ==";
if fails = 0 then
report " RESULT: ALL VHDL SKEW-RX TESTS PASSED";
else
report " RESULT: " & integer'image(fails) & " FAILURE(S)" severity error;
end if;
done <= true;
wait;
end process;
end architecture sim;Thirty checks per language, and the three implementations agree on every received byte, the whole drift table, and the simulation end time of 25,755 ns:
test what it establishes result
---- ----------------------------------------------------------- --------------
T1 a matched divider is exact on three payloads 0x55/0xAA/0x3C
T2 all five receivers match the closed-form oracle on 0x55 5/5 agree
T3 the same, on 0xAA 5/5 agree
T4 the same, on 0x3C 5/5 agree
T5 +/-3.1% survives and +/-6.2% corrupts, as 5.26% predicts 4 checks
T6 the first corrupted bit index is high, not low d7 in both
T7 a quarter-bit spike starts no frame no frameT2 also checks the framing outcome against the oracle's prediction for the stop bit, which is the constraint §1 identified as tightest.
7. Proving the Tests Can Fail
mutation checks failed verdict
----------------------------------------------- ------------- -------
M4 wait DIV/4 instead of DIV/2 after the edge 11 killed
M5 assemble the byte MSB-first instead of LSB 17 killedM4 moves every sample point a quarter of a bit off centre, which halves the timing margin; eleven checks notice. M5 is the bit-order error that Chapter 17.3 is about, and it fails seventeen checks here — which is worth noticing, because it means this chapter's suite would catch a reversal even though reversal is not what it was written to test.
8. Recognising a Divider Error from a Capture
Putting the chapter together, here is what separates a baud problem from the other things that corrupt bytes:
| Observation | Points toward | Why |
|---|---|---|
| Corruption concentrated in the high bits | baud error | the lever arm of §1 |
| Framing errors on bytes that still decode | baud error | the stop bit is sampled furthest out |
| Corruption that depends on the payload | baud error | §5 — a drifted sample may land on an identical neighbour |
| Every byte wrong in the same way regardless of value | bit order or parity config — 17.3 | a systematic remap, not a timing walk |
| Corruption spread evenly across bit positions | noise — 17.4 | noise has no lever arm |
| Bytes missing entirely rather than wrong | overrun or flow control — 17.5 | nothing was mis-sampled; it was never stored |
And the direct measurement, if you can capture the line: recover the bit period with the instrument from Chapter 17.1, then compare it against the period the receiver believes in. A mismatch there is the whole diagnosis, and it does not require decoding a single byte.
Continue learning
Related tutorials
- Related topic
Reading a UART Waveform as Evidence
Separating what a capture observes from what it implies, the run-length structure that makes a bit period measurable, a measuring block in three HDLs, and the ordinary payload that makes it report nine times the right answer.
- Related topic
Bit Order, Parity and Framing Failure Signatures
Why 0xA5 cannot detect a reversed byte, why parity is structurally blind to bit order, why one frame can never separate a parity misconfiguration from noise, and a classifier in three HDLs that resolves all four faults.
- Related topic
Missing Start Bits, False Starts and Noise
A naive start detector and a majority-vote qualifier racing on one wire, a rejection boundary measured rather than assumed, and the counter that separates a transmitter that never sent from a receiver that never listened.
- Related topic
FIFO Overrun and Flow-Control Failures
Three scenarios with the identical symptom and three different fixes, an instrumented FIFO in three HDLs that classifies each lost byte, and the headroom arithmetic that explains why RTS often fails on a USB-serial bridge.
Where this fits
Part of the UART curriculum.
