Skip to content
VLSI Mentor

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
    A comparison of two events on a serial line, both beginning with a falling edge. The upper case is a genuine start bit, which holds the line at space for a full bit period. Three sample points taken around the middle of that interval all read space, so the vote is three to zero in favour and the receiver commits to the frame. The lower case is a narrow spike that returns to mark after a quarter of a bit period. The same three sample points all read mark, so the vote is zero to three and the candidate is discarded, returning the detector to idle immediately rather than occupying it for a whole frame.majority window — 2 of 3 SPACEmajority window — 2 of 3 SPACEvote sample 1vote sample 1sample 2sample 2sample 3sample 3tick01234567891011real startspikesampled?t0t1t2t3t4t5t6t7t8t9t10t11

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

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

endmodule

SystemVerilog

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

endmodule

VHDL

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

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

The 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

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

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

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

Without 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

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

endmodule

SystemVerilog

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

endmodule

VHDL

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

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

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

  1. 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.
  2. 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.
  3. 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.
  4. Do frames start but not finish? That is not a start-bit problem. It is sampling drift, and Chapter 17.2 owns it.
  5. 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

Where this fits

Part of the UART curriculum.