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.
| Claim | Kind | What backs it |
|---|---|---|
| "The line was SPACE for 72 clocks starting here" | Observation | the capture itself |
| "Therefore the bit period is 72 clocks" | Inference | a model of how frames are built |
| "Therefore the transmitter's divider is misconfigured" | Diagnosis | inference 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 cyclesTwo 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
// ---------------------------------------------------------------------------
// 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
endmoduleSystemVerilog
// ---------------------------------------------------------------------------
// 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
endmoduleVHDL
-- ---------------------------------------------------------------------------
-- 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:
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 nineThe 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 cyclesNow 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:
- 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.
- Data bit
koccupies clocks40 + 8(k+1)to40 + 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. - 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.
- The stop bit is sampled at
44 + 8 * 9 = 116and 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 cyclesThe 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
`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
endmoduleSystemVerilog
`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
endmoduleVHDL
-- ---------------------------------------------------------------------------
-- 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):
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 -> 19T6 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:
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 killedM1 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
Related tutorials
- Related topic
Baud Mismatch and Sampling-Error Signatures
Why sampling error accumulates across a frame and corrupts the high bits first, the arithmetic that fixes the tolerance at 5.26 percent, and the measured drift table for five receiver dividers sharing one wire.
- Related topic
Bit Order, Parity and Framing Failure Signatures
Why 0xA5 cannot detect a reversed byte, why parity is structurally blind to bit order, why one frame can never separate a parity misconfiguration from noise, and a classifier in three HDLs that resolves all four faults.
- Related topic
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.
- Related topic
FIFO Overrun and Flow-Control Failures
Three scenarios with the identical symptom and three different fixes, an instrumented FIFO in three HDLs that classifies each lost byte, and the headroom arithmetic that explains why RTS often fails on a USB-serial bridge.
Where this fits
Part of the UART curriculum.
