UART · Module 9
False Starts, Noise and Malformed Frames
A glitch, a hot-plug or a reset released onto live traffic can put a receiver into a frame nobody transmitted. Idle detection suppresses it — at a cost worth pricing before adopting.
Every failure so far involved a frame that was really sent. This chapter covers the opposite: a frame the receiver assembled that nobody transmitted.
Chapter 5.2 built start qualification and established its exact guarantee — it rejects disturbances shorter than the validation delay, and nothing more. Chapter 6.2 §7 then demonstrated the case qualification cannot help with: a receiver released from reset onto a live line produced 0xFA from a frame carrying 0xA6, with clean status.
Both chapters stated the limit and deferred the remedy here. So this chapter does three things: it maps the space of start-side failures precisely, it builds the idle detection that closes the mid-frame case, and it prices that remedy honestly — because it is not free and it is not always wanted.
1. Four Start-Side Failures, by What Rejects Them
They are usually lumped together as "noise", and they are rejected — or not — by completely different mechanisms.
| Failure | Shape on the wire | Rejected by | Cost when it gets through |
|---|---|---|---|
| Narrow glitch | low for less than the validation delay | qualification — 5.2 | none; it never becomes a frame |
| Wide disturbance | low past the validation point | nothing | a fabricated frame, timed from the glitch |
| Mid-sample noise | transient at one sampling instant | majority voting, if fitted — 5.4 | one wrong bit, possibly caught by parity |
| Mid-frame attach | the line is mid-frame when we start looking | idle detection — §4 | a fabricated frame from someone else's data |
Only the first is handled by the receiver as built in Module 6. The second and fourth produce complete, well-formed-looking frames that the receiver has no basis to reject, and the third is Chapter 5.4's trade.
2. The Glitch That Is Rejected
The case qualification handles, for completeness and as the positive control every other case is measured against.
A disturbance shorter than M/2 oversample ticks: the edge detector produces a candidate, the qualifier starts counting, and at the validation point the line has returned to mark. The candidate is abandoned, no frame begins, and nothing is reported — there is no error, because nothing was lost.
That silence is correct and worth defending. A receiver that reported every rejected candidate would produce a stream of "errors" on any electrically busy board, and the events it reported would carry no information: a rejected candidate is the mechanism working.
Where a counter is genuinely useful is bring-up. A rejected-candidate count that is zero on a healthy board and climbing on a marginal one is a sensitive early indicator — and it belongs as a debug output rather than an error flag, for the reason above.
3. The Glitch That Is Not
Widen the same disturbance past the validation point and everything changes:
candidate at t=0 edge detector fires
line still at space at M/2 qualification PASSES
-> start accepted, timing origin set to a disturbance
-> eight data samples taken at 1.5, 2.5, ... UI from nothing in particular
-> a stop sample taken at 9.5 UI
-> a byte is deliveredThe receiver has no basis to reject this. Every check it performs passes or fails on its own terms: the start qualified, the data intervals were sampled at their scheduled positions, and the stop sample lands wherever the real traffic happens to be. If it happens to find mark there, the frame is delivered with completely clean status.
The observable consequences:
A framing error is likely but not guaranteed. The stop sample lands at an arbitrary point relative to real traffic, so it is mark roughly half the time on a busy line and always mark on an idle one.
The payload is meaningless, and there is no flag for that. Chapter 9.2 applies: parity over a fabricated payload is satisfied about half the time.
The receiver's timing origin is now wrong for subsequent frames too, until it returns to idle and re-anchors on a real start edge — which is Chapter 9.6's subject.
Narrow glitch rejected, wider glitch accepted
16 cycles4. Mid-Frame Attach, and the Block That Fixes It
Chapter 6.2 §7 demonstrated this and left it open. A receiver begins observing a line that is already carrying traffic — after reset, after hot-plug, after a connector is inserted — and the first mark-to-space transition it sees is not a start bit. It is a data edge inside someone else's frame.
The receiver qualifies it correctly, samples eight intervals from it correctly, and delivers a byte assembled from the tail of one frame and the head of the next. Simulation of the Module 6 receiver produced 0xFA from a frame carrying 0xA6, with the framing check passing because the line happened to be at mark where the stop sample landed.
Nothing in a UART frame identifies itself as a continuation. That is the direct consequence of Chapter 3.1's observation that framing is re-established from scratch at every start bit — there is no frame counter, no sequence number and no header. A receiver joining mid-stream cannot know it has.
The remedy is to refuse to trust a start until the line has been quiet long enough to prove no frame is in progress:
Require the line to have been continuously at MARK for one full frame time
before any start candidate may be qualified.A valid frame cannot hold the line at mark for a full frame time — the start interval is space — so observing that condition proves the line is genuinely idle rather than mid-frame.
A receiver joining live traffic
13 cyclesThe RTL
// ---------------------------------------------------------------------------
// 9.5 — idle qualification. The technique Chapter 6.2 §7 deferred: refuse to
// accept a start until the line has been at mark for a full frame time, so a
// receiver joining a live line cannot mis-frame on the remainder.
// ---------------------------------------------------------------------------
module uart_idle_qualify #(
parameter int unsigned OVERSAMPLE = 16,
parameter int unsigned FRAME_BITS = 10
) (
input logic clk,
input logic rst_n,
input logic os_tick_i,
input logic rx_sync_i,
output logic line_idle_o // LEVEL: mark held for a full frame time
);
localparam int unsigned IDLE_TICKS = FRAME_BITS * OVERSAMPLE;
localparam int unsigned CNT_W = (IDLE_TICKS <= 1) ? 1 : $clog2(IDLE_TICKS + 1);
logic [CNT_W-1:0] mark_cnt_q;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
mark_cnt_q <= '0;
line_idle_o <= 1'b0; // NOT idle out of reset — must be earned
end else if (rx_sync_i == 1'b0) begin
mark_cnt_q <= '0;
line_idle_o <= 1'b0;
end else if (os_tick_i) begin
if (mark_cnt_q == CNT_W'(IDLE_TICKS)) begin
mark_cnt_q <= mark_cnt_q; // saturate
end else begin
mark_cnt_q <= mark_cnt_q + 1'b1;
if (mark_cnt_q == CNT_W'(IDLE_TICKS - 1)) line_idle_o <= 1'b1;
end
end
end
endmoduleIt is the break detector's structure with the polarity inverted, which is not a coincidence: both answer "has the line held one level long enough to prove something?" — one about space, one about mark. Sharing the shape means sharing the reasoning, including the saturation and the +1 width derivation of Chapter 9.4 §4.
line_idle_o resets to 0, not 1. Idle must be earned, not assumed. A receiver out of reset has observed nothing and has no basis to claim the line is quiet — which is precisely the mid-frame case this block exists to catch. Resetting it high would defeat the entire mechanism at the one moment it matters most.
A single low sample clears it immediately, without waiting for a tick, for the same reason the break detector resets on a single mark: the condition is continuous, and deferring the clear would let a narrow disturbance be absorbed.
Integration is one extra term at the qualifier:
// Conceptual SystemVerilog — gating start acceptance on proven idle.
// The qualifier itself is Chapter 5.2's, unmodified; this adds a
// precondition rather than changing the mechanism.
if (start_cand_i && line_idle_o) begin
// ... Chapter 5.2's qualification proceeds from here
endThe measured behaviour
| Stimulus | Expected | Result |
|---|---|---|
| line low | not idle | pass |
| 159 mark ticks (one short of a frame) | not idle | pass |
| 160 mark ticks (a full frame) | idle | pass |
| counter after threshold | saturates at 160 | pass |
| a single low sample | idle cleared immediately | pass |
5. The Same Qualifier in Three Languages
//===========================================================================
// uart_idle_qualify_v — Synthesizable Verilog-2001
//
// Idle qualification: refuse to call the line idle until it has been at
// MARK for a full frame time. A receiver that powers up mid-character would
// otherwise treat the next low interval as a start bit and mis-frame on the
// remainder of somebody else's character.
//
// line_idle_o is NOT set out of reset. Idle is earned by observation, never
// assumed — that is the contract, and Chapter 9.5 §5 shows what assuming it
// costs.
//===========================================================================
module uart_idle_qualify_v #(
parameter OVERSAMPLE = 16,
parameter FRAME_BITS = 10
) (
input wire clk,
input wire rst_n,
input wire os_tick_i,
input wire rx_sync_i,
output reg line_idle_o
);
localparam IDLE_TICKS = FRAME_BITS * OVERSAMPLE;
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 CNT_W = (IDLE_TICKS <= 1) ? 1 : clog2(IDLE_TICKS + 1);
reg [CNT_W-1:0] mark_cnt_q;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
mark_cnt_q <= {CNT_W{1'b0}};
line_idle_o <= 1'b0; // must be earned
end else if (rx_sync_i == 1'b0) begin
mark_cnt_q <= {CNT_W{1'b0}};
line_idle_o <= 1'b0;
end else if (os_tick_i) begin
if (mark_cnt_q == IDLE_TICKS[CNT_W-1:0]) begin
mark_cnt_q <= mark_cnt_q; // saturate
end else begin
mark_cnt_q <= mark_cnt_q + 1'b1;
if (mark_cnt_q == (IDLE_TICKS[CNT_W-1:0] - 1'b1))
line_idle_o <= 1'b1;
end
end
end
endmodule--==========================================================================
-- uart_idle_qualify -- Synthesizable VHDL-2008
--
-- Idle is EARNED by observation, never assumed: line_idle_o is '0' out of
-- reset and only rises after a full frame time of continuous mark. A
-- receiver that joins a live line mid-character therefore cannot treat the
-- remainder of somebody else's character as a frame.
--==========================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use ieee.math_real.all;
entity uart_idle_qualify is
generic (
OVERSAMPLE : positive := 16;
FRAME_BITS : positive := 10
);
port (
clk : in std_logic;
rst_n : in std_logic;
os_tick_i : in std_logic;
rx_sync_i : in std_logic;
line_idle_o : out std_logic
);
end entity uart_idle_qualify;
architecture rtl of uart_idle_qualify is
constant IDLE_TICKS : positive := FRAME_BITS * OVERSAMPLE;
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 CNT_W : positive := cnt_width(IDLE_TICKS + 1);
signal mark_cnt_q : unsigned(CNT_W-1 downto 0);
begin
process (clk, rst_n)
begin
if rst_n = '0' then
mark_cnt_q <= (others => '0');
line_idle_o <= '0'; -- must be earned
elsif rising_edge(clk) then
if rx_sync_i = '0' then
mark_cnt_q <= (others => '0');
line_idle_o <= '0';
elsif os_tick_i = '1' then
if mark_cnt_q = to_unsigned(IDLE_TICKS, CNT_W) then
mark_cnt_q <= mark_cnt_q; -- saturate
else
mark_cnt_q <= mark_cnt_q + 1;
if mark_cnt_q = to_unsigned(IDLE_TICKS - 1, CNT_W) then
line_idle_o <= '1';
end if;
end if;
end if;
end if;
end process;
end architecture rtl;| Property | Verilog-2001 | SystemVerilog | VHDL-2008 | Agree? |
|---|---|---|---|---|
| counted unit | oversample ticks | same | same | yes |
| threshold | FRAME_BITS x OVERSAMPLE | same | same | yes |
line_idle_o after reset | 0 — not idle | 0 | 0 | yes |
| a low clears it | immediately, no tick needed | same | same | yes |
| re-qualification | full threshold again | same | same | yes |
| counter at the top | saturate | saturate | saturate | yes |
The reset value is the contract, and it is the one line a reviewer should
check first. line_idle_o is 0 out of reset in all three: idle is
earned by observation, never assumed. A receiver that came up believing the
line was idle would accept the next low interval as a start bit — and if it
had powered up in the middle of somebody else's character, that low interval
is a data bit, and the receiver frames the remainder of that character as
though it were a new one.
A low clears idle with no tick required. The oversample tick gates counting up, not giving up. Waiting for a tick to notice that the line has gone low would leave up to one tick of window in which a stale idle is still asserted, which is exactly the window a start-edge detector is looking at.
6. The Self-Checking Testbenches
INDEPENDENCE: the checker counts the mark ticks it drove and asserts idle
exactly at the threshold. It never reads mark_cnt_q.
Two of the fourteen checks are negative, and they are the valuable ones:
PASS reset: NOT idle -- idle must be earned
PASS a busy line never qualifies as idleThe second drives twenty cycles of alternating mark and space — what a live link actually looks like — and asserts that the qualifier never once declares idle. No positive test can catch a qualifier that is too permissive; only a negative one can.
//===========================================================================
// tb_uart_idle_qualify_v — self-checking Verilog-2001 testbench
//
// INDEPENDENCE: the checker counts the mark ticks IT DROVE and asserts idle
// exactly at FRAME_BITS x OVERSAMPLE. It never reads mark_cnt_q.
//
// The property that matters most is the NEGATIVE one: idle is not asserted
// out of reset. A receiver that assumes idle can mis-frame on the remainder
// of somebody else's character, and no positive test catches that.
//===========================================================================
`timescale 1ns/1ps
module tb_uart_idle_qualify_v;
reg clk = 1'b0, rst_n = 1'b0;
always #5 clk = ~clk;
reg os_tick = 1'b0, rx_sync = 1'b1;
wire a_idle, b_idle;
uart_idle_qualify_v #(.OVERSAMPLE(16), .FRAME_BITS(10)) dut_a (
.clk(clk), .rst_n(rst_n), .os_tick_i(os_tick), .rx_sync_i(rx_sync),
.line_idle_o(a_idle));
uart_idle_qualify_v #(.OVERSAMPLE(8), .FRAME_BITS(7)) dut_b (
.clk(clk), .rst_n(rst_n), .os_tick_i(os_tick), .rx_sync_i(rx_sync),
.line_idle_o(b_idle));
localparam A_TICKS = 160;
localparam B_TICKS = 56;
integer checks = 0, failures = 0;
task check;
input cond;
input [8*58-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 drive_ticks;
input integer n;
integer k;
begin
for (k = 0; k < n; k = k + 1) begin
@(negedge clk) os_tick = 1'b1;
@(negedge clk) os_tick = 1'b0;
@(negedge clk);
end
end
endtask
task settle; begin repeat (2) @(negedge clk); end endtask
initial begin
#4_000_000;
$display(" FAIL watchdog: simulation did not finish");
$display("== %0d checks, %0d failures ==", checks+1, failures+1);
$display(" RESULT: VERILOG IDLE-QUALIFY TESTS FAILED (timeout)");
$finish;
end
initial begin
$display("== uart_idle_qualify_v : self-checking Verilog testbench ==");
rst_n = 1'b0; rx_sync = 1'b1;
repeat (4) @(negedge clk);
check(a_idle === 1'b0, "reset: NOT idle -- idle must be earned");
@(negedge clk) rst_n = 1'b1;
@(negedge clk);
// ---- a high line is not yet a qualified idle line ----------------
drive_ticks(A_TICKS - 1); settle;
check(a_idle === 1'b0, "one tick short of a frame time: not idle");
drive_ticks(1); settle;
check(a_idle === 1'b1, "full frame time of mark: idle asserted");
// ---- any low clears it immediately, with no tick required --------
@(negedge clk) rx_sync = 1'b0;
settle;
check(a_idle === 1'b0, "a single low clears idle at once");
// ---- and it must be re-earned in full ----------------------------
@(negedge clk) rx_sync = 1'b1;
drive_ticks(A_TICKS - 1); settle;
check(a_idle === 1'b0, "re-qualification is not shortened");
drive_ticks(1); settle;
check(a_idle === 1'b1, "idle re-earned after a full frame time");
// ---- saturation: staying high does not wrap it back to not-idle --
drive_ticks(A_TICKS * 3); settle;
check(a_idle === 1'b1, "long idle: counter saturates, stays idle");
// ---- the threshold scales with the parameters --------------------
@(negedge clk) rx_sync = 1'b0; settle;
check(a_idle === 1'b0 && b_idle === 1'b0, "both instances cleared");
@(negedge clk) rx_sync = 1'b1;
drive_ticks(B_TICKS); settle;
check(b_idle === 1'b1, "8x/7-bit qualifies at 56 ticks");
check(a_idle === 1'b0, "16x/10-bit has NOT qualified yet");
drive_ticks(A_TICKS - B_TICKS); settle;
check(a_idle === 1'b1, "16x/10-bit qualifies at 160 ticks");
// ---- a busy line never qualifies ---------------------------------
// Alternating mark and space, as a live link looks, must never reach
// a full frame time of continuous mark.
@(negedge clk) rx_sync = 1'b0; settle;
begin : busy_line
integer j;
for (j = 0; j < 20; j = j + 1) begin
@(negedge clk) rx_sync = 1'b1;
drive_ticks(20);
@(negedge clk) rx_sync = 1'b0;
drive_ticks(4);
end
end
settle;
check(a_idle === 1'b0, "a busy line never qualifies as idle");
// ---- reset while qualified ---------------------------------------
@(negedge clk) rx_sync = 1'b1;
drive_ticks(A_TICKS); settle;
check(a_idle === 1'b1, "idle before reset");
@(negedge clk) rst_n = 1'b0;
repeat (2) @(negedge clk);
check(a_idle === 1'b0, "reset drops idle -- it must be re-earned");
@(negedge clk) rst_n = 1'b1;
@(negedge clk);
$display("== %0d checks, %0d failures ==", checks, failures);
if (failures == 0) $display(" RESULT: ALL VERILOG IDLE-QUALIFY TESTS PASSED");
else $display(" RESULT: VERILOG IDLE-QUALIFY TESTS FAILED");
$finish;
end
endmodule//===========================================================================
// tb_uart_idle_qualify_sv — self-checking Verilog-2001 testbench
//
// INDEPENDENCE: the checker counts the mark ticks IT DROVE and asserts idle
// exactly at FRAME_BITS x OVERSAMPLE. It never reads mark_cnt_q.
//
// The property that matters most is the NEGATIVE one: idle is not asserted
// out of reset. A receiver that assumes idle can mis-frame on the remainder
// of somebody else's character, and no positive test catches that.
//===========================================================================
`timescale 1ns/1ps
module tb_uart_idle_qualify_sv;
logic clk = 1'b0, rst_n = 1'b0;
always #5ns clk = ~clk;
logic os_tick = 1'b0, rx_sync = 1'b1;
logic a_idle, b_idle;
uart_idle_qualify #(.OVERSAMPLE(16), .FRAME_BITS(10)) dut_a (
.clk(clk), .rst_n(rst_n), .os_tick_i(os_tick), .rx_sync_i(rx_sync),
.line_idle_o(a_idle));
uart_idle_qualify #(.OVERSAMPLE(8), .FRAME_BITS(7)) dut_b (
.clk(clk), .rst_n(rst_n), .os_tick_i(os_tick), .rx_sync_i(rx_sync),
.line_idle_o(b_idle));
localparam A_TICKS = 160;
localparam B_TICKS = 56;
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 drive_ticks(input int n);
for (int k = 0; k < n; k++) begin
@(negedge clk) os_tick = 1'b1;
@(negedge clk) os_tick = 1'b0;
@(negedge clk);
end
endtask
task automatic settle(); repeat (2) @(negedge clk); endtask
initial begin
#4ms;
$display(" FAIL watchdog: simulation did not finish");
$display("== %0d checks, %0d failures ==", checks+1, failures+1);
$display(" RESULT: SYSTEMVERILOG IDLE-QUALIFY TESTS FAILED (timeout)");
$finish;
end
initial begin
$display("== uart_idle_qualify : self-checking Verilog testbench ==");
rst_n = 1'b0; rx_sync = 1'b1;
repeat (4) @(negedge clk);
check(a_idle === 1'b0, "reset: NOT idle -- idle must be earned");
@(negedge clk) rst_n = 1'b1;
@(negedge clk);
// ---- a high line is not yet a qualified idle line ----------------
drive_ticks(A_TICKS - 1); settle;
check(a_idle === 1'b0, "one tick short of a frame time: not idle");
drive_ticks(1); settle;
check(a_idle === 1'b1, "full frame time of mark: idle asserted");
// ---- any low clears it immediately, with no tick required --------
@(negedge clk) rx_sync = 1'b0;
settle();
check(a_idle === 1'b0, "a single low clears idle at once");
// ---- and it must be re-earned in full ----------------------------
@(negedge clk) rx_sync = 1'b1;
drive_ticks(A_TICKS - 1); settle;
check(a_idle === 1'b0, "re-qualification is not shortened");
drive_ticks(1); settle;
check(a_idle === 1'b1, "idle re-earned after a full frame time");
// ---- saturation: staying high does not wrap it back to not-idle --
drive_ticks(A_TICKS * 3); settle;
check(a_idle === 1'b1, "long idle: counter saturates, stays idle");
// ---- the threshold scales with the parameters --------------------
@(negedge clk) rx_sync = 1'b0; settle;
check(a_idle === 1'b0 && b_idle === 1'b0, "both instances cleared");
@(negedge clk) rx_sync = 1'b1;
drive_ticks(B_TICKS); settle;
check(b_idle === 1'b1, "8x/7-bit qualifies at 56 ticks");
check(a_idle === 1'b0, "16x/10-bit has NOT qualified yet");
drive_ticks(A_TICKS - B_TICKS); settle;
check(a_idle === 1'b1, "16x/10-bit qualifies at 160 ticks");
// ---- a busy line never qualifies ---------------------------------
// Alternating mark and space, as a live link looks, must never reach
// a full frame time of continuous mark.
@(negedge clk) rx_sync = 1'b0; settle;
begin : busy_line
int j;
for (j = 0; j < 20; j = j + 1) begin
@(negedge clk) rx_sync = 1'b1;
drive_ticks(20);
@(negedge clk) rx_sync = 1'b0;
drive_ticks(4);
end
end
settle();
check(a_idle === 1'b0, "a busy line never qualifies as idle");
// ---- reset while qualified ---------------------------------------
@(negedge clk) rx_sync = 1'b1;
drive_ticks(A_TICKS); settle;
check(a_idle === 1'b1, "idle before reset");
@(negedge clk) rst_n = 1'b0;
repeat (2) @(negedge clk);
check(a_idle === 1'b0, "reset drops idle -- it must be re-earned");
@(negedge clk) rst_n = 1'b1;
@(negedge clk);
$display("== %0d checks, %0d failures ==", checks, failures);
if (failures == 0) $display(" RESULT: ALL SYSTEMVERILOG IDLE-QUALIFY TESTS PASSED");
else $display(" RESULT: SYSTEMVERILOG IDLE-QUALIFY TESTS FAILED");
$finish;
end
endmodule--==========================================================================
-- tb_uart_idle_qualify -- self-checking VHDL-2008 testbench
-- Same 14 checks as the Verilog and SystemVerilog testbenches.
--==========================================================================
library ieee;
use ieee.std_logic_1164.all;
use std.env.finish;
entity tb_uart_idle_qualify is
end entity tb_uart_idle_qualify;
architecture sim of tb_uart_idle_qualify is
constant CLK_PERIOD : time := 10 ns;
constant A_TICKS : natural := 160;
constant B_TICKS : natural := 56;
signal clk, rst_n, done : std_logic := '0';
signal os_tick : std_logic := '0';
signal rx_sync : std_logic := '1';
signal a_idle, b_idle : std_logic;
begin
clk <= not clk after CLK_PERIOD / 2 when done = '0' else '0';
dut_a : entity work.uart_idle_qualify
generic map (OVERSAMPLE => 16, FRAME_BITS => 10)
port map (clk, rst_n, os_tick, rx_sync, a_idle);
dut_b : entity work.uart_idle_qualify
generic map (OVERSAMPLE => 8, FRAME_BITS => 7)
port map (clk, rst_n, os_tick, rx_sync, b_idle);
watchdog : process
begin
wait for 4 ms;
report "watchdog: simulation did not finish" severity error;
report " RESULT: VHDL IDLE-QUALIFY TESTS FAILED (timeout)" severity failure;
wait;
end process watchdog;
stim : process
variable checks, 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 drive_ticks(n : natural) is
begin
for k in 1 to n loop
wait until falling_edge(clk); os_tick <= '1';
wait until falling_edge(clk); os_tick <= '0';
wait until falling_edge(clk);
end loop;
end procedure;
procedure settle is
begin
for i in 0 to 1 loop wait until falling_edge(clk); end loop;
end procedure;
begin
report "== uart_idle_qualify (VHDL) : self-checking testbench ==";
rst_n <= '0'; rx_sync <= '1';
for i in 0 to 3 loop wait until falling_edge(clk); end loop;
check(a_idle = '0', "reset: NOT idle -- idle must be earned");
rst_n <= '1'; wait until falling_edge(clk);
drive_ticks(A_TICKS - 1); settle;
check(a_idle = '0', "one tick short of a frame time: not idle");
drive_ticks(1); settle;
check(a_idle = '1', "full frame time of mark: idle asserted");
wait until falling_edge(clk); rx_sync <= '0'; settle;
check(a_idle = '0', "a single low clears idle at once");
wait until falling_edge(clk); rx_sync <= '1';
drive_ticks(A_TICKS - 1); settle;
check(a_idle = '0', "re-qualification is not shortened");
drive_ticks(1); settle;
check(a_idle = '1', "idle re-earned after a full frame time");
drive_ticks(A_TICKS * 3); settle;
check(a_idle = '1', "long idle: counter saturates, stays idle");
wait until falling_edge(clk); rx_sync <= '0'; settle;
check(a_idle = '0' and b_idle = '0', "both instances cleared");
wait until falling_edge(clk); rx_sync <= '1';
drive_ticks(B_TICKS); settle;
check(b_idle = '1', "8x/7-bit qualifies at 56 ticks");
check(a_idle = '0', "16x/10-bit has NOT qualified yet");
drive_ticks(A_TICKS - B_TICKS); settle;
check(a_idle = '1', "16x/10-bit qualifies at 160 ticks");
wait until falling_edge(clk); rx_sync <= '0'; settle;
for j in 0 to 19 loop
wait until falling_edge(clk); rx_sync <= '1';
drive_ticks(20);
wait until falling_edge(clk); rx_sync <= '0';
drive_ticks(4);
end loop;
settle;
check(a_idle = '0', "a busy line never qualifies as idle");
wait until falling_edge(clk); rx_sync <= '1';
drive_ticks(A_TICKS); settle;
check(a_idle = '1', "idle before reset");
wait until falling_edge(clk); rst_n <= '0';
for i in 0 to 1 loop wait until falling_edge(clk); end loop;
check(a_idle = '0', "reset drops idle -- it must be re-earned");
wait until falling_edge(clk); rst_n <= '1';
wait until falling_edge(clk);
report "== " & integer'image(checks) & " checks, " &
integer'image(failures) & " failures ==";
if failures = 0 then report " RESULT: ALL VHDL IDLE-QUALIFY TESTS PASSED";
else report " RESULT: VHDL IDLE-QUALIFY TESTS FAILED" severity error; end if;
done <= '1'; finish;
end process stim;
end architecture sim;$ iverilog -g2001 -o sim uart_idle_qualify_v.v tb_uart_idle_qualify_v.v && ./sim
== uart_idle_qualify_v : self-checking Verilog testbench ==
PASS reset: NOT idle -- idle must be earned
PASS one tick short of a frame time: not idle
PASS full frame time of mark: idle asserted
PASS a single low clears idle at once
PASS re-qualification is not shortened
PASS long idle: counter saturates, stays idle
PASS 8x/7-bit qualifies at 56 ticks
PASS 16x/10-bit has NOT qualified yet
PASS a busy line never qualifies as idle
PASS reset drops idle -- it must be re-earned
== 14 checks, 0 failures ==
RESULT: ALL VERILOG IDLE-QUALIFY TESTS PASSED14 checks in each language, 0 failures.
Proving these checks can fail
| Mutation | What it breaks | Failures |
|---|---|---|
line_idle_o asserted out of reset | the earn-it contract | 3 / 14 |
Three checks fail and the other eleven pass, because a qualifier that starts in the idle state is right about everything else. It counts correctly, it clears correctly, it re-qualifies correctly. It is wrong only about the one moment nobody thinks to test — which is why the check for it is written first in the file rather than last.
7. What Idle Detection Costs
It is a real remedy with a real price, and adopting it without the price is how a fix becomes a new problem.
Reception latency after every quiet period. The first frame after any idle gap shorter than a full frame time is rejected, because the line has not yet proven itself idle. On a link that sends isolated bytes with short gaps, this can reject a substantial fraction of traffic — and the rejection is silent, which is worse than a reported error.
It does not help a line that is never idle. A continuously saturated link — back-to-back frames with no gaps, which Chapter 7.4 showed a transmitter producing deliberately — never presents a full frame time of mark. A receiver attaching to such a link with idle detection enabled receives nothing at all, indefinitely, which is a far worse failure than one fabricated byte.
It cannot distinguish a genuinely idle line from a disconnected one. Chapter 1.3 noted that idle and several faults are indistinguishable; this block declares idle for both.
| Without idle detection | With idle detection | |
|---|---|---|
| Mid-frame attach | one fabricated byte, then correct | correct from the first frame |
| Isolated bytes, short gaps | all received | some silently rejected |
| Saturated link, attach mid-stream | one fabricated byte, then correct | nothing received, ever |
| Hardware | none | a counter and a comparator |
So it is the right default for a link with natural gaps and the wrong default for a saturated one, which is why Module 6 left it out of the base receiver and named it here rather than adding it silently. A design should choose deliberately, and a design that cannot predict its traffic pattern should probably accept the one bad byte and let the layer above discard it.
8. Malformed Frames: What Is Observable
A frame that starts legitimately can still go wrong in several ways, and the useful question for each is not "what happened" — the receiver cannot know — but what can hardware observe, and what status is justified?
| Shape | What hardware observes | Justified status | Payload delivered? |
|---|---|---|---|
| Valid start, corrupted data | nothing unusual at all | none — unless parity is enabled and the corruption is odd-weight | yes, wrong |
| Valid start, parity fails | the parity relation does not hold | parity_error | yes, with the flag |
| Valid start, stop is space | the stop sample is not mark | framing_error | yes, with the flag |
| Line goes low and stays | the stop sample fails, then the break threshold is reached | framing_error then break | yes for the frame, then nothing |
| Wide glitch accepted as start | nothing distinguishable from a real frame | none guaranteed | yes, meaningless |
The first and last rows are the important ones, and they say the same thing from opposite directions: the receiver has no mechanism that reports "this frame is meaningless". It reports specific relation failures, and a frame can be entirely fabricated while satisfying every one of them.
The fourth row shows why two detectors are better than one. A line that goes low mid-frame and stays produces a framing error immediately and a break declaration one frame time later. The framing error alone is ambiguous; followed by a break, the interpretation is unambiguous — and this is why Chapter 9.4's detector runs as an observer rather than inside the FSM, so it keeps counting through a frame that is already failing.
9. Verification
Sweep glitch width across the qualification boundary. Narrower than M/2 must be rejected silently; wider must be accepted. The boundary is where a defect in the qualifier shows, and testing only "very short" and "a real frame" leaves it untouched.
Sweep glitch phase as well as width. A disturbance of fixed width lands differently depending on where it falls relative to the tick grid, so sweeping both finds the region rather than a single point — the same argument Chapter 5.4 §7 made about the voting window.
Test mid-frame attach explicitly. Release the receiver from reset while a frame is in flight and assert what actually happens — which, without idle detection, is a fabricated byte. Chapter 6.2 §7's simulation did exactly this and the expected result is not "no byte":
reset released mid-frame, no idle detection
-> one fabricated byte (0xFA from a frame carrying 0xA6), status clean
-> subsequent frames correctA testbench that asserts no byte appears will fail on a correct receiver. That is worth knowing before spending a day on it.
Then test the same scenario with idle detection enabled and assert the byte is suppressed — and, separately, that a saturated link with idle detection receives nothing, because that is the cost and it should be visible in the suite rather than discovered in the field.
// Assertion — idle is never claimed while the line is low.
property p_idle_implies_mark;
@(posedge clk) disable iff (!rst_n)
line_idle_o |-> rx_sync_i;
endproperty
assert property (p_idle_implies_mark);
// Assertion — idle cannot be claimed before a full frame time of mark.
property p_no_early_idle;
@(posedge clk) disable iff (!rst_n)
$rose(line_idle_o) |-> ($past(mark_cnt_q) == CNT_W'(IDLE_TICKS - 1));
endproperty
assert property (p_no_early_idle);
// Assertion — the counter saturates, matching Chapter 9.4's policy.
property p_idle_counter_saturates;
@(posedge clk) disable iff (!rst_n)
mark_cnt_q <= CNT_W'(IDLE_TICKS);
endproperty
assert property (p_idle_counter_saturates);
// Assertion — with idle gating fitted, no start is accepted from a line
// that has not proven itself idle. This is the block's entire purpose.
property p_no_start_without_idle;
@(posedge clk) disable iff (!rst_n)
start_accept_o |-> $past(line_idle_o);
endproperty
assert property (p_no_start_without_idle);10. Debugging
11. What This Means on an FPGA
A floating receive input is the most common source of fabricated frames, and it is a board-level fix rather than an RTL one. An unconnected pin with no pull-up will produce edges, some of them wide enough to qualify, and the receiver will faithfully deliver bytes assembled from noise. A pull-up to the idle level costs one resistor and removes the entire failure class.
Probe start_cand alongside start_accept. The ratio between them is the qualifier's rejection rate, and it is a direct measure of line quality that no error flag provides. A board where candidates vastly outnumber acceptances is telling you something before any data is corrupted.
Idle detection is one counter, the same size as the break detector's, and the two can share nothing because they count opposite levels — but they can share the derivation and the review.
If both are fitted, expect them to be mutually exclusive. line_idle_o and break_active_o can never be true together, since one requires a full frame of mark and the other a full frame of space. An assertion to that effect is cheap and catches a polarity error in either block.
12. Understanding Check
13. Summary
Four start-side failures, rejected by four different mechanisms — or by none. A narrow glitch is rejected by qualification. A wide disturbance is rejected by nothing and produces a fabricated frame timed from noise. Mid-sample noise is Chapter 5.4's trade. Mid-frame attach is rejected only by idle detection.
Oversampling narrows the damaging class; it does not eliminate it. Qualification rejects what is narrower than the validation delay — 4.34 µs at 115,200 baud with M/2. Voting rejects what is narrower than one tick — 542.5 ns at 16×. A wide, well-placed disturbance is untouched by both.
A rejected candidate is not an error and should not be reported as one; as a debug counter it is a sensitive indicator of line quality.
Mid-frame attach produces a complete, clean-looking, entirely fabricated byte — 0xFA from a frame carrying 0xA6 in Module 6's simulation. Nothing in a UART frame identifies itself as a continuation, so the receiver cannot know.
Idle detection closes it: require a full frame time of continuous mark before qualifying any start, since a valid frame cannot hold mark that long. line_idle_o resets to 0 — idle is earned, not assumed.
And it costs. The first frame after any short gap is silently rejected, and on a saturated link the receiver gets nothing at all, indefinitely — a worse failure than the byte it was fitted to prevent. Right default for links with gaps; wrong for links without.
No status means "this frame is meaningless." The receiver reports relation failures, and a fabricated frame can satisfy every relation — which is the argument for framing above the byte layer.
14. What Comes Next
Five chapters have produced events: framing, parity, overrun, break, and the start-side failures that produce no event at all. None of them has said what the receiver should do.
Chapter 9.6 closes the module with that. It separates transient events from persistent state, builds the sticky flags with an explicit collision policy — what happens when software clears on the same cycle a new error arrives — distinguishes per-character status from historical status, and builds the re-synchronisation that returns the receiver to a trustworthy frame boundary without turning one bad frame into many.
Browse the full path on the UART tutorials index. For the qualification mechanism this chapter attacks, read back to Chapter 5.2; for the fabricated byte it explains, Chapter 6.2.
Continue learning
Related tutorials
- Related topic
Framing Errors
A framing error is one sampled bit at one instant. It proves the line was not at mark where the receiver expected mark — and nothing about why, which is what makes the diagnosis interesting.
- Related topic
Parity Errors and the Limits of Detection
Parity detects every odd-weight corruption and provably misses every even-weight one — shown by exhaustive enumeration. A check that passes says a relation holds, not that the data is correct.
- Related topic
Overrun, Underrun and Data Loss
Overrun is a protocol-level consequence of a full holding register; transmit starvation is an architecture-dependent system event. Both lose information an error flag records but cannot recover.
- Related topic
Break Conditions: Generation and Detection
A start bit is also low, so a break cannot be detected by looking at the line. It is the one UART condition defined by duration — which means a counter, a derived threshold, and a saturation policy.
Where this fits
Part of the UART curriculum.
