Skip to content
VLSI Mentor

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:

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

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

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

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

endmodule

SystemVerilog

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

endmodule

VHDL

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

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

The 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 cycles
A timing trace of a transmitted frame during which the baud divider is reprogrammed. The first five intervals, the start bit and the first four data bits, are each sixteen clocks wide. A configuration write then lands, and the remaining intervals, the last four data bits and the stop bit, are each only eight clocks wide. The receiver at the far end synchronised on the start bit and is stepping at the original sixteen clock rate, so its sample points for the later bits fall progressively past the end of the intervals actually being transmitted. The row labelled receiver samples in bit shows ones for the early bits and zeros for the late ones, marking where the receiver has walked off the transmitted data.transmitted at the old ratetransmitted at the old ratetransmitted at the new ratetransmitted at the new ratedivider rewritten heredivider rewritten herestop sampled in the wrong placestop sampled in the wrongplaceintervalstartd0d1d2d3d4d5d6d7stopidlewidth161616161688888..tx linerx in bitt0t1t2t3t4t5t6t7t8t9t10

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

ObservationWhat it suggestedWhy that was wrong
Occasionalnoise, marginal signal integritythe rate was set by how often a timer callback coincided with a frame
Under loada FIFO or flow-control problemload only increased the number of frames, raising the collision odds
Single bytea bit flipthe whole tail of one frame was mis-sampled, which often decodes as one wrong byte
Not reproducibleenvironmentalperfectly 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.

A flowchart of the investigation that found the escape. It begins with a field report of occasional single byte corruption. The first question is whether the corruption is concentrated in the high bits of the byte, which it is, indicating a sampling or timing problem rather than noise. The next question is whether the link baud rate is misconfigured, and a static check of the divider register shows it is correct, which appears to rule out a baud fault and is where the investigation initially stalled. Reframing the question to ask whether the divider is correct at every instant rather than merely at the moment it is read leads to the decisive step, capturing the divider register together with the transmitter busy signal. That capture shows a write landing while the transmitter is busy, which identifies the escape, and the fix is to defer configuration writes to a frame boundary.noyesnoyesField report:occasional badbyteCorruption inthe HIGHbits?Noise — checkthe physicallayerDividerregistercorrect?A plain baudmisconfigurationCorrect WHEN READ isnot correct ALWAYSCapture the dividerWITH tx_busyA write landsmid-frame

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

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

endmodule

SystemVerilog

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

endmodule

VHDL

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

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

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  M9  make the mid-frame-write detector non-sticky (after T6)           2    killed

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  new dimension        values
  ------------------   --------------------------------------------
  config write timing  idle · mid-start-bit · mid-data · mid-stop
                       · same clock as frame start
                       · same clock as frame end

Crossed 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

Where this fits

Part of the UART curriculum.