Skip to content
VLSI Mentor

UART · Module 17

Reading a UART Waveform as Evidence

Separating what a capture observes from what it implies, the run-length structure that makes a bit period measurable, a measuring block in three HDLs, and the ordinary payload that makes it report nine times the right answer.

A capture is not a diagnosis. It is a record of what two levels did over time, and everything beyond that — "the baud rate is wrong", "the byte is reversed", "the receiver is deaf" — is inference layered on top. Debugging goes badly when the layers get confused, because a theory formed early is remarkably good at surviving evidence that contradicts it.

This chapter builds the smallest useful instrument in the debug toolkit: something that reads a capture and tells you the bit period. It runs, in all three HDLs, and it gets the answer wrong by a factor of nine on one perfectly ordinary payload. Understanding exactly when it lies is worth more than the measurement itself.

1. Three Claims, in Order of Strength

Hold a capture in front of you and you can make three different kinds of statement. They are not equally safe.

ClaimKindWhat backs it
"The line was SPACE for 72 clocks starting here"Observationthe capture itself
"Therefore the bit period is 72 clocks"Inferencea model of how frames are built
"Therefore the transmitter's divider is misconfigured"Diagnosisinference plus assumptions about the system

Only the first is free. The second requires that the capture actually contained an isolated single-bit interval — and §4 shows a common payload where it did not. The third requires that nothing else could produce the same waveform, which is almost never established.

The practical discipline is to write down the observation separately from the conclusion. When the conclusion turns out to be wrong — and on a hard bug it usually does, at least once — the observations survive and can be re-interpreted. Conclusions recorded as if they were observations have to be re-measured from scratch.

2. A Capture Is a Sequence of Runs

A serial line spends its life at one of two levels. The only information in a capture is therefore where the transitions are, and the useful decomposition is into runs: maximal intervals during which the level does not change.

Every run in a well-formed UART capture is an integer number of bit periods long. That single fact is what makes measurement possible: the bit period must divide every run length, so it is a common divisor of all of them — and if any run happens to be exactly one bit long, it is the bit period.

The run structure of one 8N1 frame carrying 0x55

15 cycles
A timing trace of a single eight-bit frame carrying the value fifty-five hexadecimal, drawn to show its run structure. The line begins idle at mark for three bit intervals, forming one long opening run. It then falls for the start bit. Because the payload fifty-five alternates between one and zero, and is sent least significant bit first, every data bit differs from its neighbour, so each data bit forms its own run exactly one bit period long. The stop bit returns to mark and merges with the following idle interval into a second long run. The row labelled run length shows the sequence: a long opening run, then nine consecutive runs of exactly one bit each, then a long closing run.opening fragment — unknowableopening fragment — unknowablenine complete runsnine complete runsclosing fragment — unterminatedclosing fragment — unterminatedfirst transitionfirst transitionlast transitionlast transitionintervalidleidleidlestartd0d1d2d3d4d5d6d7stopidleidlerx linerun len333111111111333t0t1t2t3t4t5t6t7t8t9t10t11t12t13t14

Two runs in that picture are special, and both are excluded from the measurement.

The opening run began before the capture window opened. Its true length is unknown — the line may have been idle for a microsecond or an hour — so counting it would be counting a fragment. The closing run has not ended yet; the capture stopped first. Only runs bounded by two observed transitions have a length that the capture actually establishes.

That is not a subtlety to be tidied away in an implementation. It is the difference between an instrument that reports a bit period and one that reports a number smaller than the bit period whenever the capture happens to start mid-bit.

3. The Instrument

The block below watches a line and maintains three numbers: the shortest completed run, the longest, and how many completed runs it has seen. The third is the one people forget, and it is the one that makes the other two trustworthy.

