UART · Module 18
Case Study: Verifying and Debugging a Production UART IP
A UART taken to sign-off with full functional and code coverage, the silicon escape that followed, the malformed frame measured off the wire in three HDLs, and the missing coverage axis that explains both.
This is a story about a verification effort that did everything right and still shipped a bug.
The plan was written before the RTL. The coverage model was reviewed. Functional coverage reached 100%, code coverage reached 100%, the assertion suite passed, the random regression ran for weeks without a failure, and sign-off was granted on evidence rather than on schedule pressure.
Eight months later a customer reported an occasional corrupted byte. The cause was a state the verification plan had never described, reached by software doing something entirely reasonable that nobody had thought to model.
1. The Plan, and What Closure Meant
The IP was a configurable UART: programmable baud divider, 5–8 data bits, optional parity of either polarity, one or two stop bits, and a 16-byte FIFO in each direction.
The verification plan enumerated the configuration space and the traffic through it:
dimension values covered cross
------------------ ----------------------------------- -----
data bits 5, 6, 7, 8 all
parity none, even, odd all
stop bits 1, 2 all
baud divider min, nominal, max, and 40 random all
payload directed corners + random all
FIFO occupancy empty, 1, threshold-1, threshold, all
threshold+1, full
error injection parity, framing, break, overrun allEvery cross bin filled. Every line and branch of the RTL was exercised. Every assertion held across a multi-week random regression.
The plan also enumerated when configuration was written, and this is the sentence that mattered:
R12 The divider, frame format and parity registers are programmed
during initialisation, before traffic begins.That is a true statement about how the IP is normally used. It was written as a requirement, verified as a requirement, and it became the boundary of the verified space without anyone deciding that it should be.
2. What Software Actually Did
The customer's system supported dynamic frequency scaling. When the CPU changed clock frequency, a power-management callback recomputed every peripheral's dividers and wrote them back — including the UART's.
The callback ran on a timer. It did not, and had no reason to, check whether the UART happened to be transmitting.
Most of the time it landed between frames and nothing happened. Occasionally it landed inside one.
3. The Escape, Reproduced
Both variants of the transmitter are below: DEFER = 0 is the shipped behaviour, DEFER = 1 is the fix.
Verilog
// ---------------------------------------------------------------------------
// uart_cfg_tx -- a configurable transmitter, built in both the escaped and the
// fixed form, selected by the DEFER parameter.
//
// The escape: the baud divider is a register, software may write it at any
// time, and the original IP applied the write on the clock it arrived. Nobody
// wrote a test that reprogrammed the baud rate WHILE A FRAME WAS IN FLIGHT,
// because no sane driver does that on purpose. A power-management routine that
// re-derived the divider after a clock-frequency change did exactly that, on
// hardware, months later.
//
// The frame already on the wire then finishes at the new bit period. Its early
// bits are one width and its late bits another, so the receiver -- which
// re-synchronised on the start bit and is stepping at the OLD rate -- samples
// the tail of the frame in the wrong places. The symptom is a single corrupted
// byte at an unpredictable moment, which is close to the least debuggable
// signature a serial link can produce.
//
// DEFER = 1 latches the write into a shadow register and applies it at the
// next frame boundary. That is the fix, and it costs one register.
//
// mid_frame_wr_o is the detector that would have caught this in simulation:
// a sticky flag saying a configuration write landed while the transmitter was
// busy. An IP with that status bit turns a silicon escape into a failed
// assertion.
// ---------------------------------------------------------------------------
module uart_cfg_tx #(
parameter DEFER = 0 // 0 = apply at once (the escape), 1 = fixed
)(
input wire clk,
input wire rst_n,
input wire [15:0] cfg_div_i, // clocks per bit
input wire cfg_wr_i, // software writes the divider
input wire [7:0] tx_data_i,
input wire tx_start_i,
output reg tx_line_o,
output reg tx_busy_o,
output reg [15:0] div_active_o, // the divider actually in use right now
output reg mid_frame_wr_o, // sticky: a write landed mid-frame
output reg [15:0] n_applied_mid_o, // writes that TOOK EFFECT mid-frame
output reg [15:0] n_deferred_o // writes held back to a frame boundary
);
reg [9:0] shifter; // {stop, 8 data, start}
reg [3:0] bit_cnt;
reg [15:0] cnt;
reg [15:0] div_shadow;
reg pending;
wire loading = tx_start_i && !tx_busy_o;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
tx_line_o <= 1'b1; // idle is MARK
tx_busy_o <= 1'b0;
shifter <= 10'h3FF;
bit_cnt <= 4'd0;
cnt <= 16'd0;
div_active_o <= 16'd16;
div_shadow <= 16'd16;
pending <= 1'b0;
mid_frame_wr_o <= 1'b0;
n_applied_mid_o <= 16'd0;
n_deferred_o <= 16'd0;
end else begin
// ---------------- the configuration write --------------------
if (cfg_wr_i) begin
if (tx_busy_o) mid_frame_wr_o <= 1'b1; // sticky evidence
if (DEFER == 0) begin
// THE ESCAPE: applied on the spot, frame in flight or not.
div_active_o <= cfg_div_i;
if (tx_busy_o) n_applied_mid_o <= n_applied_mid_o + 16'd1;
end else begin
// THE FIX: held in a shadow until the frame boundary.
if (tx_busy_o) begin
div_shadow <= cfg_div_i;
pending <= 1'b1;
n_deferred_o <= n_deferred_o + 16'd1;
end else begin
div_active_o <= cfg_div_i; // idle: no reason to wait
end
end
end
// ---------------- the transmitter ----------------------------
if (!tx_busy_o) begin
tx_line_o <= 1'b1;
if (loading) begin
shifter <= {1'b1, tx_data_i, 1'b0}; // stop, data, start
bit_cnt <= 4'd10;
cnt <= 16'd0;
tx_busy_o <= 1'b1;
end
end else begin
tx_line_o <= shifter[0];
if (cnt == div_active_o - 16'd1) begin
cnt <= 16'd0;
shifter <= {1'b1, shifter[9:1]};
bit_cnt <= bit_cnt - 4'd1;
if (bit_cnt == 4'd1) begin
tx_busy_o <= 1'b0;
tx_line_o <= 1'b1;
// frame boundary: this is where a deferred write lands
if (pending) begin
div_active_o <= div_shadow;
pending <= 1'b0;
end
end
end else begin
cnt <= cnt + 16'd1;
end
end
end
end
endmoduleSystemVerilog
// ---------------------------------------------------------------------------
// uart_cfg_tx -- a configurable transmitter, built in both the escaped and the
// fixed form, selected by the DEFER parameter.
//
// The escape: the baud divider is a register, software may write it at any
// time, and the original IP applied the write on the clock it arrived. Nobody
// wrote a test that reprogrammed the baud rate WHILE A FRAME WAS IN FLIGHT,
// because no sane driver does that on purpose. A power-management routine that
// re-derived the divider after a clock-frequency change did exactly that, on
// hardware, months later.
//
// The frame already on the wire then finishes at the new bit period. Its early
// bits are one width and its late bits another, so the receiver -- which
// re-synchronised on the start bit and is stepping at the OLD rate -- samples
// the tail of the frame in the wrong places. The symptom is a single corrupted
// byte at an unpredictable moment, which is close to the least debuggable
// signature a serial link can produce.
//
// DEFER = 1 latches the write into a shadow register and applies it at the
// next frame boundary. That is the fix, and it costs one register.
//
// mid_frame_wr_o is the detector that would have caught this in simulation:
// a sticky flag saying a configuration write landed while the transmitter was
// busy. An IP with that status bit turns a silicon escape into a failed
// assertion.
// ---------------------------------------------------------------------------
module uart_cfg_tx #(
parameter int DEFER = 0 // 0 = apply at once (the escape), 1 = fixed
)(
input logic clk,
input logic rst_n,
input logic [15:0] cfg_div_i, // clocks per bit
input logic cfg_wr_i, // software writes the divider
input logic [7:0] tx_data_i,
input logic tx_start_i,
output logic tx_line_o,
output logic tx_busy_o,
output logic [15:0] div_active_o, // the divider actually in use right now
output logic mid_frame_wr_o, // sticky: a write landed mid-frame
output logic [15:0] n_applied_mid_o, // writes that TOOK EFFECT mid-frame
output logic [15:0] n_deferred_o // writes held back to a frame boundary
);
logic [9:0] shifter; // {stop, 8 data, start}
logic [3:0] bit_cnt;
logic [15:0] cnt;
logic [15:0] div_shadow;
logic pending;
wire loading = tx_start_i && !tx_busy_o;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
tx_line_o <= 1'b1; // idle is MARK
tx_busy_o <= 1'b0;
shifter <= 10'h3FF;
bit_cnt <= 4'd0;
cnt <= 16'd0;
div_active_o <= 16'd16;
div_shadow <= 16'd16;
pending <= 1'b0;
mid_frame_wr_o <= 1'b0;
n_applied_mid_o <= 16'd0;
n_deferred_o <= 16'd0;
end else begin
// ---------------- the configuration write --------------------
if (cfg_wr_i) begin
if (tx_busy_o) mid_frame_wr_o <= 1'b1; // sticky evidence
if (DEFER == 0) begin
// THE ESCAPE: applied on the spot, frame in flight or not.
div_active_o <= cfg_div_i;
if (tx_busy_o) n_applied_mid_o <= n_applied_mid_o + 16'd1;
end else begin
// THE FIX: held in a shadow until the frame boundary.
if (tx_busy_o) begin
div_shadow <= cfg_div_i;
pending <= 1'b1;
n_deferred_o <= n_deferred_o + 16'd1;
end else begin
div_active_o <= cfg_div_i; // idle: no reason to wait
end
end
end
// ---------------- the transmitter ----------------------------
if (!tx_busy_o) begin
tx_line_o <= 1'b1;
if (loading) begin
shifter <= {1'b1, tx_data_i, 1'b0}; // stop, data, start
bit_cnt <= 4'd10;
cnt <= 16'd0;
tx_busy_o <= 1'b1;
end
end else begin
tx_line_o <= shifter[0];
if (cnt == div_active_o - 16'd1) begin
cnt <= 16'd0;
shifter <= {1'b1, shifter[9:1]};
bit_cnt <= bit_cnt - 4'd1;
if (bit_cnt == 4'd1) begin
tx_busy_o <= 1'b0;
tx_line_o <= 1'b1;
// frame boundary: this is where a deferred write lands
if (pending) begin
div_active_o <= div_shadow;
pending <= 1'b0;
end
end
end else begin
cnt <= cnt + 16'd1;
end
end
end
end
endmoduleVHDL
-- ---------------------------------------------------------------------------
-- uart_cfg_tx -- a configurable transmitter, built in both the escaped and the
-- fixed form, selected by the DEFER generic.
--
-- The escape: the baud divider is a register, software may write it at any
-- time, and the original IP applied the write on the clock it arrived. Nobody
-- wrote a test that reprogrammed the baud rate WHILE A FRAME WAS IN FLIGHT,
-- because no sane driver does that on purpose. A power-management routine that
-- re-derived the divider after a clock-frequency change did exactly that, on
-- hardware, months later.
--
-- The frame already on the wire then finishes at the new bit period. Its early
-- bits are one width and its late bits another, so the receiver -- which
-- re-synchronised on the start bit and is stepping at the OLD rate -- samples
-- the tail of the frame in the wrong places. The symptom is a single corrupted
-- byte at an unpredictable moment, which is close to the least debuggable
-- signature a serial link can produce.
--
-- DEFER = 1 latches the write into a shadow register and applies it at the
-- next frame boundary. That is the fix, and it costs one register.
--
-- mid_frame_wr_o is the detector that would have caught this in simulation:
-- a sticky flag saying a configuration write landed while the transmitter was
-- busy. An IP with that status bit turns a silicon escape into a failed
-- assertion.
-- ---------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity uart_cfg_tx is
generic (
DEFER : natural := 0 -- 0 = apply at once (escape), 1 = fixed
);
port (
clk : in std_logic;
rst_n : in std_logic;
cfg_div_i : in unsigned(15 downto 0); -- clocks per bit
cfg_wr_i : in std_logic; -- software writes it
tx_data_i : in std_logic_vector(7 downto 0);
tx_start_i : in std_logic;
tx_line_o : out std_logic;
tx_busy_o : out std_logic;
div_active_o : out unsigned(15 downto 0); -- divider in use now
mid_frame_wr_o : out std_logic; -- sticky: write mid-frame
n_applied_mid_o : out unsigned(15 downto 0); -- took effect mid-frame
n_deferred_o : out unsigned(15 downto 0) -- held to a boundary
);
end entity uart_cfg_tx;
architecture rtl of uart_cfg_tx is
signal shifter : std_logic_vector(9 downto 0) := (others => '1');
signal bit_cnt : unsigned(3 downto 0) := (others => '0');
signal cnt : unsigned(15 downto 0) := (others => '0');
signal div_shadow : unsigned(15 downto 0) := to_unsigned(16, 16);
signal pending : std_logic := '0';
-- outputs mirrored internally: an entity may not read its own outputs
signal tx_line : std_logic := '1';
signal tx_busy : std_logic := '0';
signal div_active : unsigned(15 downto 0) := to_unsigned(16, 16);
signal mid_wr : std_logic := '0';
signal n_mid : unsigned(15 downto 0) := (others => '0');
signal n_def : unsigned(15 downto 0) := (others => '0');
signal loading : std_logic;
begin
loading <= '1' when (tx_start_i = '1' and tx_busy = '0') else '0';
tx_line_o <= tx_line;
tx_busy_o <= tx_busy;
div_active_o <= div_active;
mid_frame_wr_o <= mid_wr;
n_applied_mid_o <= n_mid;
n_deferred_o <= n_def;
process (clk, rst_n)
begin
if rst_n = '0' then
tx_line <= '1'; -- idle is MARK
tx_busy <= '0';
shifter <= (others => '1');
bit_cnt <= (others => '0');
cnt <= (others => '0');
div_active <= to_unsigned(16, 16);
div_shadow <= to_unsigned(16, 16);
pending <= '0';
mid_wr <= '0';
n_mid <= (others => '0');
n_def <= (others => '0');
elsif rising_edge(clk) then
-- ---------------- the configuration write --------------------
if cfg_wr_i = '1' then
if tx_busy = '1' then
mid_wr <= '1'; -- sticky evidence
end if;
if DEFER = 0 then
-- THE ESCAPE: applied on the spot, frame in flight or not.
div_active <= cfg_div_i;
if tx_busy = '1' then
n_mid <= n_mid + 1;
end if;
else
-- THE FIX: held in a shadow until the frame boundary.
if tx_busy = '1' then
div_shadow <= cfg_div_i;
pending <= '1';
n_def <= n_def + 1;
else
div_active <= cfg_div_i; -- idle: no reason to wait
end if;
end if;
end if;
-- ---------------- the transmitter ----------------------------
if tx_busy = '0' then
tx_line <= '1';
if loading = '1' then
shifter <= '1' & tx_data_i & '0'; -- stop, data, start
bit_cnt <= to_unsigned(10, 4);
cnt <= (others => '0');
tx_busy <= '1';
end if;
else
tx_line <= shifter(0);
if cnt = div_active - 1 then
cnt <= (others => '0');
shifter <= '1' & shifter(9 downto 1);
bit_cnt <= bit_cnt - 1;
if bit_cnt = to_unsigned(1, 4) then
tx_busy <= '0';
tx_line <= '1';
-- frame boundary: a deferred write lands here
if pending = '1' then
div_active <= div_shadow;
pending <= '0';
end if;
end if;
else
cnt <= cnt + 1;
end if;
end if;
end if;
end process;
end architecture rtl;4. The Measurement
Both variants were driven identically: start a frame carrying 0x55, then rewrite the divider from 16 to 8 clocks per bit, four bit-times in.
The payload matters. 0x55 alternates, so every bit of the frame forms its own run on the wire and the testbench can read the width of each transmitted bit directly off the line rather than inferring it:
variant bit widths, as transmitted frame length
-------------------- ------------------------------ ------------
undisturbed 16 16 16 16 16 16 16 16 160 clocks
escaped (DEFER = 0) 16 16 16 16 8 8 8 8 112 clocks
fixed (DEFER = 1) 16 16 16 16 16 16 16 16 160 clocks
fixed, the NEXT frame 8 8 8 8 8 8 8 8 80 clocksThe escaped frame is malformed in the most awkward possible way. It is not truncated, not obviously broken, and carries a plausible start bit — it simply changes bit period halfway through.
A frame whose bit period changes in flight
11 cyclesThe receiver at the far end re-synchronised on the start bit and is stepping at the original rate, exactly as Chapter 17.2 describes. Its sample points for the late bits walk off the end of the intervals actually being transmitted. The result is a single corrupted byte — and, because the frame ends early, a stop bit sampled somewhere it should not be, so sometimes a framing error and sometimes not.
5. Why the Signature Was So Hard to Read
The field report was "occasional single-byte corruption under load, roughly once an hour, not reproducible on demand." Everything about that description points away from the real cause.
| Observation | What it suggested | Why that was wrong |
|---|---|---|
| Occasional | noise, marginal signal integrity | the rate was set by how often a timer callback coincided with a frame |
| Under load | a FIFO or flow-control problem | load only increased the number of frames, raising the collision odds |
| Single byte | a bit flip | the whole tail of one frame was mis-sampled, which often decodes as one wrong byte |
| Not reproducible | environmental | perfectly reproducible, given a testbench willing to write a register mid-frame |
The corruption concentrated in the high bits, which by Chapter 17.2 §8 is a baud-error signature — and the baud was wrong, for four bit-times, in the middle of a frame. The diagnostic table pointed at the right family of cause. What nobody did was ask why the baud would change without anyone changing it.
The decisive step was capturing the divider register together with the transmitter's busy signal, which is an on-chip capture of exactly the kind Chapter 17.6 describes. Neither signal alone shows anything wrong. The two together show a write landing inside a frame, once every few thousand frames.
6. The Fix, and the Detector
The fix is DEFER = 1: latch the write into a shadow register and apply it at the next frame boundary. It costs one 16-bit register and changes nothing about the programming model except that a mid-frame write now takes effect slightly later than it used to — which no correct driver can observe.
Writing while idle still applies immediately, because there is nothing to defer.
The more important addition is mid_frame_wr_o, the sticky flag that records a configuration write landing while the transmitter was busy:
7. The Testbench
Verilog
`timescale 1ns/1ps
// ---------------------------------------------------------------------------
// Testbench for uart_cfg_tx -- the silicon escape, reproduced and fixed.
//
// Both variants of the transmitter are instantiated and driven with identical
// stimulus: start a frame, then reprogram the baud divider four bit-times in.
//
// The corruption is MEASURED rather than asserted. The payload is 0x55, which
// alternates, so every bit of the frame forms its own run on the wire and the
// testbench can read back the width of each bit directly. A well-formed frame
// is ten runs of equal width; the escaped variant produces a frame whose early
// bits are 16 clocks and whose late bits are 8.
// ---------------------------------------------------------------------------
module tb_uart_cfg_tx;
localparam DIV_A = 16; // the divider the frame starts with
localparam DIV_B = 8; // what software reprograms it to
reg clk = 1'b0;
reg rst_n = 1'b0;
reg [15:0] cfg_div = DIV_A;
reg cfg_wr_e = 1'b0, cfg_wr_f = 1'b0;
reg [7:0] tx_data = 8'h55;
reg tx_start_e = 1'b0, tx_start_f = 1'b0;
// escaped variant
wire line_e, busy_e, midwr_e;
wire [15:0] div_e, n_mid_e, n_def_e;
// fixed variant
wire line_f, busy_f, midwr_f;
wire [15:0] div_f, n_mid_f, n_def_f;
integer checks = 0;
integer fails = 0;
always #5 clk = ~clk;
uart_cfg_tx #(.DEFER(0)) esc (
.clk(clk), .rst_n(rst_n), .cfg_div_i(cfg_div), .cfg_wr_i(cfg_wr_e),
.tx_data_i(tx_data), .tx_start_i(tx_start_e),
.tx_line_o(line_e), .tx_busy_o(busy_e), .div_active_o(div_e),
.mid_frame_wr_o(midwr_e), .n_applied_mid_o(n_mid_e), .n_deferred_o(n_def_e));
uart_cfg_tx #(.DEFER(1)) fix (
.clk(clk), .rst_n(rst_n), .cfg_div_i(cfg_div), .cfg_wr_i(cfg_wr_f),
.tx_data_i(tx_data), .tx_start_i(tx_start_f),
.tx_line_o(line_f), .tx_busy_o(busy_f), .div_active_o(div_f),
.mid_frame_wr_o(midwr_f), .n_applied_mid_o(n_mid_f), .n_deferred_o(n_def_f));
// ---- measure the width of every bit on the wire ----
// The payload alternates, so each bit is its own run and the run lengths
// ARE the bit widths as actually transmitted.
integer runs_e [0:19];
integer runs_f [0:19];
integer n_e = 0, n_f = 0;
integer len_e = 0, len_f = 0;
reg prev_e = 1'b1, prev_f = 1'b1;
reg rec = 1'b0;
always @(posedge clk) begin
if (!rec) begin
n_e = 0; len_e = 0; prev_e = 1'b1;
n_f = 0; len_f = 0; prev_f = 1'b1;
end else begin
if (line_e !== prev_e) begin
if (n_e < 20) begin runs_e[n_e] = len_e; n_e = n_e + 1; end
len_e = 1;
end else len_e = len_e + 1;
prev_e = line_e;
if (line_f !== prev_f) begin
if (n_f < 20) begin runs_f[n_f] = len_f; n_f = n_f + 1; end
len_f = 1;
end else len_f = len_f + 1;
prev_f = line_f;
end
end
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
// runs[1..8] are the start bit and the first seven data bits; runs[0] is
// the idle fragment before the start edge and is not a bit.
function integer uniform_e;
input integer w;
integer i, r;
begin
r = 1;
for (i = 1; i <= 8; i = i + 1) if (runs_e[i] != w) r = 0;
uniform_e = r;
end
endfunction
function integer uniform_f;
input integer w;
integer i, r;
begin
r = 1;
for (i = 1; i <= 8; i = i + 1) if (runs_f[i] != w) r = 0;
uniform_f = r;
end
endfunction
task show_e;
input [127:0] tag;
integer i;
begin
$write(" %0s bit widths:", tag);
for (i = 1; i <= 8; i = i + 1) $write(" %0d", runs_e[i]);
$display("");
end
endtask
task show_f;
input [127:0] tag;
integer i;
begin
$write(" %0s bit widths:", tag);
for (i = 1; i <= 8; i = i + 1) $write(" %0d", runs_f[i]);
$display("");
end
endtask
integer t_start, dur_e, dur_f;
task do_reset;
begin
rst_n = 1'b0; rec = 1'b0; cfg_div = DIV_A;
cfg_wr_e = 0; cfg_wr_f = 0; tx_start_e = 0; tx_start_f = 0;
repeat (3) @(posedge clk);
@(negedge clk); rst_n = 1'b1;
repeat (2) @(posedge clk);
end
endtask
// Start both frames, optionally reprogramming the divider `at_bit` bit
// times in, and record how long each frame took.
task run_frame;
input integer at_bit; // -1 = no reconfiguration
integer i;
begin
@(negedge clk); tx_start_e = 1'b1; tx_start_f = 1'b1; rec = 1'b1;
@(posedge clk);
@(negedge clk); tx_start_e = 1'b0; tx_start_f = 1'b0;
t_start = $time;
if (at_bit >= 0) begin
repeat (at_bit * DIV_A) @(posedge clk);
@(negedge clk); cfg_div = DIV_B; cfg_wr_e = 1'b1; cfg_wr_f = 1'b1;
@(posedge clk);
@(negedge clk); cfg_wr_e = 1'b0; cfg_wr_f = 1'b0;
end
wait (busy_e == 1'b0 && busy_f == 1'b0);
@(posedge clk);
dur_e = 0; dur_f = 0;
@(negedge clk); rec = 1'b0;
repeat (4) @(posedge clk);
end
endtask
// frame duration measured independently, by watching busy
integer te0, te1, tf0, tf1;
initial begin : dur_mon
forever begin
@(posedge busy_e); te0 = $time;
@(negedge busy_e); te1 = $time;
end
end
initial begin : dur_mon_f
forever begin
@(posedge busy_f); tf0 = $time;
@(negedge busy_f); tf1 = $time;
end
end
initial begin
// ---------------- T1: no reconfiguration, both variants -----------
do_reset;
run_frame(-1);
$display("T1 undisturbed frame");
show_e(" escaped"); show_f(" fixed ");
chk("T1 escaped frame is uniform", uniform_e(DIV_A), 1);
chk("T1 fixed frame is uniform", uniform_f(DIV_A), 1);
chk("T1 both took 160 clocks (escaped)", (te1-te0)/10, 160);
chk("T1 both took 160 clocks (fixed)", (tf1-tf0)/10, 160);
chk("T1 no mid-frame write flagged", midwr_e, 0);
// ---------------- T2/T3: reprogram 4 bit-times into the frame -----
do_reset;
run_frame(4);
$display("T2 divider rewritten from %0d to %0d, four bit-times in", DIV_A, DIV_B);
show_e(" escaped"); show_f(" fixed ");
$display(" escaped frame: %0d clocks fixed frame: %0d clocks",
(te1-te0)/10, (tf1-tf0)/10);
chk("T2 the escaped frame is MALFORMED", uniform_e(DIV_A), 0);
chk("T2 the escaped frame finished early",
((te1-te0)/10 < 160) ? 1 : 0, 1);
chk("T2 the escape applied the write mid-frame", n_mid_e, 1);
chk("T2 the detector fired", midwr_e, 1);
chk("T3 the fixed frame is well formed", uniform_f(DIV_A), 1);
chk("T3 the fixed frame took full length", (tf1-tf0)/10, 160);
chk("T3 the write was deferred", n_def_f, 1);
chk("T3 nothing was applied mid-frame", n_mid_f, 0);
chk("T3 the detector still warns", midwr_f, 1);
// ---------------- T4: the deferred write lands at the boundary ----
chk("T4 the fixed variant now runs at the new rate", div_f, DIV_B);
run_frame(-1);
$display("T4 the next frame, after the deferred write");
show_f(" fixed ");
chk("T4 the next frame is uniform at the new rate", uniform_f(DIV_B), 1);
chk("T4 and takes half as long", (tf1-tf0)/10, 80);
// ---------------- T5: reconfiguring while idle is always fine -----
do_reset;
@(negedge clk); cfg_div = DIV_B; cfg_wr_e = 1'b1; cfg_wr_f = 1'b1;
@(posedge clk);
@(negedge clk); cfg_wr_e = 1'b0; cfg_wr_f = 1'b0;
#1;
$display("T5 written while idle : escaped div=%0d fixed div=%0d",
div_e, div_f);
chk("T5 escaped applies it", div_e, DIV_B);
chk("T5 fixed applies it too -- there is nothing to defer", div_f, DIV_B);
chk("T5 no mid-frame write (escaped)", midwr_e, 0);
chk("T5 no mid-frame write (fixed)", midwr_f, 0);
// ---------------- T6: the detector is STICKY ----------------------
// A mid-frame write raises the flag. A later, perfectly legal, idle
// write must NOT erase the evidence -- a detector that forgets is a
// detector nobody ever sees fire.
do_reset;
run_frame(4); // the offence
#1;
chk("T6 the offence was flagged (escaped)", midwr_e, 1);
chk("T6 the offence was flagged (fixed)", midwr_f, 1);
@(negedge clk); cfg_div = DIV_A; cfg_wr_e = 1'b1; cfg_wr_f = 1'b1;
@(posedge clk);
@(negedge clk); cfg_wr_e = 1'b0; cfg_wr_f = 1'b0;
repeat (2) @(posedge clk); #1;
$display("T6 legal idle write after : midwr_e=%0b midwr_f=%0b", midwr_e, midwr_f);
chk("T6 evidence survives (escaped)", midwr_e, 1);
chk("T6 evidence survives (fixed)", midwr_f, 1);
$display("");
$display("== %0d checks, %0d failures ==", checks, fails);
if (fails == 0) $display(" RESULT: ALL VERILOG CFG-TX TESTS PASSED");
else $display(" RESULT: %0d FAILURE(S)", fails);
$finish;
end
endmoduleSystemVerilog
`timescale 1ns/1ps
// ---------------------------------------------------------------------------
// Testbench for uart_cfg_tx -- the silicon escape, reproduced and fixed.
//
// Both variants of the transmitter are instantiated and driven with identical
// stimulus: start a frame, then reprogram the baud divider four bit-times in.
//
// The corruption is MEASURED rather than asserted. The payload is 0x55, which
// alternates, so every bit of the frame forms its own run on the wire and the
// testbench can read back the width of each bit directly. A well-formed frame
// is ten runs of equal width; the escaped variant produces a frame whose early
// bits are 16 clocks and whose late bits are 8.
// ---------------------------------------------------------------------------
module tb_uart_cfg_tx;
localparam DIV_A = 16; // the divider the frame starts with
localparam DIV_B = 8; // what software reprograms it to
logic clk = 1'b0;
logic rst_n = 1'b0;
logic [15:0] cfg_div = DIV_A;
logic cfg_wr_e = 1'b0, cfg_wr_f = 1'b0;
logic [7:0] tx_data = 8'h55;
logic tx_start_e = 1'b0, tx_start_f = 1'b0;
// escaped variant
logic line_e, busy_e, midwr_e;
logic [15:0] div_e, n_mid_e, n_def_e;
// fixed variant
logic line_f, busy_f, midwr_f;
logic [15:0] div_f, n_mid_f, n_def_f;
integer checks = 0;
integer fails = 0;
always #5 clk = ~clk;
uart_cfg_tx #(.DEFER(0)) esc (
.clk(clk), .rst_n(rst_n), .cfg_div_i(cfg_div), .cfg_wr_i(cfg_wr_e),
.tx_data_i(tx_data), .tx_start_i(tx_start_e),
.tx_line_o(line_e), .tx_busy_o(busy_e), .div_active_o(div_e),
.mid_frame_wr_o(midwr_e), .n_applied_mid_o(n_mid_e), .n_deferred_o(n_def_e));
uart_cfg_tx #(.DEFER(1)) fix (
.clk(clk), .rst_n(rst_n), .cfg_div_i(cfg_div), .cfg_wr_i(cfg_wr_f),
.tx_data_i(tx_data), .tx_start_i(tx_start_f),
.tx_line_o(line_f), .tx_busy_o(busy_f), .div_active_o(div_f),
.mid_frame_wr_o(midwr_f), .n_applied_mid_o(n_mid_f), .n_deferred_o(n_def_f));
// ---- measure the width of every bit on the wire ----
// The payload alternates, so each bit is its own run and the run lengths
// ARE the bit widths as actually transmitted.
integer runs_e [0:19];
integer runs_f [0:19];
integer n_e = 0, n_f = 0;
integer len_e = 0, len_f = 0;
logic prev_e = 1'b1, prev_f = 1'b1;
logic rec = 1'b0;
always @(posedge clk) begin
if (!rec) begin
n_e = 0; len_e = 0; prev_e = 1'b1;
n_f = 0; len_f = 0; prev_f = 1'b1;
end else begin
if (line_e !== prev_e) begin
if (n_e < 20) begin runs_e[n_e] = len_e; n_e = n_e + 1; end
len_e = 1;
end else len_e = len_e + 1;
prev_e = line_e;
if (line_f !== prev_f) begin
if (n_f < 20) begin runs_f[n_f] = len_f; n_f = n_f + 1; end
len_f = 1;
end else len_f = len_f + 1;
prev_f = line_f;
end
end
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
// runs[1..8] are the start bit and the first seven data bits; runs[0] is
// the idle fragment before the start edge and is not a bit.
function automatic int uniform_e(input int w);
int i, r;
begin
r = 1;
for (i = 1; i <= 8; i = i + 1) if (runs_e[i] != w) r = 0;
uniform_e = r;
end
endfunction
function automatic int uniform_f(input int w);
int i, r;
begin
r = 1;
for (i = 1; i <= 8; i = i + 1) if (runs_f[i] != w) r = 0;
uniform_f = r;
end
endfunction
task automatic show_e(input string tag);
int i;
begin
$write(" %0s bit widths:", tag);
for (i = 1; i <= 8; i = i + 1) $write(" %0d", runs_e[i]);
$display("");
end
endtask
task automatic show_f(input string tag);
int i;
begin
$write(" %0s bit widths:", tag);
for (i = 1; i <= 8; i = i + 1) $write(" %0d", runs_f[i]);
$display("");
end
endtask
integer t_start, dur_e, dur_f;
task automatic do_reset();
begin
rst_n = 1'b0; rec = 1'b0; cfg_div = DIV_A;
cfg_wr_e = 0; cfg_wr_f = 0; tx_start_e = 0; tx_start_f = 0;
repeat (3) @(posedge clk);
@(negedge clk); rst_n = 1'b1;
repeat (2) @(posedge clk);
end
endtask
// Start both frames, optionally reprogramming the divider `at_bit` bit
// times in, and record how long each frame took.
task automatic run_frame(input int at_bit); // -1 = no reconfiguration
int i;
begin
@(negedge clk); tx_start_e = 1'b1; tx_start_f = 1'b1; rec = 1'b1;
@(posedge clk);
@(negedge clk); tx_start_e = 1'b0; tx_start_f = 1'b0;
t_start = $time;
if (at_bit >= 0) begin
repeat (at_bit * DIV_A) @(posedge clk);
@(negedge clk); cfg_div = DIV_B; cfg_wr_e = 1'b1; cfg_wr_f = 1'b1;
@(posedge clk);
@(negedge clk); cfg_wr_e = 1'b0; cfg_wr_f = 1'b0;
end
wait (busy_e == 1'b0 && busy_f == 1'b0);
@(posedge clk);
dur_e = 0; dur_f = 0;
@(negedge clk); rec = 1'b0;
repeat (4) @(posedge clk);
end
endtask
// frame duration measured independently, by watching busy
integer te0, te1, tf0, tf1;
initial begin : dur_mon
forever begin
@(posedge busy_e); te0 = $time;
@(negedge busy_e); te1 = $time;
end
end
initial begin : dur_mon_f
forever begin
@(posedge busy_f); tf0 = $time;
@(negedge busy_f); tf1 = $time;
end
end
initial begin
// ---------------- T1: no reconfiguration, both variants -----------
do_reset;
run_frame(-1);
$display("T1 undisturbed frame");
show_e(" escaped"); show_f(" fixed ");
chk("T1 escaped frame is uniform", uniform_e(DIV_A), 1);
chk("T1 fixed frame is uniform", uniform_f(DIV_A), 1);
chk("T1 both took 160 clocks (escaped)", (te1-te0)/10, 160);
chk("T1 both took 160 clocks (fixed)", (tf1-tf0)/10, 160);
chk("T1 no mid-frame write flagged", midwr_e, 0);
// ---------------- T2/T3: reprogram 4 bit-times into the frame -----
do_reset;
run_frame(4);
$display("T2 divider rewritten from %0d to %0d, four bit-times in", DIV_A, DIV_B);
show_e(" escaped"); show_f(" fixed ");
$display(" escaped frame: %0d clocks fixed frame: %0d clocks",
(te1-te0)/10, (tf1-tf0)/10);
chk("T2 the escaped frame is MALFORMED", uniform_e(DIV_A), 0);
chk("T2 the escaped frame finished early",
((te1-te0)/10 < 160) ? 1 : 0, 1);
chk("T2 the escape applied the write mid-frame", n_mid_e, 1);
chk("T2 the detector fired", midwr_e, 1);
chk("T3 the fixed frame is well formed", uniform_f(DIV_A), 1);
chk("T3 the fixed frame took full length", (tf1-tf0)/10, 160);
chk("T3 the write was deferred", n_def_f, 1);
chk("T3 nothing was applied mid-frame", n_mid_f, 0);
chk("T3 the detector still warns", midwr_f, 1);
// ---------------- T4: the deferred write lands at the boundary ----
chk("T4 the fixed variant now runs at the new rate", div_f, DIV_B);
run_frame(-1);
$display("T4 the next frame, after the deferred write");
show_f(" fixed ");
chk("T4 the next frame is uniform at the new rate", uniform_f(DIV_B), 1);
chk("T4 and takes half as long", (tf1-tf0)/10, 80);
// ---------------- T5: reconfiguring while idle is always fine -----
do_reset;
@(negedge clk); cfg_div = DIV_B; cfg_wr_e = 1'b1; cfg_wr_f = 1'b1;
@(posedge clk);
@(negedge clk); cfg_wr_e = 1'b0; cfg_wr_f = 1'b0;
#1;
$display("T5 written while idle : escaped div=%0d fixed div=%0d",
div_e, div_f);
chk("T5 escaped applies it", div_e, DIV_B);
chk("T5 fixed applies it too -- there is nothing to defer", div_f, DIV_B);
chk("T5 no mid-frame write (escaped)", midwr_e, 0);
chk("T5 no mid-frame write (fixed)", midwr_f, 0);
// ---------------- T6: the detector is STICKY ----------------------
// A mid-frame write raises the flag. A later, perfectly legal, idle
// write must NOT erase the evidence -- a detector that forgets is a
// detector nobody ever sees fire.
do_reset;
run_frame(4); // the offence
#1;
chk("T6 the offence was flagged (escaped)", midwr_e, 1);
chk("T6 the offence was flagged (fixed)", midwr_f, 1);
@(negedge clk); cfg_div = DIV_A; cfg_wr_e = 1'b1; cfg_wr_f = 1'b1;
@(posedge clk);
@(negedge clk); cfg_wr_e = 1'b0; cfg_wr_f = 1'b0;
repeat (2) @(posedge clk); #1;
$display("T6 legal idle write after : midwr_e=%0b midwr_f=%0b", midwr_e, midwr_f);
chk("T6 evidence survives (escaped)", midwr_e, 1);
chk("T6 evidence survives (fixed)", midwr_f, 1);
$display("");
$display("== %0d checks, %0d failures ==", checks, fails);
if (fails == 0) $display(" RESULT: ALL SYSTEMVERILOG CFG-TX TESTS PASSED");
else $display(" RESULT: %0d FAILURE(S)", fails);
$finish;
end
endmoduleVHDL
-- ---------------------------------------------------------------------------
-- Testbench for uart_cfg_tx -- the silicon escape, reproduced and fixed.
--
-- Both variants of the transmitter are instantiated and driven with identical
-- stimulus: start a frame, then reprogram the baud divider four bit-times in.
--
-- The corruption is MEASURED rather than asserted. The payload is 0x55, which
-- alternates, so every bit of the frame forms its own run on the wire and the
-- testbench can read back the width of each bit directly. A well-formed frame
-- is ten runs of equal width; the escaped variant produces a frame whose early
-- bits are 16 clocks and whose late bits are 8.
-- ---------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity tb_uart_cfg_tx is
end entity tb_uart_cfg_tx;
architecture sim of tb_uart_cfg_tx is
constant DIV_A : natural := 16; -- the divider a frame starts with
constant DIV_B : natural := 8; -- what software reprograms it to
constant TCLK : time := 10 ns;
type runarr is array (0 to 19) of integer;
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal cfg_div : unsigned(15 downto 0) := to_unsigned(DIV_A, 16);
signal cfg_wr_e : std_logic := '0';
signal cfg_wr_f : std_logic := '0';
signal tx_data : std_logic_vector(7 downto 0) := x"55";
signal tx_start_e : std_logic := '0';
signal tx_start_f : std_logic := '0';
signal sim_done : boolean := false;
-- escaped variant
signal line_e, busy_e, midwr_e : std_logic;
signal div_e, n_mid_e, n_def_e : unsigned(15 downto 0);
-- fixed variant
signal line_f, busy_f, midwr_f : std_logic;
signal div_f, n_mid_f, n_def_f : unsigned(15 downto 0);
signal rec : std_logic := '0';
signal runs_e : runarr := (others => 0);
signal runs_f : runarr := (others => 0);
signal dur_e : integer := 0;
signal dur_f : integer := 0;
begin
clk <= '0' when sim_done else not clk after TCLK/2;
esc : entity work.uart_cfg_tx
generic map (DEFER => 0)
port map (clk => clk, rst_n => rst_n, cfg_div_i => cfg_div,
cfg_wr_i => cfg_wr_e, tx_data_i => tx_data,
tx_start_i => tx_start_e, tx_line_o => line_e,
tx_busy_o => busy_e, div_active_o => div_e,
mid_frame_wr_o => midwr_e, n_applied_mid_o => n_mid_e,
n_deferred_o => n_def_e);
fix : entity work.uart_cfg_tx
generic map (DEFER => 1)
port map (clk => clk, rst_n => rst_n, cfg_div_i => cfg_div,
cfg_wr_i => cfg_wr_f, tx_data_i => tx_data,
tx_start_i => tx_start_f, tx_line_o => line_f,
tx_busy_o => busy_f, div_active_o => div_f,
mid_frame_wr_o => midwr_f, n_applied_mid_o => n_mid_f,
n_deferred_o => n_def_f);
-- ---- measure the width of every bit on the wire ----
-- The payload alternates, so each bit is its own run and the run lengths
-- ARE the bit widths as actually transmitted.
meas : process (clk)
variable n_e, n_f, len_e, len_f : integer := 0;
variable prev_e, prev_f : std_logic := '1';
begin
if rising_edge(clk) then
if rec = '0' then
n_e := 0; len_e := 0; prev_e := '1';
n_f := 0; len_f := 0; prev_f := '1';
else
if line_e /= prev_e then
if n_e < 20 then runs_e(n_e) <= len_e; n_e := n_e + 1; end if;
len_e := 1;
else
len_e := len_e + 1;
end if;
prev_e := line_e;
if line_f /= prev_f then
if n_f < 20 then runs_f(n_f) <= len_f; n_f := n_f + 1; end if;
len_f := 1;
else
len_f := len_f + 1;
end if;
prev_f := line_f;
end if;
end if;
end process;
-- ---- frame duration, measured independently by watching busy ----
dur_mon_e : process
variable t0 : time;
begin
wait until rising_edge(busy_e);
t0 := now;
wait until falling_edge(busy_e);
dur_e <= (now - t0) / TCLK;
end process;
dur_mon_f : process
variable t0 : time;
begin
wait until rising_edge(busy_f);
t0 := now;
wait until falling_edge(busy_f);
dur_f <= (now - t0) / TCLK;
end process;
stim : process
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;
-- runs(1..8) are the start bit and the first seven data bits; runs(0)
-- is the idle fragment before the start edge and is not a bit.
impure function uniform_e (w : integer) return integer is
begin
for i in 1 to 8 loop
if runs_e(i) /= w then return 0; end if;
end loop;
return 1;
end function;
impure function uniform_f (w : integer) return integer is
begin
for i in 1 to 8 loop
if runs_f(i) /= w then return 0; end if;
end loop;
return 1;
end function;
impure function widths_e return string is
variable s : string(1 to 32) := (others => ' ');
variable p : integer := 1;
begin
for i in 1 to 8 loop
s(p to p+2) := " ";
if runs_e(i) >= 10 then
s(p+1 to p+2) := integer'image(runs_e(i));
else
s(p+2 to p+2) := integer'image(runs_e(i));
end if;
p := p + 3;
end loop;
return s;
end function;
impure function widths_f return string is
variable s : string(1 to 32) := (others => ' ');
variable p : integer := 1;
begin
for i in 1 to 8 loop
s(p to p+2) := " ";
if runs_f(i) >= 10 then
s(p+1 to p+2) := integer'image(runs_f(i));
else
s(p+2 to p+2) := integer'image(runs_f(i));
end if;
p := p + 3;
end loop;
return s;
end function;
procedure do_reset is
begin
rst_n <= '0'; rec <= '0'; cfg_div <= to_unsigned(DIV_A, 16);
cfg_wr_e <= '0'; cfg_wr_f <= '0';
tx_start_e <= '0'; tx_start_f <= '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;
-- Start both frames, optionally reprogramming the divider `at_bit`
-- bit-times in.
procedure run_frame (at_bit : integer) is -- -1 = no reconfiguration
begin
wait until falling_edge(clk);
tx_start_e <= '1'; tx_start_f <= '1'; rec <= '1';
wait until rising_edge(clk);
wait until falling_edge(clk);
tx_start_e <= '0'; tx_start_f <= '0';
if at_bit >= 0 then
for i in 0 to at_bit * DIV_A - 1 loop
wait until rising_edge(clk);
end loop;
wait until falling_edge(clk);
cfg_div <= to_unsigned(DIV_B, 16);
cfg_wr_e <= '1'; cfg_wr_f <= '1';
wait until rising_edge(clk);
wait until falling_edge(clk);
cfg_wr_e <= '0'; cfg_wr_f <= '0';
end if;
wait until busy_e = '0' and busy_f = '0';
wait until rising_edge(clk);
wait until falling_edge(clk); rec <= '0';
for i in 0 to 3 loop wait until rising_edge(clk); end loop;
end procedure;
begin
-- ---------------- T1: no reconfiguration, both variants -----------
do_reset;
run_frame(-1);
report "T1 undisturbed frame";
report " escaped bit widths:" & widths_e;
report " fixed bit widths:" & widths_f;
chk("T1 escaped frame is uniform", uniform_e(DIV_A), 1);
chk("T1 fixed frame is uniform", uniform_f(DIV_A), 1);
chk("T1 both took 160 clocks (escaped)", dur_e, 160);
chk("T1 both took 160 clocks (fixed)", dur_f, 160);
chk("T1 no mid-frame write flagged", to_integer(unsigned'("" & midwr_e)), 0);
-- ---------------- T2/T3: reprogram 4 bit-times into the frame -----
do_reset;
run_frame(4);
report "T2 divider rewritten from " & integer'image(DIV_A) &
" to " & integer'image(DIV_B) & ", four bit-times in";
report " escaped bit widths:" & widths_e;
report " fixed bit widths:" & widths_f;
report " escaped frame: " & integer'image(dur_e) &
" clocks fixed frame: " & integer'image(dur_f) & " clocks";
chk("T2 the escaped frame is MALFORMED", uniform_e(DIV_A), 0);
if dur_e < 160 then
chk("T2 the escaped frame finished early", 1, 1);
else
chk("T2 the escaped frame finished early", 0, 1);
end if;
chk("T2 the escape applied the write mid-frame", to_integer(n_mid_e), 1);
chk("T2 the detector fired", to_integer(unsigned'("" & midwr_e)), 1);
chk("T3 the fixed frame is well formed", uniform_f(DIV_A), 1);
chk("T3 the fixed frame took full length", dur_f, 160);
chk("T3 the write was deferred", to_integer(n_def_f), 1);
chk("T3 nothing was applied mid-frame", to_integer(n_mid_f), 0);
chk("T3 the detector still warns", to_integer(unsigned'("" & midwr_f)), 1);
-- ---------------- T4: the deferred write lands at the boundary ----
chk("T4 the fixed variant now runs at the new rate",
to_integer(div_f), DIV_B);
run_frame(-1);
report "T4 the next frame, after the deferred write";
report " fixed bit widths:" & widths_f;
chk("T4 the next frame is uniform at the new rate", uniform_f(DIV_B), 1);
chk("T4 and takes half as long", dur_f, 80);
-- ---------------- T5: reconfiguring while idle is always fine -----
do_reset;
wait until falling_edge(clk);
cfg_div <= to_unsigned(DIV_B, 16); cfg_wr_e <= '1'; cfg_wr_f <= '1';
wait until rising_edge(clk);
wait until falling_edge(clk); cfg_wr_e <= '0'; cfg_wr_f <= '0';
wait for 1 ns;
report "T5 written while idle : escaped div=" &
integer'image(to_integer(div_e)) & " fixed div=" &
integer'image(to_integer(div_f));
chk("T5 escaped applies it", to_integer(div_e), DIV_B);
chk("T5 fixed applies it too -- there is nothing to defer",
to_integer(div_f), DIV_B);
chk("T5 no mid-frame write (escaped)", to_integer(unsigned'("" & midwr_e)), 0);
chk("T5 no mid-frame write (fixed)", to_integer(unsigned'("" & midwr_f)), 0);
-- ---------------- T6: the detector is STICKY ----------------------
-- A mid-frame write raises the flag. A later, perfectly legal, idle
-- write must NOT erase the evidence -- a detector that forgets is a
-- detector nobody ever sees fire.
do_reset;
run_frame(4); -- the offence
wait for 1 ns;
chk("T6 the offence was flagged (escaped)",
to_integer(unsigned'("" & midwr_e)), 1);
chk("T6 the offence was flagged (fixed)",
to_integer(unsigned'("" & midwr_f)), 1);
wait until falling_edge(clk);
cfg_div <= to_unsigned(DIV_A, 16); cfg_wr_e <= '1'; cfg_wr_f <= '1';
wait until rising_edge(clk);
wait until falling_edge(clk); cfg_wr_e <= '0'; cfg_wr_f <= '0';
for i in 0 to 1 loop wait until rising_edge(clk); end loop;
wait for 1 ns;
report "T6 legal idle write after : midwr_e=" &
std_logic'image(midwr_e)(2) & " midwr_f=" &
std_logic'image(midwr_f)(2);
chk("T6 evidence survives (escaped)",
to_integer(unsigned'("" & midwr_e)), 1);
chk("T6 evidence survives (fixed)",
to_integer(unsigned'("" & midwr_f)), 1);
report "";
report "== " & integer'image(checks) & " checks, " &
integer'image(fails) & " failures ==";
if fails = 0 then
report " RESULT: ALL VHDL CFG-TX TESTS PASSED";
else
report " RESULT: " & integer'image(fails) & " FAILURE(S)" severity error;
end if;
sim_done <= true;
wait;
end process;
end architecture sim;Twenty-five checks per language. Both variants are instantiated and driven from the same stimulus, so every comparison is controlled rather than asserted.
T6 is there because of a surviving mutant — see §8.
8. Proving the Tests Can Fail
mutation checks failed verdict
----------------------------------------------------- ------------- -------
M7 make the deferred path behave like the escaped one 4 killed
M8 never apply the shadow at the frame boundary 3 killed
M9 make the mid-frame-write detector non-sticky 0 SURVIVEDM9 survived the original suite, and the reason was instructive. Every test wrote configuration mid-frame and then stopped. Nothing ever performed a later, legal, idle write — so the mutant, which sets the flag to tx_busy on every write rather than latching it high, behaved identically throughout. The stickiness that the comment explicitly claims was never tested.
T6 now injects the offence, then performs a perfectly legal idle write, and asserts the evidence survives it:
M9 make the mid-frame-write detector non-sticky (after T6) 2 killedThat matters beyond the mutation score. A detector that forgets is worse than no detector, because it reports clean — which is exactly the failure mode this whole chapter is about.
9. What the Plan Should Have Said
The escape was not caused by a missing test. It was caused by a missing axis. The plan enumerated configuration values and traffic patterns, and crossed them thoroughly. It never enumerated when configuration changes relative to traffic.
Adding that axis is cheap:
new dimension values
------------------ --------------------------------------------
config write timing idle · mid-start-bit · mid-data · mid-stop
· same clock as frame start
· same clock as frame endCrossed against the existing configuration dimensions, that is a modest number of additional bins, and it would have caught this in a week rather than in eight months.
The general lesson generalises past UARTs:
Continue learning
Related tutorials
- Related topic
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.
- 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.
Where this fits
Part of the UART curriculum.
