Skip to content
VLSI Mentor

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.

FailureShape on the wireRejected byCost when it gets through
Narrow glitchlow for less than the validation delayqualification — 5.2none; it never becomes a frame
Wide disturbancelow past the validation pointnothinga fabricated frame, timed from the glitch
Mid-sample noisetransient at one sampling instantmajority voting, if fitted — 5.4one wrong bit, possibly caught by parity
Mid-frame attachthe line is mid-frame when we start lookingidle detection — §4a 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:

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

The 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 cycles
A comparison across sixteen oversample ticks of two disturbances on a receive line. Both begin with a mark-to-space transition at tick zero which the edge detector reports as a start candidate. The first disturbance returns to the mark level after four ticks, so when qualification samples the line at tick eight it finds mark and abandons the candidate with no frame begun and nothing reported. The second disturbance persists to tick twelve, so at tick eight qualification finds the line still at space and accepts it as a genuine start bit, after which the receiver schedules an entire frame of samples timed from a disturbance.candidate — bothcandidate — bothVALIDATE: narrow=mark reject / wide=space ACCEPTVALIDATE: narrow=markreject / wide=space ACCEPTos tick0123456789101112131415narrowwidet0t1t2t3t4t5t6t7t8t9t10t11t12t13t14t15
Figure 1 — the qualification boundary. Columns are OVERSAMPLE TICKS at M = 16; validation occurs at tick 8, half a bit interval after the candidate. The two disturbances differ only in width. The narrow one has ended by the validation point and is discarded silently; the wider one is still at space there, so it is accepted as a start and the receiver times an entire frame from it.

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 cycles
A trace of thirteen bit intervals of ongoing UART traffic. The first frame is already in progress when the receiver begins observing, indicated by an attach marker partway through it. The receiver sees the next mark-to-space transition and treats it as a start bit, but that transition is a data edge inside the frame already in flight. The receiver then samples eight intervals from that false origin, spanning the remainder of the first frame and the beginning of the next, and assembles a byte from intervals that were never a single transmitted character. The resulting byte is delivered, and the stop check may pass if the line happens to be at mark where that sample lands.receiver attaches herereceiver attaches heredata edge read as a STARTdata edge read as a STARTfabricated byte deliveredfabricated byte deliveredreal framed4d5d6d7STOPSTd0d1d2d3d4d5d6rx_iRX thinksSTd0d1d2d3d4d5d6d7STOPSTOPt0t1t2t3t4t5t6t7t8t9t10t11t12
Figure 2 — mid-frame attach. Columns are BIT INTERVALS of traffic already in progress. The receiver begins observing partway through a frame and latches the next mark-to-space transition — which is a data edge, not a start bit. It then samples eight intervals from that origin, spanning the tail of one frame and the head of the next, and delivers a byte that was never transmitted.

The RTL

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

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

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

The measured behaviour

StimulusExpectedResult
line lownot idlepass
159 mark ticks (one short of a frame)not idlepass
160 mark ticks (a full frame)idlepass
counter after thresholdsaturates at 160pass
a single low sampleidle cleared immediatelypass

5. The Same Qualifier in Three Languages

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
//===========================================================================
//  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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
--==========================================================================
--  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;
PropertyVerilog-2001SystemVerilogVHDL-2008Agree?
counted unitoversample tickssamesameyes
thresholdFRAME_BITS x OVERSAMPLEsamesameyes
line_idle_o after reset0 — not idle00yes
a low clears itimmediately, no tick neededsamesameyes
re-qualificationfull threshold againsamesameyes
counter at the topsaturatesaturatesaturateyes

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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  PASS reset: NOT idle -- idle must be earned
  PASS a busy line never qualifies as idle

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

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

14 checks in each language, 0 failures.

Proving these checks can fail

MutationWhat it breaksFailures
line_idle_o asserted out of resetthe earn-it contract3 / 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 detectionWith idle detection
Mid-frame attachone fabricated byte, then correctcorrect from the first frame
Isolated bytes, short gapsall receivedsome silently rejected
Saturated link, attach mid-streamone fabricated byte, then correctnothing received, ever
Hardwarenonea 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?

ShapeWhat hardware observesJustified statusPayload delivered?
Valid start, corrupted datanothing unusual at allnone — unless parity is enabled and the corruption is odd-weightyes, wrong
Valid start, parity failsthe parity relation does not holdparity_erroryes, with the flag
Valid start, stop is spacethe stop sample is not markframing_erroryes, with the flag
Line goes low and staysthe stop sample fails, then the break threshold is reachedframing_error then breakyes for the frame, then nothing
Wide glitch accepted as startnothing distinguishable from a real framenone guaranteedyes, 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":

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
reset released mid-frame, no idle detection
-> one fabricated byte (0xFA from a frame carrying 0xA6), status clean
-> subsequent frames correct

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

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

Where this fits

Part of the UART curriculum.