Verilog

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------------
// uart_bit_period_meas -- measure the bit period of a captured serial line.
//
// A capture is a sequence of runs: intervals during which the line holds one
// level. Every run is an integer number of bit periods long, so the SHORTEST
// completed run is the bit period -- provided the capture contains at least
// one isolated single-bit run. That proviso is the whole lesson: this block
// reports what it measured AND how much evidence it had, so the reader can
// decide whether to believe it.
//
// The first run is deliberately excluded. Enable can be asserted part-way
// through a level, so the opening run is a fragment of unknown length; counting
// it would drag min_run_o below the true bit period. Only runs bounded by two
// observed transitions are complete.
// ---------------------------------------------------------------------------
module uart_bit_period_meas #(
    parameter W = 16                 // width of the run counters, in clocks
)(
    input  wire          clk,
    input  wire          rst_n,
    input  wire          en_i,       // capture window is open
    input  wire          line_i,     // the captured serial line
    output reg  [W-1:0]  min_run_o,  // shortest COMPLETED run, in clocks
    output reg  [W-1:0]  max_run_o,  // longest  COMPLETED run, in clocks
    output reg  [W-1:0]  n_runs_o,   // how many complete runs were measured
    output reg           valid_o     // at least one complete run was measured
);

    localparam [W-1:0] RUN_MAX = {W{1'b1}};

    reg           line_q;   // the line, one clock ago
    reg           armed;    // a first transition has been seen
    reg  [W-1:0]  run;      // clocks spent at the current level

    wire edge_now = en_i && (line_i != line_q);

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            line_q    <= 1'b1;        // idle line is MARK
            armed     <= 1'b0;
            run       <= {W{1'b0}};
            min_run_o <= RUN_MAX;
            max_run_o <= {W{1'b0}};
            n_runs_o  <= {W{1'b0}};
            valid_o   <= 1'b0;
        end else if (en_i) begin
            line_q <= line_i;

            if (edge_now) begin
                // The run that just ended is complete only if it STARTED at an
                // observed transition too -- i.e. only once we are armed.
                if (armed) begin
                    if (run < min_run_o) min_run_o <= run;
                    if (run > max_run_o) max_run_o <= run;
                    n_runs_o <= n_runs_o + 1'b1;
                    valid_o  <= 1'b1;
                end
                armed <= 1'b1;
                run   <= {{(W-1){1'b0}}, 1'b1};   // this clock belongs to the new run
            end else begin
                if (run != RUN_MAX) run <= run + 1'b1;   // saturate, never wrap
            end
        end
    end

endmodule

SystemVerilog

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------------
// uart_bit_period_meas -- measure the bit period of a captured serial line.
//
// A capture is a sequence of runs: intervals during which the line holds one
// level. Every run is an integer number of bit periods long, so the SHORTEST
// completed run is the bit period -- provided the capture contains at least
// one isolated single-bit run. That proviso is the whole lesson: this block
// reports what it measured AND how much evidence it had, so the reader can
// decide whether to believe it.
//
// The first run is deliberately excluded. Enable can be asserted part-way
// through a level, so the opening run is a fragment of unknown length; counting
// it would drag min_run_o below the true bit period. Only runs bounded by two
// observed transitions are complete.
// ---------------------------------------------------------------------------
module uart_bit_period_meas #(
    parameter int W = 16             // width of the run counters, in clocks
)(
    input  logic          clk,
    input  logic          rst_n,
    input  logic          en_i,      // capture window is open
    input  logic          line_i,    // the captured serial line
    output logic [W-1:0]  min_run_o, // shortest COMPLETED run, in clocks
    output logic [W-1:0]  max_run_o, // longest  COMPLETED run, in clocks
    output logic [W-1:0]  n_runs_o,  // how many complete runs were measured
    output logic          valid_o    // at least one complete run was measured
);

    localparam logic [W-1:0] RUN_MAX = '1;

    logic          line_q;   // the line, one clock ago
    logic          armed;    // a first transition has been seen
    logic [W-1:0]  run;      // clocks spent at the current level

    wire edge_now = en_i && (line_i != line_q);

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            line_q    <= 1'b1;        // idle line is MARK
            armed     <= 1'b0;
            run       <= '0;
            min_run_o <= RUN_MAX;
            max_run_o <= '0;
            n_runs_o  <= '0;
            valid_o   <= 1'b0;
        end else if (en_i) begin
            line_q <= line_i;

            if (edge_now) begin
                // The run that just ended is complete only if it STARTED at an
                // observed transition too -- i.e. only once we are armed.
                if (armed) begin
                    if (run < min_run_o) min_run_o <= run;
                    if (run > max_run_o) max_run_o <= run;
                    n_runs_o <= n_runs_o + 1'b1;
                    valid_o  <= 1'b1;
                end
                armed <= 1'b1;
                run   <= W'(1);       // this clock belongs to the new run
            end else begin
                if (run != RUN_MAX) run <= run + 1'b1;   // saturate, never wrap
            end
        end
    end

endmodule

VHDL

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- ---------------------------------------------------------------------------
-- uart_bit_period_meas -- measure the bit period of a captured serial line.
--
-- A capture is a sequence of runs: intervals during which the line holds one
-- level. Every run is an integer number of bit periods long, so the SHORTEST
-- completed run is the bit period -- provided the capture contains at least
-- one isolated single-bit run. That proviso is the whole lesson: this block
-- reports what it measured AND how much evidence it had, so the reader can
-- decide whether to believe it.
--
-- The first run is deliberately excluded. Enable can be asserted part-way
-- through a level, so the opening run is a fragment of unknown length; counting
-- it would drag min_run_o below the true bit period. Only runs bounded by two
-- observed transitions are complete.
-- ---------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity uart_bit_period_meas is
    generic (
        W : natural := 16                      -- run-counter width, in clocks
    );
    port (
        clk       : in  std_logic;
        rst_n     : in  std_logic;
        en_i      : in  std_logic;             -- capture window is open
        line_i    : in  std_logic;             -- the captured serial line
        min_run_o : out unsigned(W-1 downto 0);-- shortest COMPLETED run
        max_run_o : out unsigned(W-1 downto 0);-- longest  COMPLETED run
        n_runs_o  : out unsigned(W-1 downto 0);-- how many complete runs
        valid_o   : out std_logic              -- at least one complete run
    );
end entity uart_bit_period_meas;

architecture rtl of uart_bit_period_meas is

    constant RUN_MAX : unsigned(W-1 downto 0) := (others => '1');

    -- An entity may not read its own outputs, so every output is mirrored in
    -- an internal signal and driven out by a concurrent assignment.
    signal line_q  : std_logic := '1';         -- the line, one clock ago
    signal armed   : std_logic := '0';         -- a first transition was seen
    signal run     : unsigned(W-1 downto 0) := (others => '0');
    signal min_r   : unsigned(W-1 downto 0) := RUN_MAX;
    signal max_r   : unsigned(W-1 downto 0) := (others => '0');
    signal n_r     : unsigned(W-1 downto 0) := (others => '0');
    signal valid_r : std_logic := '0';

begin

    min_run_o <= min_r;
    max_run_o <= max_r;
    n_runs_o  <= n_r;
    valid_o   <= valid_r;

    process (clk, rst_n)
    begin
        if rst_n = '0' then
            line_q  <= '1';                    -- idle line is MARK
            armed   <= '0';
            run     <= (others => '0');
            min_r   <= RUN_MAX;
            max_r   <= (others => '0');
            n_r     <= (others => '0');
            valid_r <= '0';
        elsif rising_edge(clk) then
            if en_i = '1' then
                line_q <= line_i;

                if line_i /= line_q then
                    -- The run that just ended is complete only if it STARTED at
                    -- an observed transition too -- i.e. only once armed.
                    if armed = '1' then
                        if run < min_r then min_r <= run; end if;
                        if run > max_r then max_r <= run; end if;
                        n_r     <= n_r + 1;
                        valid_r <= '1';
                    end if;
                    armed <= '1';
                    run   <= to_unsigned(1, W);  -- this clock starts the new run
                else
                    if run /= RUN_MAX then
                        run <= run + 1;          -- saturate, never wrap
                    end if;
                end if;
            end if;
        end if;
    end process;

end architecture rtl;

The saturation on the run counter is the other detail worth a moment. A capture containing a long break interval can produce a run longer than the counter, and a counter that wraps reports a small number — which would then be latched as the minimum, producing not just a wrong answer but a wrong answer in the most misleading possible direction. Saturating is boring and correct.

4. What the Instrument Measured

Three payloads, each sent as one 8N1 frame with a bit period of exactly 8 clocks, into the block above:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  payload   min_run   max_run   n_runs   verdict
  -------   -------   -------   ------   -----------------------------
  0x55            8         8        9   correct, with strong evidence
  0xFF            8         8        1   correct, with weak evidence
  0x00           72        72        1   WRONG by a factor of nine

The 0x00 row is the one to sit with.

0x00 is sent as a start bit followed by eight zero data bits. All nine are SPACE, so they form one single run of nine bit periods, and the stop bit ends it. There is exactly one completed run in the whole capture, and its length is 72 clocks. The instrument reports a bit period of 72. It is not malfunctioning; it is correctly reporting the shortest completed run in a capture that never contained a one-bit run.

0x00 — nine bit intervals that merge into a single run

14 cycles
A timing trace of an eight-bit frame carrying the value zero. The line is idle at mark, then falls for the start bit. Because every data bit is zero, the line stays at space through the start bit and all eight data bits without a single transition, forming one continuous run nine bit periods long. Only at the stop bit does the line return to mark. The capture therefore contains exactly one completed run, of nine bit periods, and a measuring instrument that reports the shortest completed run will report nine bit periods as if it were one.one run, nine bits longone run, nine bits longfirst transitionfirst transitiononly transition that closes a runonly transition that closesa runintervalidleidlestartd0d1d2d3d4d5d6d7stopidleidlerx lineruns seen00000000000111t0t1t2t3t4t5t6t7t8t9t10t11t12t13

Now compare it with 0xFF, which also produced n_runs = 1. There the single completed run is the start bit alone — one bit period — because all eight data bits are MARK and merge with the stop bit instead. The answer is right.

So two captures with identical confidence signals gave one right answer and one wrong one. That looks like a defeat for n_runs_o, and it is worth being precise about what it actually tells you:

This generalises past this one block. An instrument that reports only its answer invites you to trust it uniformly. An instrument that reports how much evidence the answer rests on lets you notice when the stimulus, not the hardware, is the thing that needs fixing.

5. Decoding a Frame by Hand

Once the bit period is established, decoding is mechanical — and doing it by hand once is worth more than any amount of reading, because it fixes the sampling convention in memory.

Take a capture with a measured bit period of 8 clocks and a falling edge at clock 40:

  1. The start bit occupies clocks 40 to 47. Sample its middle, clock 44, and confirm SPACE. If it is MARK, this was not a frame — see Chapter 17.4.
  2. Data bit k occupies clocks 40 + 8(k+1) to 40 + 8(k+2) - 1. Sample the middle: 44 + 8(k+1). So d0 at 52, d1 at 60, d2 at 68, and so on to d7 at 108.
  3. Assemble LSB first. The first data bit off the wire is bit 0 of the byte. This is the step people get backwards, and Chapter 17.3 is about the failure it produces.
  4. The stop bit is sampled at 44 + 8 * 9 = 116 and must be MARK. If it is SPACE, either the frame is malformed or the bit period is wrong — and those are distinguishable, because a wrong period corrupts the late data bits too.

Sampling points on a frame carrying 0x3C, LSB first

11 cycles
A timing trace showing where a receiver samples each bit of a frame carrying three C hexadecimal. The line falls for the start bit, which is sampled at its midpoint and confirmed to be space. Eight data bits follow. Because the value three C is zero zero one one one one zero zero in binary and is transmitted least significant bit first, the wire carries zero, zero, one, one, one, one, zero, zero in that order. Each data bit is sampled at its own midpoint. The sample row marks the instant within each interval at which the receiver latches the level. The stop bit returns to mark and is sampled last.eight data bits, LSB firsteight data bits, LSB firststart confirmed SPACEstart confirmed SPACEd0 is bit 0d0 is bit 0stop must be MARKstop must be MARKintervalidlestartd0d1d2d3d4d5d6d7stoprx linesampledbyte so far....0000040C1C3C3C3C3Ct0t1t2t3t4t5t6t7t8t9t10

The byte so far row is the assembly in progress: each sampled bit is shifted in from the top, so after d5 the register already holds 0x3C and the two remaining zeros leave it unchanged. That is why a frame whose high bits are corrupted can still produce a plausible byte — the corruption does not announce itself.

6. The Testbench, and Why Its Oracle Is Not a Second Counter

The obvious way to test a run-length counter is to write another run-length counter and compare. That proves the two agree, which is a much weaker statement than it looks: a shared misunderstanding of where a run boundary falls would be invisible.

So the oracle here works in a different unit. The testbench builds each capture as an array of bit-times, walks that array in software to derive the expected run statistics, and multiplies by the bit period only at the very end. The design under test counts clocks. The two agree only if the block's notion of a run boundary matches the stimulus that was actually driven.

Verilog

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
`timescale 1ns/1ps
// ---------------------------------------------------------------------------
// Testbench for uart_bit_period_meas.
//
// The oracle is deliberately NOT a second run-length counter. The testbench
// builds the capture as a vector of bit-times, then derives the expected
// run statistics by walking that vector in software, in units of BIT-TIMES.
// The DUT counts CLOCKS. The two only agree if the DUT's notion of a run
// boundary matches the stimulus, which is the property under test.
// ---------------------------------------------------------------------------
module tb_uart_bit_period_meas;

    localparam BIT = 8;          // clocks per bit-time
    localparam W   = 16;

    reg         clk = 1'b0;
    reg         rst_n = 1'b0;
    reg         en = 1'b0;
    reg         line = 1'b1;

    wire [W-1:0] min_run, max_run, n_runs;
    wire         valid;

    integer checks = 0;
    integer fails  = 0;

    always #5 clk = ~clk;

    uart_bit_period_meas #(.W(W)) dut (
        .clk(clk), .rst_n(rst_n), .en_i(en), .line_i(line),
        .min_run_o(min_run), .max_run_o(max_run),
        .n_runs_o(n_runs), .valid_o(valid)
    );

    // ---- narrow instance, used only to prove the counter saturates ----
    reg          en6 = 1'b0;
    wire [5:0]   min6, max6, n6;
    wire         valid6;
    uart_bit_period_meas #(.W(6)) dut6 (
        .clk(clk), .rst_n(rst_n), .en_i(en6), .line_i(line),
        .min_run_o(min6), .max_run_o(max6),
        .n_runs_o(n6), .valid_o(valid6)
    );

    // ---- the capture, as one entry per bit-time ----
    reg     seq [0:255];
    integer seq_len;

    // ---- oracle results, in CLOCKS ----
    integer o_min, o_max, o_n;

    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

    // Build an 8N1 frame into seq[], LSB first, with `lead` idle bit-times
    // in front and `trail` idle bit-times behind.
    task build_frame;
        input [7:0]  data;
        input integer lead;
        input integer trail;
        integer i;
        begin
            seq_len = 0;
            for (i = 0; i < lead; i = i + 1)  begin seq[seq_len] = 1'b1; seq_len = seq_len + 1; end
            seq[seq_len] = 1'b0; seq_len = seq_len + 1;               // start
            for (i = 0; i < 8; i = i + 1) begin
                seq[seq_len] = data[i]; seq_len = seq_len + 1;        // LSB first
            end
            seq[seq_len] = 1'b1; seq_len = seq_len + 1;               // stop
            for (i = 0; i < trail; i = i + 1) begin seq[seq_len] = 1'b1; seq_len = seq_len + 1; end
        end
    endtask

    // Walk seq[] and derive expected min/max/count of COMPLETE runs -- runs
    // bounded by two transitions. Lengths are converted to clocks at the end.
    task run_oracle;
        integer i, first_t, last_t, prev_t, len;
        begin
            o_min = 0; o_max = 0; o_n = 0;
            first_t = -1; prev_t = -1;
            for (i = 1; i < seq_len; i = i + 1) begin
                if (seq[i] !== seq[i-1]) begin
                    if (first_t == -1) begin
                        first_t = i;
                    end else begin
                        len = i - prev_t;
                        if (o_n == 0) begin o_min = len; o_max = len; end
                        else begin
                            if (len < o_min) o_min = len;
                            if (len > o_max) o_max = len;
                        end
                        o_n = o_n + 1;
                    end
                    prev_t = i;
                end
            end
            last_t = prev_t;
            o_min = o_min * BIT;
            o_max = o_max * BIT;
        end
    endtask

    task drive_seq;
        integer i, j;
        begin
            for (i = 0; i < seq_len; i = i + 1) begin
                @(negedge clk);
                line = seq[i];
                for (j = 0; j < BIT; j = j + 1) @(posedge clk);
            end
        end
    endtask

    task do_reset;
        begin
            en = 1'b0; en6 = 1'b0; line = 1'b1;
            rst_n = 1'b0;
            repeat (3) @(posedge clk);
            @(negedge clk); rst_n = 1'b1;
            repeat (2) @(posedge clk);
        end
    endtask

    integer i;

    initial begin
        // ---------------- T1: alternating payload, rich evidence -----------
        do_reset;
        build_frame(8'h55, 3, 3);
        run_oracle;
        @(negedge clk); en = 1'b1;
        drive_seq;
        @(posedge clk); #1;
        $display("T1 0x55: min=%0d max=%0d n=%0d valid=%0b (oracle min=%0d max=%0d n=%0d)",
                 min_run, max_run, n_runs, valid, o_min, o_max, o_n);
        chk("T1 min", min_run, o_min);
        chk("T1 max", max_run, o_max);
        chk("T1 n",   n_runs,  o_n);
        chk("T1 valid", valid, 1);
        chk("T1 min is one bit", min_run, BIT);

        // ---------------- T2: 0x00 -- every bit merges into the start ------
        do_reset;
        build_frame(8'h00, 3, 3);
        run_oracle;
        @(negedge clk); en = 1'b1;
        drive_seq;
        @(posedge clk); #1;
        $display("T2 0x00: min=%0d max=%0d n=%0d (oracle min=%0d n=%0d)",
                 min_run, max_run, n_runs, o_min, o_n);
        chk("T2 min", min_run, o_min);
        chk("T2 n",   n_runs,  o_n);
        chk("T2 only one complete run", n_runs, 1);
        chk("T2 measured period is 9 bits, NOT 1", min_run, 9*BIT);

        // ---------------- T3: 0xFF -- one run, and it is correct -----------
        do_reset;
        build_frame(8'hFF, 3, 3);
        run_oracle;
        @(negedge clk); en = 1'b1;
        drive_seq;
        @(posedge clk); #1;
        $display("T3 0xFF: min=%0d n=%0d (oracle min=%0d n=%0d)", min_run, n_runs, o_min, o_n);
        chk("T3 min", min_run, o_min);
        chk("T3 n",   n_runs,  o_n);
        chk("T3 only one complete run", n_runs, 1);
        chk("T3 measured period is correct", min_run, BIT);

        // ---------------- T4: enable opens mid-run -------------------------
        // The line is already SPACE when the capture window opens. The opening
        // fragment must not be counted, or min_run would fall below one bit.
        do_reset;
        @(negedge clk); line = 1'b0;
        repeat (3) @(posedge clk);          // 3 clocks of an unmeasurable fragment
        @(negedge clk); en = 1'b1;
        repeat (2) @(posedge clk);          // 2 more clocks inside that fragment
        build_frame(8'h55, 0, 3);
        run_oracle;
        drive_seq;
        @(posedge clk); #1;
        $display("T4 mid-run enable: min=%0d n=%0d", min_run, n_runs);
        chk("T4 fragment not counted -- min is a full bit", min_run, BIT);

        // ---------------- T5: nothing measured yet -------------------------
        do_reset;
        @(negedge clk); en = 1'b1;
        repeat (40) @(posedge clk);         // idle only: no transition at all
        #1;
        chk("T5 valid low with no edges", valid, 0);
        chk("T5 no runs with no edges",   n_runs, 0);

        // ---------------- T6: the counter saturates, never wraps -----------
        do_reset;
        build_frame(8'h00, 3, 3);           // contains a 9-bit run = 72 clocks
        @(negedge clk); en6 = 1'b1;
        drive_seq;
        @(posedge clk); #1;
        $display("T6 W=6 saturation: max6=%0d (cap %0d)", max6, 63);
        chk("T6 saturates at the cap", max6, 63);

        // ---------------- T7: two frames accumulate evidence ---------------
        do_reset;
        @(negedge clk); en = 1'b1;
        build_frame(8'h55, 3, 2);
        run_oracle;
        drive_seq;
        begin : two_frame
            integer n_after_one;
            n_after_one = n_runs;
            build_frame(8'h55, 1, 3);
            drive_seq;
            @(posedge clk); #1;
            $display("T7 after 1 frame n=%0d, after 2 frames n=%0d", n_after_one, n_runs);
            chk("T7 evidence accumulates", (n_runs > n_after_one) ? 1 : 0, 1);
            chk("T7 min still one bit", min_run, BIT);
        end

        $display("");
        $display("== %0d checks, %0d failures ==", checks, fails);
        if (fails == 0) $display("   RESULT: ALL VERILOG BIT-PERIOD-MEAS 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_bit_period_meas.
//
// The oracle is deliberately NOT a second run-length counter. The testbench
// builds the capture as a vector of bit-times, then derives the expected
// run statistics by walking that vector in software, in units of BIT-TIMES.
// The DUT counts CLOCKS. The two only agree if the DUT's notion of a run
// boundary matches the stimulus, which is the property under test.
// ---------------------------------------------------------------------------
module tb_uart_bit_period_meas;

    localparam BIT = 8;          // clocks per bit-time
    localparam W   = 16;

    logic         clk = 1'b0;
    logic         rst_n = 1'b0;
    logic         en = 1'b0;
    logic         line = 1'b1;

    logic [W-1:0] min_run, max_run, n_runs;
    logic         valid;

    integer checks = 0;
    integer fails  = 0;

    always #5 clk = ~clk;

    uart_bit_period_meas #(.W(W)) dut (
        .clk(clk), .rst_n(rst_n), .en_i(en), .line_i(line),
        .min_run_o(min_run), .max_run_o(max_run),
        .n_runs_o(n_runs), .valid_o(valid)
    );

    // ---- narrow instance, used only to prove the counter saturates ----
    logic          en6 = 1'b0;
    logic [5:0]   min6, max6, n6;
    logic         valid6;
    uart_bit_period_meas #(.W(6)) dut6 (
        .clk(clk), .rst_n(rst_n), .en_i(en6), .line_i(line),
        .min_run_o(min6), .max_run_o(max6),
        .n_runs_o(n6), .valid_o(valid6)
    );

    // ---- the capture, as one entry per bit-time ----
    logic seq [0:255];
    integer seq_len;

    // ---- oracle results, in CLOCKS ----
    integer o_min, o_max, o_n;

    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

    // Build an 8N1 frame into seq[], LSB first, with `lead` idle bit-times
    // in front and `trail` idle bit-times behind.
    task automatic build_frame(input logic [7:0] data, input int lead, input int trail);
        int i;
        begin
            seq_len = 0;
            for (i = 0; i < lead; i = i + 1)  begin seq[seq_len] = 1'b1; seq_len = seq_len + 1; end
            seq[seq_len] = 1'b0; seq_len = seq_len + 1;               // start
            for (i = 0; i < 8; i = i + 1) begin
                seq[seq_len] = data[i]; seq_len = seq_len + 1;        // LSB first
            end
            seq[seq_len] = 1'b1; seq_len = seq_len + 1;               // stop
            for (i = 0; i < trail; i = i + 1) begin seq[seq_len] = 1'b1; seq_len = seq_len + 1; end
        end
    endtask

    // Walk seq[] and derive expected min/max/count of COMPLETE runs -- runs
    // bounded by two transitions. Lengths are converted to clocks at the end.
    task automatic run_oracle();
        int i, first_t, last_t, prev_t, len;
        begin
            o_min = 0; o_max = 0; o_n = 0;
            first_t = -1; prev_t = -1;
            for (i = 1; i < seq_len; i = i + 1) begin
                if (seq[i] !== seq[i-1]) begin
                    if (first_t == -1) begin
                        first_t = i;
                    end else begin
                        len = i - prev_t;
                        if (o_n == 0) begin o_min = len; o_max = len; end
                        else begin
                            if (len < o_min) o_min = len;
                            if (len > o_max) o_max = len;
                        end
                        o_n = o_n + 1;
                    end
                    prev_t = i;
                end
            end
            last_t = prev_t;
            o_min = o_min * BIT;
            o_max = o_max * BIT;
        end
    endtask

    task automatic drive_seq();
        int i, j;
        begin
            for (i = 0; i < seq_len; i = i + 1) begin
                @(negedge clk);
                line = seq[i];
                for (j = 0; j < BIT; j = j + 1) @(posedge clk);
            end
        end
    endtask

    task automatic do_reset();
        begin
            en = 1'b0; en6 = 1'b0; line = 1'b1;
            rst_n = 1'b0;
            repeat (3) @(posedge clk);
            @(negedge clk); rst_n = 1'b1;
            repeat (2) @(posedge clk);
        end
    endtask

    integer i;

    initial begin
        // ---------------- T1: alternating payload, rich evidence -----------
        do_reset;
        build_frame(8'h55, 3, 3);
        run_oracle;
        @(negedge clk); en = 1'b1;
        drive_seq;
        @(posedge clk); #1;
        $display("T1 0x55: min=%0d max=%0d n=%0d valid=%0b (oracle min=%0d max=%0d n=%0d)",
                 min_run, max_run, n_runs, valid, o_min, o_max, o_n);
        chk("T1 min", min_run, o_min);
        chk("T1 max", max_run, o_max);
        chk("T1 n",   n_runs,  o_n);
        chk("T1 valid", valid, 1);
        chk("T1 min is one bit", min_run, BIT);

        // ---------------- T2: 0x00 -- every bit merges into the start ------
        do_reset;
        build_frame(8'h00, 3, 3);
        run_oracle;
        @(negedge clk); en = 1'b1;
        drive_seq;
        @(posedge clk); #1;
        $display("T2 0x00: min=%0d max=%0d n=%0d (oracle min=%0d n=%0d)",
                 min_run, max_run, n_runs, o_min, o_n);
        chk("T2 min", min_run, o_min);
        chk("T2 n",   n_runs,  o_n);
        chk("T2 only one complete run", n_runs, 1);
        chk("T2 measured period is 9 bits, NOT 1", min_run, 9*BIT);

        // ---------------- T3: 0xFF -- one run, and it is correct -----------
        do_reset;
        build_frame(8'hFF, 3, 3);
        run_oracle;
        @(negedge clk); en = 1'b1;
        drive_seq;
        @(posedge clk); #1;
        $display("T3 0xFF: min=%0d n=%0d (oracle min=%0d n=%0d)", min_run, n_runs, o_min, o_n);
        chk("T3 min", min_run, o_min);
        chk("T3 n",   n_runs,  o_n);
        chk("T3 only one complete run", n_runs, 1);
        chk("T3 measured period is correct", min_run, BIT);

        // ---------------- T4: enable opens mid-run -------------------------
        // The line is already SPACE when the capture window opens. The opening
        // fragment must not be counted, or min_run would fall below one bit.
        do_reset;
        @(negedge clk); line = 1'b0;
        repeat (3) @(posedge clk);          // 3 clocks of an unmeasurable fragment
        @(negedge clk); en = 1'b1;
        repeat (2) @(posedge clk);          // 2 more clocks inside that fragment
        build_frame(8'h55, 0, 3);
        run_oracle;
        drive_seq;
        @(posedge clk); #1;
        $display("T4 mid-run enable: min=%0d n=%0d", min_run, n_runs);
        chk("T4 fragment not counted -- min is a full bit", min_run, BIT);

        // ---------------- T5: nothing measured yet -------------------------
        do_reset;
        @(negedge clk); en = 1'b1;
        repeat (40) @(posedge clk);         // idle only: no transition at all
        #1;
        chk("T5 valid low with no edges", valid, 0);
        chk("T5 no runs with no edges",   n_runs, 0);

        // ---------------- T6: the counter saturates, never wraps -----------
        do_reset;
        build_frame(8'h00, 3, 3);           // contains a 9-bit run = 72 clocks
        @(negedge clk); en6 = 1'b1;
        drive_seq;
        @(posedge clk); #1;
        $display("T6 W=6 saturation: max6=%0d (cap %0d)", max6, 63);
        chk("T6 saturates at the cap", max6, 63);

        // ---------------- T7: two frames accumulate evidence ---------------
        do_reset;
        @(negedge clk); en = 1'b1;
        build_frame(8'h55, 3, 2);
        run_oracle;
        drive_seq;
        begin : two_frame
            integer n_after_one;
            n_after_one = n_runs;
            build_frame(8'h55, 1, 3);
            drive_seq;
            @(posedge clk); #1;
            $display("T7 after 1 frame n=%0d, after 2 frames n=%0d", n_after_one, n_runs);
            chk("T7 evidence accumulates", (n_runs > n_after_one) ? 1 : 0, 1);
            chk("T7 min still one bit", min_run, BIT);
        end

        $display("");
        $display("== %0d checks, %0d failures ==", checks, fails);
        if (fails == 0) $display("   RESULT: ALL SYSTEMVERILOG BIT-PERIOD-MEAS TESTS PASSED");
        else            $display("   RESULT: %0d FAILURE(S)", fails);
        $finish;
    end

endmodule

VHDL

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- ---------------------------------------------------------------------------
-- Testbench for uart_bit_period_meas.
--
-- The oracle is deliberately NOT a second run-length counter. The testbench
-- builds the capture as a vector of bit-times, then derives the expected run
-- statistics by walking that vector in software, in units of BIT-TIMES. The
-- DUT counts CLOCKS. The two only agree if the DUT's notion of a run boundary
-- matches the stimulus, which is the property under test.
-- ---------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity tb_uart_bit_period_meas is
end entity tb_uart_bit_period_meas;

architecture sim of tb_uart_bit_period_meas is

    constant BITT : natural := 8;              -- clocks per bit-time
    constant W    : natural := 16;
    constant TCLK : time    := 10 ns;

    signal clk   : std_logic := '0';
    signal rst_n : std_logic := '0';
    signal en    : std_logic := '0';
    signal en6   : std_logic := '0';
    signal line  : std_logic := '1';
    signal done  : boolean   := false;

    signal min_run, max_run, n_runs : unsigned(W-1 downto 0);
    signal valid                    : std_logic;

    signal min6, max6, n6 : unsigned(5 downto 0);
    signal valid6         : std_logic;

begin

    clk <= '0' when done else not clk after TCLK/2;

    dut : entity work.uart_bit_period_meas
        generic map (W => W)
        port map (clk => clk, rst_n => rst_n, en_i => en, line_i => line,
                  min_run_o => min_run, max_run_o => max_run,
                  n_runs_o => n_runs, valid_o => valid);

    -- narrow instance, used only to prove the counter saturates
    dut6 : entity work.uart_bit_period_meas
        generic map (W => 6)
        port map (clk => clk, rst_n => rst_n, en_i => en6, line_i => line,
                  min_run_o => min6, max_run_o => max6,
                  n_runs_o => n6, valid_o => valid6);

    stim : process
        type seq_t is array (0 to 255) of std_logic;
        variable seq     : seq_t := (others => '1');
        variable seq_len : integer := 0;
        variable o_min, o_max, o_n : integer := 0;
        variable checks, fails     : integer := 0;

        procedure chk (name : string; got : integer; exp : integer) is
        begin
            checks := checks + 1;
            if got /= exp then
                fails := fails + 1;
                report "  FAIL " & name & ": got " & integer'image(got) &
                       " expected " & integer'image(exp) severity error;
            end if;
        end procedure;

        -- Build an 8N1 frame into seq, LSB first, with `lead` idle bit-times
        -- in front and `trail` idle bit-times behind.
        procedure build_frame (data : std_logic_vector(7 downto 0);
                               lead : integer; trail : integer) is
        begin
            seq_len := 0;
            for i in 0 to lead-1 loop
                seq(seq_len) := '1'; seq_len := seq_len + 1;
            end loop;
            seq(seq_len) := '0'; seq_len := seq_len + 1;          -- start
            for i in 0 to 7 loop
                seq(seq_len) := data(i); seq_len := seq_len + 1;  -- LSB first
            end loop;
            seq(seq_len) := '1'; seq_len := seq_len + 1;          -- stop
            for i in 0 to trail-1 loop
                seq(seq_len) := '1'; seq_len := seq_len + 1;
            end loop;
        end procedure;

        -- Walk seq and derive expected min/max/count of COMPLETE runs -- runs
        -- bounded by two transitions. Lengths convert to clocks at the end.
        procedure run_oracle is
            variable first_t, prev_t, len : integer;
        begin
            o_min := 0; o_max := 0; o_n := 0;
            first_t := -1; prev_t := -1;
            for i in 1 to seq_len-1 loop
                if seq(i) /= seq(i-1) then
                    if first_t = -1 then
                        first_t := i;
                    else
                        len := i - prev_t;
                        if o_n = 0 then
                            o_min := len; o_max := len;
                        else
                            if len < o_min then o_min := len; end if;
                            if len > o_max then o_max := len; end if;
                        end if;
                        o_n := o_n + 1;
                    end if;
                    prev_t := i;
                end if;
            end loop;
            o_min := o_min * BITT;
            o_max := o_max * BITT;
        end procedure;

        procedure drive_seq is
        begin
            for i in 0 to seq_len-1 loop
                wait until falling_edge(clk);
                line <= seq(i);
                for j in 0 to BITT-1 loop
                    wait until rising_edge(clk);
                end loop;
            end loop;
        end procedure;

        procedure do_reset is
        begin
            en <= '0'; en6 <= '0'; line <= '1';
            rst_n <= '0';
            for i in 0 to 2 loop wait until rising_edge(clk); end loop;
            wait until falling_edge(clk);
            rst_n <= '1';
            for i in 0 to 1 loop wait until rising_edge(clk); end loop;
        end procedure;

        variable n_after_one : integer;
    begin
        -- ---------------- T1: alternating payload, rich evidence -----------
        do_reset;
        build_frame(x"55", 3, 3);
        run_oracle;
        wait until falling_edge(clk); en <= '1';
        drive_seq;
        wait until rising_edge(clk); wait for 1 ns;
        report "T1 0x55: min=" & integer'image(to_integer(min_run)) &
               " max=" & integer'image(to_integer(max_run)) &
               " n=" & integer'image(to_integer(n_runs)) &
               " (oracle min=" & integer'image(o_min) &
               " max=" & integer'image(o_max) &
               " n=" & integer'image(o_n) & ")";
        chk("T1 min", to_integer(min_run), o_min);
        chk("T1 max", to_integer(max_run), o_max);
        chk("T1 n",   to_integer(n_runs),  o_n);
        chk("T1 valid", to_integer(unsigned'("" & valid)), 1);
        chk("T1 min is one bit", to_integer(min_run), BITT);

        -- ---------------- T2: 0x00 -- every bit merges into the start ------
        do_reset;
        build_frame(x"00", 3, 3);
        run_oracle;
        wait until falling_edge(clk); en <= '1';
        drive_seq;
        wait until rising_edge(clk); wait for 1 ns;
        report "T2 0x00: min=" & integer'image(to_integer(min_run)) &
               " n=" & integer'image(to_integer(n_runs)) &
               " (oracle min=" & integer'image(o_min) & ")";
        chk("T2 min", to_integer(min_run), o_min);
        chk("T2 n",   to_integer(n_runs),  o_n);
        chk("T2 only one complete run", to_integer(n_runs), 1);
        chk("T2 measured period is 9 bits, NOT 1", to_integer(min_run), 9*BITT);

        -- ---------------- T3: 0xFF -- one run, and it is correct -----------
        do_reset;
        build_frame(x"FF", 3, 3);
        run_oracle;
        wait until falling_edge(clk); en <= '1';
        drive_seq;
        wait until rising_edge(clk); wait for 1 ns;
        report "T3 0xFF: min=" & integer'image(to_integer(min_run)) &
               " n=" & integer'image(to_integer(n_runs));
        chk("T3 min", to_integer(min_run), o_min);
        chk("T3 n",   to_integer(n_runs),  o_n);
        chk("T3 only one complete run", to_integer(n_runs), 1);
        chk("T3 measured period is correct", to_integer(min_run), BITT);

        -- ---------------- T4: enable opens mid-run -------------------------
        -- The line is already SPACE when the capture window opens. The opening
        -- fragment must not be counted, or min_run would fall below one bit.
        do_reset;
        wait until falling_edge(clk); line <= '0';
        for i in 0 to 2 loop wait until rising_edge(clk); end loop;
        wait until falling_edge(clk); en <= '1';
        for i in 0 to 1 loop wait until rising_edge(clk); end loop;
        build_frame(x"55", 0, 3);
        run_oracle;
        drive_seq;
        wait until rising_edge(clk); wait for 1 ns;
        report "T4 mid-run enable: min=" & integer'image(to_integer(min_run)) &
               " n=" & integer'image(to_integer(n_runs));
        chk("T4 fragment not counted -- min is a full bit", to_integer(min_run), BITT);

        -- ---------------- T5: nothing measured yet -------------------------
        do_reset;
        wait until falling_edge(clk); en <= '1';
        for i in 0 to 39 loop wait until rising_edge(clk); end loop;
        wait for 1 ns;
        chk("T5 valid low with no edges", to_integer(unsigned'("" & valid)), 0);
        chk("T5 no runs with no edges",   to_integer(n_runs), 0);

        -- ---------------- T6: the counter saturates, never wraps -----------
        do_reset;
        build_frame(x"00", 3, 3);              -- contains a 9-bit run = 72 clocks
        wait until falling_edge(clk); en6 <= '1';
        drive_seq;
        wait until rising_edge(clk); wait for 1 ns;
        report "T6 W=6 saturation: max6=" & integer'image(to_integer(max6)) & " (cap 63)";
        chk("T6 saturates at the cap", to_integer(max6), 63);

        -- ---------------- T7: two frames accumulate evidence ---------------
        do_reset;
        wait until falling_edge(clk); en <= '1';
        build_frame(x"55", 3, 2);
        run_oracle;
        drive_seq;
        n_after_one := to_integer(n_runs);
        build_frame(x"55", 1, 3);
        drive_seq;
        wait until rising_edge(clk); wait for 1 ns;
        report "T7 after 1 frame n=" & integer'image(n_after_one) &
               ", after 2 frames n=" & integer'image(to_integer(n_runs));
        if to_integer(n_runs) > n_after_one then
            chk("T7 evidence accumulates", 1, 1);
        else
            chk("T7 evidence accumulates", 0, 1);
        end if;
        chk("T7 min still one bit", to_integer(min_run), BITT);

        report "";
        report "== " & integer'image(checks) & " checks, " &
               integer'image(fails) & " failures ==";
        if fails = 0 then
            report "   RESULT: ALL VHDL BIT-PERIOD-MEAS TESTS PASSED";
        else
            report "   RESULT: " & integer'image(fails) & " FAILURE(S)" severity error;
        end if;
        done <= true;
        wait;
    end process;

end architecture sim;

Seven tests, nineteen checks, and the three languages produce not merely the same verdicts but the same numbers and the same simulation end time (9,386 ns):

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  test  what it establishes                                   result
  ----  ---------------------------------------------------   ---------------
  T1    0x55 -- nine runs, all agreeing                        min=8   n=9
  T2    0x00 -- one run, and the answer is wrong               min=72  n=1
  T3    0xFF -- one run, and the answer is right               min=8   n=1
  T4    window opens mid-SPACE; the fragment is excluded       min=8
  T5    no transitions at all -- valid stays low               valid=0 n=0
  T6    a run longer than the counter saturates, never wraps   max=63 (cap 63)
  T7    a second frame accumulates evidence                    n: 9 -> 19

T6 uses a second instance of the same block narrowed to W = 6, so the cap is 63 and a 72-clock run has to saturate against it. Testing saturation by building a capture long enough to overflow a 16-bit counter would take 65,536 clocks and prove the same thing; narrowing the instance proves it in 72.

7. Proving the Tests Can Fail

A suite that passes tells you nothing until you have watched it fail. Three mutations were applied to the published Verilog, each removing exactly one decision, and the suite was re-run against each:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  mutation                                            checks failed   verdict
  -------------------------------------------------   -------------   -------
  M1  remove the `armed` gate                                     9    killed
  M2  start a new run at 0 instead of 1                           9    killed
  M3  remove the saturation guard                                 1    killed

M1 is the opening-fragment guard from §2 and §3, and nine checks notice its absence — T4 most directly, but the frame tests too, because the capture's opening idle interval is then counted as a legitimate run. M3 fails exactly one check, T6, which is the correct signature for a guard that only matters at the extreme: a mutation that broke many tests would mean the saturation logic was doing something it should not have been doing in the normal case.

8. What to Write Down

The output of reading a waveform should be a short written record, and it should separate the three claim types from §1:

  • The capture conditions. Which pin, which board, what the sample rate was, and whether the capture was triggered or free-running. A capture whose sample rate is not recorded cannot be re-analysed later.
  • The measured bit period, with its evidence count. "8 clocks, from 9 independent runs" is a usable fact. "8 clocks" alone is a number someone will later have to re-derive.
  • The decoded bytes, and the assumption used to decode them. Chiefly: LSB first, and the parity configuration assumed.
  • What you expected to see and did not. This is the entry that turns out to matter, because it is the one that stops the next person re-running the measurement you already did.

Continue learning

Where this fits

Part of the UART curriculum.