UART · Module 17
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.
A receiver that produces no data and a receiver that produces nonsense are usually treated as different bugs. They are frequently the same bug seen from two sides of a threshold, because both come down to one decision the receiver makes over and over: is this falling edge a start bit?
Get that decision wrong in one direction and noise becomes frames. Get it wrong in the other and frames become silence. This chapter puts a naive detector and a qualified one on the same wire, counts what each one commits to, and then measures — rather than asserts — exactly how narrow a pulse the qualified one will still accept.
1. The Decision at the Edge
An idle UART line sits at MARK. A frame begins when it falls to SPACE. The textbook block diagram draws that as an edge detector feeding a state machine, and for a clean simulation it is exactly right.
On a board it is not, because a falling edge is produced by many things that are not start bits: a neighbouring signal coupling into the trace, a connector being seated, a ground bounce, a transmitter powering up, a cable being hot-plugged. Each one is a falling edge, and a detector that treats every falling edge as a frame will start a frame for each.
The consequence is worse than a spurious byte. Having committed, the receiver is busy for ten bit periods, and any real frame that arrives during that window is missed entirely. One spike does not cost one bad byte; it costs one bad byte plus every good byte that overlapped the window it opened.
So a real receiver treats the edge as a candidate and goes looking for confirmation.
2. What a 16550 Actually Does
The classic design oversamples at 16 times the bit rate and, on a falling edge, waits roughly half a bit period and then takes three samples around the middle of the putative start bit — historically ticks 7, 8 and 9 of the 16. If at least two of the three read SPACE, the frame is committed. Otherwise the candidate is discarded and the detector returns to idle immediately.
That design does three things at once, and it is worth separating them:
-
It rejects narrow pulses. Anything that has recovered to MARK before the middle of the bit fails the vote.
-
It tolerates a single bad sample. Two of three is a majority, so one noisy sample inside an otherwise valid start bit does not lose the frame.
-
It costs nothing in latency. The receiver had to wait until mid-bit anyway to sample the start bit; the vote happens in the time it was already spending.
The three-sample vote on a real start bit and on a spike
12 cycles
3. Two Detectors, One Wire
The block below runs both designs against the same line simultaneously, which turns "you should qualify the start bit" from advice into a measurement: the two counters differ by exactly the number of events that were never frames.
Both detectors hold off for a full frame after committing. That matters for fairness — a detector that could re-trigger inside its own frame would inflate its count for reasons unrelated to qualification, and the comparison would be measuring the wrong thing.
Verilog
// ---------------------------------------------------------------------------
// uart_start_qualify -- two start-bit detectors racing on one wire.
//
// The naive detector does what the textbook diagram implies: a falling edge is
// a start bit. The qualified detector does what a 16550 actually does -- it
// treats the edge as a CANDIDATE, waits until the middle of the putative start
// bit, and takes a three-sample majority vote before committing. For DIV = 16
// those three samples land on oversample ticks 7, 8 and 9, which is the
// classic arrangement.
//
// Running both against the same wire turns "add a glitch filter" from advice
// into a measurement: the two counters disagree by exactly the number of
// events that were never frames.
//
// Both detectors hold off for a whole frame after committing. That matters for
// a fair comparison -- a detector that re-triggers inside its own frame would
// inflate its count for reasons that have nothing to do with qualification.
// ---------------------------------------------------------------------------
module uart_start_qualify #(
parameter DIV = 16 // clocks per bit
)(
input wire clk,
input wire rst_n,
input wire line_i,
output reg [15:0] naive_cnt_o, // frames the naive detector started
output reg [15:0] cand_cnt_o, // edges the qualifier examined
output reg [15:0] qual_cnt_o, // candidates it committed to
output reg [15:0] rej_cnt_o, // candidates it threw away
output wire qual_busy_o
);
localparam FRAME = 10 * DIV; // start + 8 data + stop, in clocks
reg line_q;
wire fall = line_q && !line_i;
// ---------------- the naive detector ----------------------------------
reg [15:0] naive_hold;
wire naive_free = (naive_hold == 16'd0);
// ---------------- the qualified detector ------------------------------
localparam Q_IDLE = 2'd0, Q_ARM = 2'd1, Q_BUSY = 2'd2;
reg [1:0] qstate;
reg [15:0] qcnt;
reg [1:0] votes; // how many of the three samples were SPACE
assign qual_busy_o = (qstate != Q_IDLE);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
line_q <= 1'b1;
naive_hold <= 16'd0;
naive_cnt_o <= 16'd0;
qstate <= Q_IDLE;
qcnt <= 16'd0;
votes <= 2'd0;
cand_cnt_o <= 16'd0;
qual_cnt_o <= 16'd0;
rej_cnt_o <= 16'd0;
end else begin
line_q <= line_i;
// ---- naive: an edge is a frame, no questions asked ----------
if (naive_free) begin
if (fall) begin
naive_cnt_o <= naive_cnt_o + 16'd1;
naive_hold <= FRAME[15:0];
end
end else begin
naive_hold <= naive_hold - 16'd1;
end
// ---- qualified: an edge is a question ------------------------
case (qstate)
Q_IDLE: begin
if (fall) begin
cand_cnt_o <= cand_cnt_o + 16'd1;
qstate <= Q_ARM;
qcnt <= 16'd0;
votes <= 2'd0;
end
end
Q_ARM: begin
// three samples straddling the middle of the start bit
if (qcnt == (DIV/2) - 2 || qcnt == (DIV/2) - 1 || qcnt == (DIV/2)) begin
if (!line_i) votes <= votes + 2'd1;
end
if (qcnt == (DIV/2)) begin
// majority of three: two SPACE samples commit the frame
if ((votes + (line_i ? 2'd0 : 2'd1)) >= 2'd2) begin
qual_cnt_o <= qual_cnt_o + 16'd1;
qstate <= Q_BUSY;
qcnt <= 16'd0;
end else begin
rej_cnt_o <= rej_cnt_o + 16'd1;
qstate <= Q_IDLE;
end
end else begin
qcnt <= qcnt + 16'd1;
end
end
Q_BUSY: begin
// hold off for the rest of the frame
if (qcnt == FRAME - (DIV/2) - 1) qstate <= Q_IDLE;
else qcnt <= qcnt + 16'd1;
end
default: qstate <= Q_IDLE;
endcase
end
end
endmoduleSystemVerilog
// ---------------------------------------------------------------------------
// uart_start_qualify -- two start-bit detectors racing on one wire.
//
// The naive detector does what the textbook diagram implies: a falling edge is
// a start bit. The qualified detector does what a 16550 actually does -- it
// treats the edge as a CANDIDATE, waits until the middle of the putative start
// bit, and takes a three-sample majority vote before committing. For DIV = 16
// those three samples land on oversample ticks 7, 8 and 9, which is the
// classic arrangement.
//
// Running both against the same wire turns "add a glitch filter" from advice
// into a measurement: the two counters disagree by exactly the number of
// events that were never frames.
//
// Both detectors hold off for a whole frame after committing. That matters for
// a fair comparison -- a detector that re-triggers inside its own frame would
// inflate its count for reasons that have nothing to do with qualification.
// ---------------------------------------------------------------------------
module uart_start_qualify #(
parameter int DIV = 16 // clocks per bit
)(
input logic clk,
input logic rst_n,
input logic line_i,
output logic [15:0] naive_cnt_o, // frames the naive detector started
output logic [15:0] cand_cnt_o, // edges the qualifier examined
output logic [15:0] qual_cnt_o, // candidates it committed to
output logic [15:0] rej_cnt_o, // candidates it threw away
output logic qual_busy_o
);
localparam FRAME = 10 * DIV; // start + 8 data + stop, in clocks
logic line_q;
wire fall = line_q && !line_i;
// ---------------- the naive detector ----------------------------------
logic [15:0] naive_hold;
wire naive_free = (naive_hold == 16'd0);
// ---------------- the qualified detector ------------------------------
localparam Q_IDLE = 2'd0, Q_ARM = 2'd1, Q_BUSY = 2'd2;
logic [1:0] qstate;
logic [15:0] qcnt;
logic [1:0] votes; // how many of the three samples were SPACE
assign qual_busy_o = (qstate != Q_IDLE);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
line_q <= 1'b1;
naive_hold <= 16'd0;
naive_cnt_o <= 16'd0;
qstate <= Q_IDLE;
qcnt <= 16'd0;
votes <= 2'd0;
cand_cnt_o <= 16'd0;
qual_cnt_o <= 16'd0;
rej_cnt_o <= 16'd0;
end else begin
line_q <= line_i;
// ---- naive: an edge is a frame, no questions asked ----------
if (naive_free) begin
if (fall) begin
naive_cnt_o <= naive_cnt_o + 16'd1;
naive_hold <= FRAME[15:0];
end
end else begin
naive_hold <= naive_hold - 16'd1;
end
// ---- qualified: an edge is a question ------------------------
case (qstate)
Q_IDLE: begin
if (fall) begin
cand_cnt_o <= cand_cnt_o + 16'd1;
qstate <= Q_ARM;
qcnt <= 16'd0;
votes <= 2'd0;
end
end
Q_ARM: begin
// three samples straddling the middle of the start bit
if (qcnt == (DIV/2) - 2 || qcnt == (DIV/2) - 1 || qcnt == (DIV/2)) begin
if (!line_i) votes <= votes + 2'd1;
end
if (qcnt == (DIV/2)) begin
// majority of three: two SPACE samples commit the frame
if ((votes + (line_i ? 2'd0 : 2'd1)) >= 2'd2) begin
qual_cnt_o <= qual_cnt_o + 16'd1;
qstate <= Q_BUSY;
qcnt <= 16'd0;
end else begin
rej_cnt_o <= rej_cnt_o + 16'd1;
qstate <= Q_IDLE;
end
end else begin
qcnt <= qcnt + 16'd1;
end
end
Q_BUSY: begin
// hold off for the rest of the frame
if (qcnt == FRAME - (DIV/2) - 1) qstate <= Q_IDLE;
else qcnt <= qcnt + 16'd1;
end
default: qstate <= Q_IDLE;
endcase
end
end
endmoduleVHDL
-- ---------------------------------------------------------------------------
-- uart_start_qualify -- two start-bit detectors racing on one wire.
--
-- The naive detector does what the textbook diagram implies: a falling edge is
-- a start bit. The qualified detector does what a 16550 actually does -- it
-- treats the edge as a CANDIDATE, waits until the middle of the putative start
-- bit, and takes a three-sample majority vote before committing. For DIV = 16
-- those three samples land on oversample ticks 6, 7 and 8, which is the
-- classic arrangement.
--
-- Running both against the same wire turns "add a glitch filter" from advice
-- into a measurement: the two counters disagree by exactly the number of
-- events that were never frames.
--
-- Both detectors hold off for a whole frame after committing. That matters for
-- a fair comparison -- a detector that re-triggers inside its own frame would
-- inflate its count for reasons that have nothing to do with qualification.
-- ---------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity uart_start_qualify is
generic (
DIV : natural := 16 -- clocks per bit
);
port (
clk : in std_logic;
rst_n : in std_logic;
line_i : in std_logic;
naive_cnt_o : out unsigned(15 downto 0); -- frames the naive detector started
cand_cnt_o : out unsigned(15 downto 0); -- edges the qualifier examined
qual_cnt_o : out unsigned(15 downto 0); -- candidates it committed to
rej_cnt_o : out unsigned(15 downto 0); -- candidates it threw away
qual_busy_o : out std_logic
);
end entity uart_start_qualify;
architecture rtl of uart_start_qualify is
constant FRAME : natural := 10 * DIV; -- start + 8 data + stop, in clocks
type qstate_t is (Q_IDLE, Q_ARM, Q_BUSY);
signal line_q : std_logic := '1';
signal fall : std_logic;
signal naive_hold : unsigned(15 downto 0) := (others => '0');
signal naive_cnt : unsigned(15 downto 0) := (others => '0');
signal qstate : qstate_t := Q_IDLE;
signal qcnt : unsigned(15 downto 0) := (others => '0');
signal votes : unsigned(1 downto 0) := (others => '0');
signal cand_cnt : unsigned(15 downto 0) := (others => '0');
signal qual_cnt : unsigned(15 downto 0) := (others => '0');
signal rej_cnt : unsigned(15 downto 0) := (others => '0');
-- VHDL has no conditional expression inside an operand before VHDL-2019,
-- so the third sample's contribution is carried on its own signal.
signal this_vote : unsigned(1 downto 0);
begin
fall <= '1' when (line_q = '1' and line_i = '0') else '0';
this_vote <= to_unsigned(1, 2) when line_i = '0' else to_unsigned(0, 2);
naive_cnt_o <= naive_cnt;
cand_cnt_o <= cand_cnt;
qual_cnt_o <= qual_cnt;
rej_cnt_o <= rej_cnt;
qual_busy_o <= '0' when qstate = Q_IDLE else '1';
process (clk, rst_n)
begin
if rst_n = '0' then
line_q <= '1';
naive_hold <= (others => '0');
naive_cnt <= (others => '0');
qstate <= Q_IDLE;
qcnt <= (others => '0');
votes <= (others => '0');
cand_cnt <= (others => '0');
qual_cnt <= (others => '0');
rej_cnt <= (others => '0');
elsif rising_edge(clk) then
line_q <= line_i;
-- ---- naive: an edge is a frame, no questions asked ----------
if naive_hold = 0 then
if fall = '1' then
naive_cnt <= naive_cnt + 1;
naive_hold <= to_unsigned(FRAME, 16);
end if;
else
naive_hold <= naive_hold - 1;
end if;
-- ---- qualified: an edge is a question ------------------------
case qstate is
when Q_IDLE =>
if fall = '1' then
cand_cnt <= cand_cnt + 1;
qstate <= Q_ARM;
qcnt <= (others => '0');
votes <= (others => '0');
end if;
when Q_ARM =>
-- three samples straddling the middle of the start bit
if qcnt = to_unsigned((DIV/2) - 2, 16) or
qcnt = to_unsigned((DIV/2) - 1, 16) or
qcnt = to_unsigned((DIV/2), 16) then
if line_i = '0' then
votes <= votes + 1;
end if;
end if;
if qcnt = to_unsigned((DIV/2), 16) then
-- majority of three: two SPACE samples commit the frame
if (votes + this_vote) >= 2 then
qual_cnt <= qual_cnt + 1;
qstate <= Q_BUSY;
qcnt <= (others => '0');
else
rej_cnt <= rej_cnt + 1;
qstate <= Q_IDLE;
end if;
else
qcnt <= qcnt + 1;
end if;
when Q_BUSY =>
-- hold off for the rest of the frame
if qcnt = to_unsigned(FRAME - (DIV/2) - 1, 16) then
qstate <= Q_IDLE;
else
qcnt <= qcnt + 1;
end if;
end case;
end if;
end process;
end architecture rtl;The cand_cnt_o output is the one that earns its place in a debug register. It counts edges the qualifier examined, whether or not they were accepted, and §5 shows why that is the single most diagnostic number on the block.
4. The Measured Rejection Boundary
Rather than asserting a threshold, the testbench sweeps the pulse width from 1 clock to a full bit and reports the narrowest pulse the qualifier commits to. With DIV = 16, the three samples land on ticks 6, 7 and 8:
spike width sweep (DIV = 16, samples at ticks 6/7/8):
width 1 clocks -> rejected width 9 clocks -> COMMITTED
width 2 clocks -> rejected width 10 clocks -> COMMITTED
width 3 clocks -> rejected width 11 clocks -> COMMITTED
width 4 clocks -> rejected width 12 clocks -> COMMITTED
width 5 clocks -> rejected width 13 clocks -> COMMITTED
width 6 clocks -> rejected width 14 clocks -> COMMITTED
width 7 clocks -> rejected width 15 clocks -> COMMITTED
width 8 clocks -> rejected width 16 clocks -> COMMITTED
narrowest committed pulse: 9 clocksThe transition is sharp — no ambiguous band — and it lands at 9 clocks, which is DIV/2 + 1.
That value follows directly from the design. The edge is registered at clock 0; the three votes are taken at clocks 7, 8 and 9 after it. A majority needs two of those to read SPACE, so the pulse must still be low at clock 8, which means it must be at least 9 clocks wide. The sweep and the arithmetic agree exactly, and the testbench asserts the measured boundary against the formula rather than against a constant somebody typed in.
5. What the Detectors Measured
scenario naive qualified rejected candidates
------------------------------- ----- --------- -------- ----------
T1 six real frames 6 6 0 6
T2 six 3-clock spikes 6 0 6 6
T3 four frames among four spikes 8 4 4 8
T4 line stuck at MARK 0 0 0 0
T5 a break (long SPACE) 1 1 0 1T2 is the headline: six spikes, and the naive detector started six frames on them. Each of those frames occupied it for ten bit periods, during which a real frame would have been lost.
T3 mixes them, and the arithmetic is worth stating explicitly. The naive detector counted 8, the qualifier counted 4, and the difference is exactly the number of spikes. The qualifier did not merely reduce the error rate; it recovered the true frame count.
The two rows that are really a diagnostic
T4 and T5 look like edge cases and are in fact the most useful part of the block.
T4 — the line is stuck at MARK. Nothing arrives. cand_cnt_o is zero, meaning no falling edge ever reached the receiver. That single fact separates two faults that look identical from software:
candidates = 0 -> nothing arrived. The problem is upstream:
transmitter, wiring, level shifter, or the
far end never being configured.
candidates > 0 -> edges ARE arriving and the receiver is
discarding them. The problem is here:
qualification, polarity, or baud.
but frames = 0Without a candidate counter both cases present as "no data received", and teams spend days on the wrong end of the cable. With it, the question is answered in one register read.
T5 — a break. A break is a long SPACE, far longer than a frame. It has exactly one falling edge, and at mid-bit it is unambiguously SPACE, so the qualifier commits to it — and that is correct. A break is a legitimate signalling event that the receiver must see and report as a framing error, not filter away. A qualifier that rejected breaks would be discarding a protocol feature in the name of noise immunity.
6. The Testbench
Verilog
`timescale 1ns/1ps
// ---------------------------------------------------------------------------
// Testbench for uart_start_qualify.
//
// The two detectors see the same wire, so every test is a controlled
// comparison rather than a claim. The last test does not assert a threshold at
// all -- it sweeps the spike width and REPORTS the narrowest pulse the
// qualifier will commit to, then checks that measured boundary against the
// sampling arithmetic.
// ---------------------------------------------------------------------------
module tb_uart_start_qualify;
localparam DIV = 16;
localparam FRAME = 10 * DIV;
reg clk = 1'b0;
reg rst_n = 1'b0;
reg line = 1'b1;
wire [15:0] naive_cnt, cand_cnt, qual_cnt, rej_cnt;
wire qual_busy;
integer checks = 0;
integer fails = 0;
always #5 clk = ~clk;
uart_start_qualify #(.DIV(DIV)) dut (
.clk(clk), .rst_n(rst_n), .line_i(line),
.naive_cnt_o(naive_cnt), .cand_cnt_o(cand_cnt),
.qual_cnt_o(qual_cnt), .rej_cnt_o(rej_cnt), .qual_busy_o(qual_busy));
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
task do_reset;
begin
line = 1'b1; rst_n = 1'b0;
repeat (4) @(posedge clk);
@(negedge clk); rst_n = 1'b1;
repeat (4) @(posedge clk);
end
endtask
task hold_line;
input lvl;
input integer n;
integer i;
begin
@(negedge clk);
line = lvl;
for (i = 0; i < n; i = i + 1) @(posedge clk);
end
endtask
// one 8N1 frame at DIV clocks per bit, then two idle bit-times
task send_frame;
input [7:0] b;
integer j;
begin
hold_line(1'b0, DIV); // start
for (j = 0; j < 8; j = j + 1) hold_line(b[j], DIV);
hold_line(1'b1, DIV); // stop
hold_line(1'b1, 2*DIV); // gap
end
endtask
// a SPACE pulse `w` clocks wide, then a full frame time of idle so both
// detectors are certainly free again
task send_spike;
input integer w;
begin
hold_line(1'b0, w);
hold_line(1'b1, FRAME + 2*DIV);
end
endtask
integer i, w, min_accept;
initial begin
// ---------------- T1: six real frames -----------------------------
do_reset;
for (i = 0; i < 6; i = i + 1) send_frame(8'h55);
#1;
$display("T1 six real frames : naive=%0d qualified=%0d rejected=%0d",
naive_cnt, qual_cnt, rej_cnt);
chk("T1 naive counts six", naive_cnt, 6);
chk("T1 qualified counts six", qual_cnt, 6);
chk("T1 nothing rejected", rej_cnt, 0);
// ---------------- T2: six narrow spikes, no frames at all ---------
do_reset;
for (i = 0; i < 6; i = i + 1) send_spike(3);
#1;
$display("T2 six 3-clock spikes : naive=%0d qualified=%0d rejected=%0d",
naive_cnt, qual_cnt, rej_cnt);
chk("T2 naive was fooled six times", naive_cnt, 6);
chk("T2 qualifier committed to none", qual_cnt, 0);
chk("T2 qualifier rejected all six", rej_cnt, 6);
chk("T2 both saw the same edges", cand_cnt, naive_cnt);
// ---------------- T3: four frames among four spikes ---------------
do_reset;
for (i = 0; i < 4; i = i + 1) begin
send_frame(8'h3C);
send_spike(2);
end
#1;
$display("T3 4 frames + 4 spikes: naive=%0d qualified=%0d rejected=%0d",
naive_cnt, qual_cnt, rej_cnt);
chk("T3 qualified counted only the frames", qual_cnt, 4);
chk("T3 naive counted the spikes too", naive_cnt, 8);
chk("T3 the difference is the spike count", naive_cnt - qual_cnt, 4);
// ---------------- T4: nothing is arriving at all ------------------
// A dead transmitter and a deaf receiver look identical from software.
// cand_cnt separates them: zero candidates means no edges reached us.
do_reset;
hold_line(1'b1, 4*FRAME);
#1;
$display("T4 line stuck at MARK : candidates=%0d naive=%0d", cand_cnt, naive_cnt);
chk("T4 no edges arrived", cand_cnt, 0);
chk("T4 no frames started", naive_cnt, 0);
// ---------------- T5: a break is not noise ------------------------
// A long SPACE has one falling edge and is SPACE at mid-bit, so the
// qualifier commits -- correctly. Rejecting a break would be a bug.
do_reset;
hold_line(1'b0, 3*FRAME);
hold_line(1'b1, 2*FRAME);
#1;
$display("T5 break (long SPACE) : candidates=%0d qualified=%0d rejected=%0d",
cand_cnt, qual_cnt, rej_cnt);
chk("T5 exactly one edge", cand_cnt, 1);
chk("T5 the qualifier committed", qual_cnt, 1);
chk("T5 a break was not rejected", rej_cnt, 0);
// ---------------- T6: measure the rejection boundary --------------
// Sweep the pulse width and find the narrowest one that commits.
min_accept = -1;
$display("");
$display("spike width sweep (DIV = %0d, samples at ticks %0d/%0d/%0d):",
DIV, (DIV/2)-2, (DIV/2)-1, DIV/2);
for (w = 1; w <= DIV; w = w + 1) begin
do_reset;
send_spike(w);
#1;
$display(" width %2d clocks -> %0s", w,
(qual_cnt == 1) ? "COMMITTED" : "rejected");
if (qual_cnt == 1 && min_accept == -1) min_accept = w;
end
$display("");
$display("narrowest committed pulse: %0d clocks", min_accept);
// The third sample lands DIV/2 ticks after the edge was registered, so
// a majority needs the line still SPACE at that tick: w >= DIV/2 + 1.
chk("T6 boundary matches the sampling arithmetic", min_accept, (DIV/2) + 1);
do_reset;
send_spike((DIV/2));
#1;
chk("T6 one clock below the boundary is rejected", qual_cnt, 0);
do_reset;
send_spike((DIV/2) + 1);
#1;
chk("T6 the boundary width commits", qual_cnt, 1);
$display("");
$display("== %0d checks, %0d failures ==", checks, fails);
if (fails == 0) $display(" RESULT: ALL VERILOG START-QUALIFY TESTS PASSED");
else $display(" RESULT: %0d FAILURE(S)", fails);
$finish;
end
endmoduleSystemVerilog
`timescale 1ns/1ps
// ---------------------------------------------------------------------------
// Testbench for uart_start_qualify.
//
// The two detectors see the same wire, so every test is a controlled
// comparison rather than a claim. The last test does not assert a threshold at
// all -- it sweeps the spike width and REPORTS the narrowest pulse the
// qualifier will commit to, then checks that measured boundary against the
// sampling arithmetic.
// ---------------------------------------------------------------------------
module tb_uart_start_qualify;
localparam DIV = 16;
localparam FRAME = 10 * DIV;
logic clk = 1'b0;
logic rst_n = 1'b0;
logic line = 1'b1;
logic [15:0] naive_cnt, cand_cnt, qual_cnt, rej_cnt;
logic qual_busy;
integer checks = 0;
integer fails = 0;
always #5 clk = ~clk;
uart_start_qualify #(.DIV(DIV)) dut (
.clk(clk), .rst_n(rst_n), .line_i(line),
.naive_cnt_o(naive_cnt), .cand_cnt_o(cand_cnt),
.qual_cnt_o(qual_cnt), .rej_cnt_o(rej_cnt), .qual_busy_o(qual_busy));
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
task automatic do_reset();
begin
line = 1'b1; rst_n = 1'b0;
repeat (4) @(posedge clk);
@(negedge clk); rst_n = 1'b1;
repeat (4) @(posedge clk);
end
endtask
task automatic hold_line(input logic lvl, input int n);
int i;
begin
@(negedge clk);
line = lvl;
for (i = 0; i < n; i = i + 1) @(posedge clk);
end
endtask
// one 8N1 frame at DIV clocks per bit, then two idle bit-times
task automatic send_frame(input logic [7:0] b);
int j;
begin
hold_line(1'b0, DIV); // start
for (j = 0; j < 8; j = j + 1) hold_line(b[j], DIV);
hold_line(1'b1, DIV); // stop
hold_line(1'b1, 2*DIV); // gap
end
endtask
// a SPACE pulse `w` clocks wide, then a full frame time of idle so both
// detectors are certainly free again
task automatic send_spike(input int w);
begin
hold_line(1'b0, w);
hold_line(1'b1, FRAME + 2*DIV);
end
endtask
integer i, w, min_accept;
initial begin
// ---------------- T1: six real frames -----------------------------
do_reset;
for (i = 0; i < 6; i = i + 1) send_frame(8'h55);
#1;
$display("T1 six real frames : naive=%0d qualified=%0d rejected=%0d",
naive_cnt, qual_cnt, rej_cnt);
chk("T1 naive counts six", naive_cnt, 6);
chk("T1 qualified counts six", qual_cnt, 6);
chk("T1 nothing rejected", rej_cnt, 0);
// ---------------- T2: six narrow spikes, no frames at all ---------
do_reset;
for (i = 0; i < 6; i = i + 1) send_spike(3);
#1;
$display("T2 six 3-clock spikes : naive=%0d qualified=%0d rejected=%0d",
naive_cnt, qual_cnt, rej_cnt);
chk("T2 naive was fooled six times", naive_cnt, 6);
chk("T2 qualifier committed to none", qual_cnt, 0);
chk("T2 qualifier rejected all six", rej_cnt, 6);
chk("T2 both saw the same edges", cand_cnt, naive_cnt);
// ---------------- T3: four frames among four spikes ---------------
do_reset;
for (i = 0; i < 4; i = i + 1) begin
send_frame(8'h3C);
send_spike(2);
end
#1;
$display("T3 4 frames + 4 spikes: naive=%0d qualified=%0d rejected=%0d",
naive_cnt, qual_cnt, rej_cnt);
chk("T3 qualified counted only the frames", qual_cnt, 4);
chk("T3 naive counted the spikes too", naive_cnt, 8);
chk("T3 the difference is the spike count", naive_cnt - qual_cnt, 4);
// ---------------- T4: nothing is arriving at all ------------------
// A dead transmitter and a deaf receiver look identical from software.
// cand_cnt separates them: zero candidates means no edges reached us.
do_reset;
hold_line(1'b1, 4*FRAME);
#1;
$display("T4 line stuck at MARK : candidates=%0d naive=%0d", cand_cnt, naive_cnt);
chk("T4 no edges arrived", cand_cnt, 0);
chk("T4 no frames started", naive_cnt, 0);
// ---------------- T5: a break is not noise ------------------------
// A long SPACE has one falling edge and is SPACE at mid-bit, so the
// qualifier commits -- correctly. Rejecting a break would be a bug.
do_reset;
hold_line(1'b0, 3*FRAME);
hold_line(1'b1, 2*FRAME);
#1;
$display("T5 break (long SPACE) : candidates=%0d qualified=%0d rejected=%0d",
cand_cnt, qual_cnt, rej_cnt);
chk("T5 exactly one edge", cand_cnt, 1);
chk("T5 the qualifier committed", qual_cnt, 1);
chk("T5 a break was not rejected", rej_cnt, 0);
// ---------------- T6: measure the rejection boundary --------------
// Sweep the pulse width and find the narrowest one that commits.
min_accept = -1;
$display("");
$display("spike width sweep (DIV = %0d, samples at ticks %0d/%0d/%0d):",
DIV, (DIV/2)-2, (DIV/2)-1, DIV/2);
for (w = 1; w <= DIV; w = w + 1) begin
do_reset;
send_spike(w);
#1;
$display(" width %2d clocks -> %0s", w,
(qual_cnt == 1) ? "COMMITTED" : "rejected");
if (qual_cnt == 1 && min_accept == -1) min_accept = w;
end
$display("");
$display("narrowest committed pulse: %0d clocks", min_accept);
// The third sample lands DIV/2 ticks after the edge was registered, so
// a majority needs the line still SPACE at that tick: w >= DIV/2 + 1.
chk("T6 boundary matches the sampling arithmetic", min_accept, (DIV/2) + 1);
do_reset;
send_spike((DIV/2));
#1;
chk("T6 one clock below the boundary is rejected", qual_cnt, 0);
do_reset;
send_spike((DIV/2) + 1);
#1;
chk("T6 the boundary width commits", qual_cnt, 1);
$display("");
$display("== %0d checks, %0d failures ==", checks, fails);
if (fails == 0) $display(" RESULT: ALL SYSTEMVERILOG START-QUALIFY TESTS PASSED");
else $display(" RESULT: %0d FAILURE(S)", fails);
$finish;
end
endmoduleVHDL
-- ---------------------------------------------------------------------------
-- Testbench for uart_start_qualify.
--
-- The two detectors see the same wire, so every test is a controlled
-- comparison rather than a claim. The last test does not assert a threshold at
-- all -- it sweeps the spike width and REPORTS the narrowest pulse the
-- qualifier will commit to, then checks that measured boundary against the
-- sampling arithmetic.
-- ---------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity tb_uart_start_qualify is
end entity tb_uart_start_qualify;
architecture sim of tb_uart_start_qualify is
constant DIV : natural := 16;
constant FRAME : natural := 10 * DIV;
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 naive_cnt, cand_cnt, qual_cnt, rej_cnt : unsigned(15 downto 0);
signal qual_busy : std_logic;
begin
clk <= '0' when done else not clk after TCLK/2;
dut : entity work.uart_start_qualify
generic map (DIV => DIV)
port map (clk => clk, rst_n => rst_n, line_i => line,
naive_cnt_o => naive_cnt, cand_cnt_o => cand_cnt,
qual_cnt_o => qual_cnt, rej_cnt_o => rej_cnt,
qual_busy_o => qual_busy);
stim : process
variable checks, fails : integer := 0;
variable min_accept : integer := -1;
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 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 3 loop wait until rising_edge(clk); end loop;
end procedure;
procedure hold_line (lvl : std_logic; n : integer) is
begin
wait until falling_edge(clk);
line <= lvl;
for i in 0 to n-1 loop wait until rising_edge(clk); end loop;
end procedure;
-- one 8N1 frame at DIV clocks per bit, then two idle bit-times
procedure send_frame (b : std_logic_vector(7 downto 0)) is
begin
hold_line('0', DIV); -- start
for j in 0 to 7 loop hold_line(b(j), DIV); end loop;
hold_line('1', DIV); -- stop
hold_line('1', 2*DIV); -- gap
end procedure;
-- a SPACE pulse `w` clocks wide, then a full frame time of idle so both
-- detectors are certainly free again
procedure send_spike (w : integer) is
begin
hold_line('0', w);
hold_line('1', FRAME + 2*DIV);
end procedure;
begin
-- ---------------- T1: six real frames -----------------------------
do_reset;
for i in 0 to 5 loop send_frame(x"55"); end loop;
wait for 1 ns;
report "T1 six real frames : naive=" & integer'image(to_integer(naive_cnt)) &
" qualified=" & integer'image(to_integer(qual_cnt)) &
" rejected=" & integer'image(to_integer(rej_cnt));
chk("T1 naive counts six", to_integer(naive_cnt), 6);
chk("T1 qualified counts six", to_integer(qual_cnt), 6);
chk("T1 nothing rejected", to_integer(rej_cnt), 0);
-- ---------------- T2: six narrow spikes, no frames at all ---------
do_reset;
for i in 0 to 5 loop send_spike(3); end loop;
wait for 1 ns;
report "T2 six 3-clock spikes : naive=" & integer'image(to_integer(naive_cnt)) &
" qualified=" & integer'image(to_integer(qual_cnt)) &
" rejected=" & integer'image(to_integer(rej_cnt));
chk("T2 naive was fooled six times", to_integer(naive_cnt), 6);
chk("T2 qualifier committed to none", to_integer(qual_cnt), 0);
chk("T2 qualifier rejected all six", to_integer(rej_cnt), 6);
chk("T2 both saw the same edges", to_integer(cand_cnt), to_integer(naive_cnt));
-- ---------------- T3: four frames among four spikes ---------------
do_reset;
for i in 0 to 3 loop
send_frame(x"3C");
send_spike(2);
end loop;
wait for 1 ns;
report "T3 4 frames + 4 spikes: naive=" & integer'image(to_integer(naive_cnt)) &
" qualified=" & integer'image(to_integer(qual_cnt)) &
" rejected=" & integer'image(to_integer(rej_cnt));
chk("T3 qualified counted only the frames", to_integer(qual_cnt), 4);
chk("T3 naive counted the spikes too", to_integer(naive_cnt), 8);
chk("T3 the difference is the spike count",
to_integer(naive_cnt) - to_integer(qual_cnt), 4);
-- ---------------- T4: nothing is arriving at all ------------------
-- A dead transmitter and a deaf receiver look identical from software.
-- cand_cnt separates them: zero candidates means no edges reached us.
do_reset;
hold_line('1', 4*FRAME);
wait for 1 ns;
report "T4 line stuck at MARK : candidates=" & integer'image(to_integer(cand_cnt)) &
" naive=" & integer'image(to_integer(naive_cnt));
chk("T4 no edges arrived", to_integer(cand_cnt), 0);
chk("T4 no frames started", to_integer(naive_cnt), 0);
-- ---------------- T5: a break is not noise ------------------------
-- A long SPACE has one falling edge and is SPACE at mid-bit, so the
-- qualifier commits -- correctly. Rejecting a break would be a bug.
do_reset;
hold_line('0', 3*FRAME);
hold_line('1', 2*FRAME);
wait for 1 ns;
report "T5 break (long SPACE) : candidates=" & integer'image(to_integer(cand_cnt)) &
" qualified=" & integer'image(to_integer(qual_cnt)) &
" rejected=" & integer'image(to_integer(rej_cnt));
chk("T5 exactly one edge", to_integer(cand_cnt), 1);
chk("T5 the qualifier committed", to_integer(qual_cnt), 1);
chk("T5 a break was not rejected", to_integer(rej_cnt), 0);
-- ---------------- T6: measure the rejection boundary --------------
-- Sweep the pulse width and find the narrowest one that commits.
min_accept := -1;
report "";
report "spike width sweep (DIV = " & integer'image(DIV) &
", samples at ticks " & integer'image((DIV/2)-2) & "/" &
integer'image((DIV/2)-1) & "/" & integer'image(DIV/2) & "):";
for w in 1 to DIV loop
do_reset;
send_spike(w);
wait for 1 ns;
if to_integer(qual_cnt) = 1 then
report " width " & integer'image(w) & " clocks -> COMMITTED";
if min_accept = -1 then min_accept := w; end if;
else
report " width " & integer'image(w) & " clocks -> rejected";
end if;
end loop;
report "";
report "narrowest committed pulse: " & integer'image(min_accept) & " clocks";
-- The third sample lands DIV/2 ticks after the edge was registered, so
-- a majority needs the line still SPACE at that tick: w >= DIV/2 + 1.
chk("T6 boundary matches the sampling arithmetic", min_accept, (DIV/2) + 1);
do_reset;
send_spike(DIV/2);
wait for 1 ns;
chk("T6 one clock below the boundary is rejected", to_integer(qual_cnt), 0);
do_reset;
send_spike((DIV/2) + 1);
wait for 1 ns;
chk("T6 the boundary width commits", to_integer(qual_cnt), 1);
report "";
report "== " & integer'image(checks) & " checks, " &
integer'image(fails) & " failures ==";
if fails = 0 then
report " RESULT: ALL VHDL START-QUALIFY TESTS PASSED";
else
report " RESULT: " & integer'image(fails) & " FAILURE(S)" severity error;
end if;
done <= true;
wait;
end process;
end architecture sim;7. Proving the Tests Can Fail
mutation checks failed verdict
--------------------------------------------------- ------------- -------
M9 demand 3 of 3 samples instead of a majority 2 killed
M10 remove the naive detector's frame hold-off 3 killedM9 is killed by the boundary checks, exactly as intended: the measured narrowest-accepted width moves from 9 to 10 and both the swept measurement and the arithmetic assertion disagree with it.
M10 is a mutation of the reference, not of the design under test, and it is there deliberately. If the naive detector could re-trigger inside its own frame, its count would inflate for reasons that have nothing to do with start-bit qualification, and every comparison in §5 would overstate the qualifier's benefit. Three checks fail, which confirms the comparison is measuring what it claims to measure.
8. A Diagnostic Sequence
When a receiver is producing nothing, or producing bytes nobody sent, these questions resolve it in order — each one narrowing the space before the next is worth asking:
- Are any edges arriving? Read the candidate count. Zero means the problem is upstream of this receiver, and nothing about the receiver's configuration can be the cause.
- Are candidates being rejected? A high rejection count with real traffic expected means either genuine noise, or a start bit shortened below the filter's threshold by sampling drift — check the baud before blaming the cabling.
- Is the line idling at the right level? An idle line must sit at MARK. If it idles at SPACE, the polarity is inverted somewhere — a level shifter, an opto-isolator, or an RS-232 transceiver that is present at one end and not the other — and the receiver will see a continuous break rather than traffic.
- Do frames start but not finish? That is not a start-bit problem. It is sampling drift, and Chapter 17.2 owns it.
- Do frames complete but bytes go missing? Also not a start-bit problem. The frames were received and then lost downstream — Chapter 17.5.
The ordering matters because each step's evidence is only meaningful if the previous step has been settled. Debugging a qualification threshold on a link where no edges are arriving is a common and entirely avoidable way to lose a day.
Continue learning
Related tutorials
- Related topic
FPGA Debug, ILA Capture and Board Bring-Up
A capture buffer whose trigger schedules the stop rather than the start, the off-by-one in the ring unwrap that only unique probe data exposes, and a bring-up sequence that orders the measurements so each one is interpretable.
- 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
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.
- 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.
Where this fits
Part of the UART curriculum.
