UART · Module 18
Case Study: FPGA UART Implementation and Bring-Up
Divider arithmetic and clock choice, the constraints an FPGA UART actually needs, and a self-test in three HDLs that reports a diagnosis instead of pass or fail — including a board it correctly passes while reporting it could not have failed.
The UART is almost always the first thing that has to work on a new board, because it is how everything else will tell you what it is doing. That makes it the one block you cannot debug using the tools you would normally debug with. There is no console, because the console is what is broken.
This chapter is a worked bring-up: the arithmetic done before synthesis, the constraints that matter, and the self-test that runs at first light. The self-test is the interesting part, because it is built to answer a question most self-tests cannot — not "did this pass?" but "was this test capable of failing?"
1. The Arithmetic, Before Any RTL
Two numbers decide whether the link can work at all, and both are fixed before a line of RTL is written: the clock the UART runs on, and the divider derived from it.
f_clk MHz baud ideal div div error verdict
--------- ------ --------- ----- ------- --------
50 115200 434.03 434 -0.006% ok
50 9600 5208.33 5208 -0.006% ok
100 115200 868.06 868 -0.006% ok
27 115200 234.38 234 -0.160% ok
12 115200 104.17 104 -0.160% ok
48 921600 52.08 52 -0.160% okEvery row there is inside the 5.26% budget of Chapter 17.2, including the ones marked marginal — those are flagged because they consume more than a third of a budget that has to be shared with the other end, not because they fail alone.
The number that actually breaks links is not in this table, because it is not a rounding error:
2. The Constraints That Matter
An FPGA UART is small enough that timing closure is rarely the issue. Three constraint questions still decide whether it works:
| Question | Why it matters | What to write |
|---|---|---|
| Is the RX pin synchronised? | it is asynchronous to your clock by definition; a raw pin into an FSM is a metastability path | two flip-flops before any logic, and a false path onto the first |
| Is the divider constant or a register? | a register makes the divider a timing path into every counter comparison | keep it registered and stable; see 18.3 |
| Is TX registered at the pad? | an unregistered output makes the bit period depend on routing delay | register the output in the IOB |
The loopback is drawn dashed because it is temporary — a wire between two pins, or a mux inside the device. It exists only during bring-up, and what it proves depends entirely on where it is closed.
3. The Self-Test
The block below drives a pattern set through whichever loopback is closed and reports a diagnosis. A self-test that returns PASS or FAIL sends you to look at everything; one that returns STUCK_HIGH or BIT_ORDER sends you to one place.
Verilog
// ---------------------------------------------------------------------------
// uart_bringup_selftest -- the loopback self-test a board runs at first light.
//
// A self-test that reports PASS or FAIL is nearly useless on a bench: "FAIL"
// sends you to look at everything. This one drives a pattern set through a
// byte-level loopback, accumulates evidence across ALL patterns rather than
// stopping at the first mismatch, and reports WHICH fault the evidence fits.
//
// It also reports something most self-tests cannot: whether the pattern set it
// just ran was CAPABLE of detecting a bit-order fault. Bytes that are their own
// bit-mirror (0x00, 0xFF, 0xA5, 0x3C ...) return unchanged from a link that
// assembles bits backwards, so a suite built from them passes a broken board.
// Selecting the naive pattern set makes the block report order_proven_o = 0
// while still returning PASS, which is the honest answer and the reason the
// output exists.
// ---------------------------------------------------------------------------
module uart_bringup_selftest #(
parameter TIMEOUT = 2000 // clocks to wait for each echo
)(
input wire clk,
input wire rst_n,
input wire start_i,
input wire use_naive_i, // 1 = the palindrome-only pattern set
// byte-level loopback interface
output reg [7:0] tx_data_o,
output reg tx_valid_o,
input wire tx_ready_i,
input wire [7:0] rx_data_i,
input wire rx_valid_i,
output reg busy_o,
output reg done_o,
output reg [3:0] verdict_o,
output reg [2:0] stage_o, // pattern index currently under test
output reg [3:0] n_timeout_o,
output reg order_proven_o // the pattern set could detect a reversal
);
localparam [3:0] V_NONE = 4'd0, // not run yet
V_PASS = 4'd1,
V_NO_ECHO = 4'd2, // nothing came back at all
V_PARTIAL = 4'd3, // some echoes lost -- intermittent
V_STUCK_HI = 4'd4, // every byte read back as 0xFF
V_STUCK_LO = 4'd5, // every byte read back as 0x00
V_BIT_ORDER = 4'd6, // every byte came back mirrored
V_CORRUPT = 4'd7; // wrong, and not a clean mirror
localparam [2:0] S_IDLE = 3'd0, S_SEND = 3'd1, S_WAIT = 3'd2, S_NEXT = 3'd3;
function [7:0] bitrev;
input [7:0] b;
integer i;
begin
for (i = 0; i < 8; i = i + 1) bitrev[i] = b[7-i];
end
endfunction
// The two pattern sets. The full set alternates, so a reversal turns 0x55
// into 0xAA and cannot be missed. Every byte of the naive set is its own
// mirror, which is exactly why it must not be trusted.
function [7:0] pattern;
input [2:0] idx;
input naive;
begin
if (naive) begin
case (idx)
3'd0: pattern = 8'h00;
3'd1: pattern = 8'hFF;
3'd2: pattern = 8'hA5;
default: pattern = 8'h3C;
endcase
end else begin
case (idx)
3'd0: pattern = 8'h55;
3'd1: pattern = 8'hAA;
3'd2: pattern = 8'h00;
default: pattern = 8'hFF;
endcase
end
end
endfunction
reg [2:0] state;
reg [15:0] timer;
reg naive_q;
// evidence accumulated across the whole run
reg [7:0] and_all; // AND of every byte received
reg [7:0] or_all; // OR of every byte received
reg all_rev; // every echo was the mirror of what went out
reg any_mismatch; // at least one echo differed from what went out
reg any_echo; // at least one echo arrived
reg can_prove_order; // at least one pattern was not a palindrome
wire [7:0] cur_pat = pattern(stage_o, naive_q);
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= S_IDLE; tx_valid_o <= 1'b0; tx_data_o <= 8'd0;
busy_o <= 1'b0; done_o <= 1'b0; verdict_o <= V_NONE;
stage_o <= 3'd0; n_timeout_o <= 4'd0; order_proven_o <= 1'b0;
timer <= 16'd0; naive_q <= 1'b0;
and_all <= 8'hFF; or_all <= 8'h00;
all_rev <= 1'b1; any_mismatch <= 1'b0; any_echo <= 1'b0;
can_prove_order <= 1'b0;
end else begin
case (state)
S_IDLE: begin
if (start_i) begin
state <= S_SEND;
busy_o <= 1'b1;
done_o <= 1'b0;
verdict_o <= V_NONE;
stage_o <= 3'd0;
n_timeout_o <= 4'd0;
order_proven_o <= 1'b0;
naive_q <= use_naive_i;
and_all <= 8'hFF;
or_all <= 8'h00;
all_rev <= 1'b1;
any_mismatch <= 1'b0;
any_echo <= 1'b0;
can_prove_order <= 1'b0;
end
end
S_SEND: begin
tx_data_o <= cur_pat;
tx_valid_o <= 1'b1;
// A pattern can only prove bit order if it differs from
// its own mirror.
if (bitrev(cur_pat) != cur_pat) can_prove_order <= 1'b1;
if (tx_valid_o && tx_ready_i) begin
tx_valid_o <= 1'b0;
timer <= TIMEOUT[15:0];
state <= S_WAIT;
end
end
S_WAIT: begin
if (rx_valid_i) begin
any_echo <= 1'b1;
and_all <= and_all & rx_data_i;
or_all <= or_all | rx_data_i;
if (rx_data_i != cur_pat) any_mismatch <= 1'b1;
if (rx_data_i != bitrev(cur_pat)) all_rev <= 1'b0;
state <= S_NEXT;
end else if (timer == 16'd0) begin
n_timeout_o <= n_timeout_o + 4'd1;
all_rev <= 1'b0; // a lost echo proves nothing
state <= S_NEXT;
end else begin
timer <= timer - 16'd1;
end
end
S_NEXT: begin
if (stage_o == 3'd3) begin
// ---- weigh the evidence, once, at the end ----------
order_proven_o <= can_prove_order;
busy_o <= 1'b0;
done_o <= 1'b1;
state <= S_IDLE;
if (!any_echo) verdict_o <= V_NO_ECHO;
else if (n_timeout_o != 4'd0) verdict_o <= V_PARTIAL;
else if (and_all == 8'hFF) verdict_o <= V_STUCK_HI;
else if (or_all == 8'h00) verdict_o <= V_STUCK_LO;
else if (all_rev && any_mismatch) verdict_o <= V_BIT_ORDER;
else if (any_mismatch) verdict_o <= V_CORRUPT;
else verdict_o <= V_PASS;
end else begin
stage_o <= stage_o + 3'd1;
state <= S_SEND;
end
end
default: state <= S_IDLE;
endcase
end
end
endmoduleSystemVerilog
// ---------------------------------------------------------------------------
// uart_bringup_selftest -- the loopback self-test a board runs at first light.
//
// A self-test that reports PASS or FAIL is nearly useless on a bench: "FAIL"
// sends you to look at everything. This one drives a pattern set through a
// byte-level loopback, accumulates evidence across ALL patterns rather than
// stopping at the first mismatch, and reports WHICH fault the evidence fits.
//
// It also reports something most self-tests cannot: whether the pattern set it
// just ran was CAPABLE of detecting a bit-order fault. Bytes that are their own
// bit-mirror (0x00, 0xFF, 0xA5, 0x3C ...) return unchanged from a link that
// assembles bits backwards, so a suite built from them passes a broken board.
// Selecting the naive pattern set makes the block report order_proven_o = 0
// while still returning PASS, which is the honest answer and the reason the
// output exists.
// ---------------------------------------------------------------------------
module uart_bringup_selftest #(
parameter int TIMEOUT = 2000 // clocks to wait for each echo
)(
input logic clk,
input logic rst_n,
input logic start_i,
input logic use_naive_i, // 1 = the palindrome-only pattern set
// byte-level loopback interface
output logic [7:0] tx_data_o,
output logic tx_valid_o,
input logic tx_ready_i,
input logic [7:0] rx_data_i,
input logic rx_valid_i,
output logic busy_o,
output logic done_o,
output logic [3:0] verdict_o,
output logic [2:0] stage_o, // pattern index currently under test
output logic [3:0] n_timeout_o,
output logic order_proven_o // the pattern set could detect a reversal
);
localparam [3:0] V_NONE = 4'd0, // not run yet
V_PASS = 4'd1,
V_NO_ECHO = 4'd2, // nothing came back at all
V_PARTIAL = 4'd3, // some echoes lost -- intermittent
V_STUCK_HI = 4'd4, // every byte read back as 0xFF
V_STUCK_LO = 4'd5, // every byte read back as 0x00
V_BIT_ORDER = 4'd6, // every byte came back mirrored
V_CORRUPT = 4'd7; // wrong, and not a clean mirror
localparam [2:0] S_IDLE = 3'd0, S_SEND = 3'd1, S_WAIT = 3'd2, S_NEXT = 3'd3;
function automatic logic [7:0] bitrev(input logic [7:0] b);
int i;
begin
for (i = 0; i < 8; i = i + 1) bitrev[i] = b[7-i];
end
endfunction
// The two pattern sets. The full set alternates, so a reversal turns 0x55
// into 0xAA and cannot be missed. Every byte of the naive set is its own
// mirror, which is exactly why it must not be trusted.
function automatic logic [7:0] pattern(input logic [2:0] idx, input logic naive);
begin
if (naive) begin
case (idx)
3'd0: pattern = 8'h00;
3'd1: pattern = 8'hFF;
3'd2: pattern = 8'hA5;
default: pattern = 8'h3C;
endcase
end else begin
case (idx)
3'd0: pattern = 8'h55;
3'd1: pattern = 8'hAA;
3'd2: pattern = 8'h00;
default: pattern = 8'hFF;
endcase
end
end
endfunction
logic [2:0] state;
logic [15:0] timer;
logic naive_q;
// evidence accumulated across the whole run
logic [7:0] and_all; // AND of every byte received
logic [7:0] or_all; // OR of every byte received
logic all_rev; // every echo was the mirror of what went out
logic any_mismatch; // at least one echo differed from what went out
logic any_echo; // at least one echo arrived
logic can_prove_order; // at least one pattern was not a palindrome
wire [7:0] cur_pat = pattern(stage_o, naive_q);
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
state <= S_IDLE; tx_valid_o <= 1'b0; tx_data_o <= 8'd0;
busy_o <= 1'b0; done_o <= 1'b0; verdict_o <= V_NONE;
stage_o <= 3'd0; n_timeout_o <= 4'd0; order_proven_o <= 1'b0;
timer <= 16'd0; naive_q <= 1'b0;
and_all <= 8'hFF; or_all <= 8'h00;
all_rev <= 1'b1; any_mismatch <= 1'b0; any_echo <= 1'b0;
can_prove_order <= 1'b0;
end else begin
case (state)
S_IDLE: begin
if (start_i) begin
state <= S_SEND;
busy_o <= 1'b1;
done_o <= 1'b0;
verdict_o <= V_NONE;
stage_o <= 3'd0;
n_timeout_o <= 4'd0;
order_proven_o <= 1'b0;
naive_q <= use_naive_i;
and_all <= 8'hFF;
or_all <= 8'h00;
all_rev <= 1'b1;
any_mismatch <= 1'b0;
any_echo <= 1'b0;
can_prove_order <= 1'b0;
end
end
S_SEND: begin
tx_data_o <= cur_pat;
tx_valid_o <= 1'b1;
// A pattern can only prove bit order if it differs from
// its own mirror.
if (bitrev(cur_pat) != cur_pat) can_prove_order <= 1'b1;
if (tx_valid_o && tx_ready_i) begin
tx_valid_o <= 1'b0;
timer <= TIMEOUT[15:0];
state <= S_WAIT;
end
end
S_WAIT: begin
if (rx_valid_i) begin
any_echo <= 1'b1;
and_all <= and_all & rx_data_i;
or_all <= or_all | rx_data_i;
if (rx_data_i != cur_pat) any_mismatch <= 1'b1;
if (rx_data_i != bitrev(cur_pat)) all_rev <= 1'b0;
state <= S_NEXT;
end else if (timer == 16'd0) begin
n_timeout_o <= n_timeout_o + 4'd1;
all_rev <= 1'b0; // a lost echo proves nothing
state <= S_NEXT;
end else begin
timer <= timer - 16'd1;
end
end
S_NEXT: begin
if (stage_o == 3'd3) begin
// ---- weigh the evidence, once, at the end ----------
order_proven_o <= can_prove_order;
busy_o <= 1'b0;
done_o <= 1'b1;
state <= S_IDLE;
if (!any_echo) verdict_o <= V_NO_ECHO;
else if (n_timeout_o != 4'd0) verdict_o <= V_PARTIAL;
else if (and_all == 8'hFF) verdict_o <= V_STUCK_HI;
else if (or_all == 8'h00) verdict_o <= V_STUCK_LO;
else if (all_rev && any_mismatch) verdict_o <= V_BIT_ORDER;
else if (any_mismatch) verdict_o <= V_CORRUPT;
else verdict_o <= V_PASS;
end else begin
stage_o <= stage_o + 3'd1;
state <= S_SEND;
end
end
default: state <= S_IDLE;
endcase
end
end
endmoduleVHDL
-- ---------------------------------------------------------------------------
-- uart_bringup_selftest -- the loopback self-test a board runs at first light.
--
-- A self-test that reports PASS or FAIL is nearly useless on a bench: "FAIL"
-- sends you to look at everything. This one drives a pattern set through a
-- byte-level loopback, accumulates evidence across ALL patterns rather than
-- stopping at the first mismatch, and reports WHICH fault the evidence fits.
--
-- It also reports something most self-tests cannot: whether the pattern set it
-- just ran was CAPABLE of detecting a bit-order fault. Bytes that are their own
-- bit-mirror (0x00, 0xFF, 0xA5, 0x3C ...) return unchanged from a link that
-- assembles bits backwards, so a suite built from them passes a broken board.
-- Selecting the naive pattern set makes the block report order_proven_o = '0'
-- while still returning PASS, which is the honest answer and the reason the
-- output exists.
-- ---------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity uart_bringup_selftest is
generic (
TIMEOUT : natural := 2000 -- clocks to wait for each echo
);
port (
clk : in std_logic;
rst_n : in std_logic;
start_i : in std_logic;
use_naive_i : in std_logic; -- '1' = palindrome-only set
-- byte-level loopback interface
tx_data_o : out std_logic_vector(7 downto 0);
tx_valid_o : out std_logic;
tx_ready_i : in std_logic;
rx_data_i : in std_logic_vector(7 downto 0);
rx_valid_i : in std_logic;
busy_o : out std_logic;
done_o : out std_logic;
verdict_o : out unsigned(3 downto 0);
stage_o : out unsigned(2 downto 0);
n_timeout_o : out unsigned(3 downto 0);
order_proven_o : out std_logic -- the set could detect a reversal
);
end entity uart_bringup_selftest;
architecture rtl of uart_bringup_selftest is
constant V_NONE : unsigned(3 downto 0) := x"0"; -- not run yet
constant V_PASS : unsigned(3 downto 0) := x"1";
constant V_NO_ECHO : unsigned(3 downto 0) := x"2"; -- nothing came back
constant V_PARTIAL : unsigned(3 downto 0) := x"3"; -- some echoes lost
constant V_STUCK_HI : unsigned(3 downto 0) := x"4"; -- every byte 0xFF
constant V_STUCK_LO : unsigned(3 downto 0) := x"5"; -- every byte 0x00
constant V_BIT_ORDER : unsigned(3 downto 0) := x"6"; -- every byte mirrored
constant V_CORRUPT : unsigned(3 downto 0) := x"7"; -- wrong, not a mirror
type state_t is (S_IDLE, S_SEND, S_WAIT, S_NEXT);
function bitrev (b : std_logic_vector(7 downto 0))
return std_logic_vector is
variable r : std_logic_vector(7 downto 0);
begin
for i in 0 to 7 loop
r(i) := b(7-i);
end loop;
return r;
end function;
-- The two pattern sets. The full set alternates, so a reversal turns 0x55
-- into 0xAA and cannot be missed. Every byte of the naive set is its own
-- mirror, which is exactly why it must not be trusted.
function pattern (idx : unsigned(2 downto 0); naive : std_logic)
return std_logic_vector is
begin
if naive = '1' then
case to_integer(idx) is
when 0 => return x"00";
when 1 => return x"FF";
when 2 => return x"A5";
when others => return x"3C";
end case;
else
case to_integer(idx) is
when 0 => return x"55";
when 1 => return x"AA";
when 2 => return x"00";
when others => return x"FF";
end case;
end if;
end function;
signal state : state_t := S_IDLE;
signal timer : unsigned(15 downto 0) := (others => '0');
signal naive_q : std_logic := '0';
signal stage : unsigned(2 downto 0) := (others => '0');
-- evidence accumulated across the whole run
signal and_all : std_logic_vector(7 downto 0) := (others => '1');
signal or_all : std_logic_vector(7 downto 0) := (others => '0');
signal all_rev : std_logic := '1';
signal any_mismatch : std_logic := '0';
signal any_echo : std_logic := '0';
signal can_prove_order : std_logic := '0';
-- outputs mirrored internally: an entity may not read its own outputs
signal tx_data : std_logic_vector(7 downto 0) := (others => '0');
signal tx_valid : std_logic := '0';
signal busy : std_logic := '0';
signal done : std_logic := '0';
signal verdict : unsigned(3 downto 0) := V_NONE;
signal n_timeout : unsigned(3 downto 0) := (others => '0');
signal ord_prov : std_logic := '0';
signal cur_pat : std_logic_vector(7 downto 0);
begin
cur_pat <= pattern(stage, naive_q);
tx_data_o <= tx_data;
tx_valid_o <= tx_valid;
busy_o <= busy;
done_o <= done;
verdict_o <= verdict;
stage_o <= stage;
n_timeout_o <= n_timeout;
order_proven_o <= ord_prov;
process (clk, rst_n)
begin
if rst_n = '0' then
state <= S_IDLE; tx_valid <= '0'; tx_data <= (others => '0');
busy <= '0'; done <= '0'; verdict <= V_NONE;
stage <= (others => '0'); n_timeout <= (others => '0');
ord_prov <= '0'; timer <= (others => '0'); naive_q <= '0';
and_all <= (others => '1'); or_all <= (others => '0');
all_rev <= '1'; any_mismatch <= '0'; any_echo <= '0';
can_prove_order <= '0';
elsif rising_edge(clk) then
case state is
when S_IDLE =>
if start_i = '1' then
state <= S_SEND;
busy <= '1';
done <= '0';
verdict <= V_NONE;
stage <= (others => '0');
n_timeout <= (others => '0');
ord_prov <= '0';
naive_q <= use_naive_i;
and_all <= (others => '1');
or_all <= (others => '0');
all_rev <= '1';
any_mismatch <= '0';
any_echo <= '0';
can_prove_order <= '0';
end if;
when S_SEND =>
tx_data <= cur_pat;
tx_valid <= '1';
-- A pattern can only prove bit order if it differs from
-- its own mirror.
if bitrev(cur_pat) /= cur_pat then
can_prove_order <= '1';
end if;
if tx_valid = '1' and tx_ready_i = '1' then
tx_valid <= '0';
timer <= to_unsigned(TIMEOUT, 16);
state <= S_WAIT;
end if;
when S_WAIT =>
if rx_valid_i = '1' then
any_echo <= '1';
and_all <= and_all and rx_data_i;
or_all <= or_all or rx_data_i;
if rx_data_i /= cur_pat then
any_mismatch <= '1';
end if;
if rx_data_i /= bitrev(cur_pat) then
all_rev <= '0';
end if;
state <= S_NEXT;
elsif timer = 0 then
n_timeout <= n_timeout + 1;
all_rev <= '0'; -- a lost echo proves nothing
state <= S_NEXT;
else
timer <= timer - 1;
end if;
when S_NEXT =>
if stage = to_unsigned(3, 3) then
-- ---- weigh the evidence, once, at the end ----------
ord_prov <= can_prove_order;
busy <= '0';
done <= '1';
state <= S_IDLE;
if any_echo = '0' then
verdict <= V_NO_ECHO;
elsif n_timeout /= 0 then
verdict <= V_PARTIAL;
elsif and_all = x"FF" then
verdict <= V_STUCK_HI;
elsif or_all = x"00" then
verdict <= V_STUCK_LO;
elsif all_rev = '1' and any_mismatch = '1' then
verdict <= V_BIT_ORDER;
elsif any_mismatch = '1' then
verdict <= V_CORRUPT;
else
verdict <= V_PASS;
end if;
else
stage <= stage + 1;
state <= S_SEND;
end if;
end case;
end if;
end process;
end architecture rtl;Three design decisions in that listing are worth pulling out.
It runs every pattern, not just until the first mismatch. A test that stops at the first failure has one data point. This one has four, and the difference is what lets it distinguish a stuck line (every byte identical) from corruption (bytes wrong in varying ways) from a reversal (every byte a clean mirror).
The verdict is formed once, at the end, from accumulated evidence. and_all and or_all accumulate across every received byte; all_rev is a running conjunction; any_echo and n_timeout_o record what arrived. That is the same discipline as the classifier in Chapter 17.3 — a single frame cannot carry the answer.
It reports whether it was capable of failing. order_proven_o is the unusual output, and §5 is entirely about why it exists.
4. The Measured Diagnoses
Nine runs, each injecting one fault into the loopback channel:
loopback fault verdict order_proven timeouts
------------------------ ---------- ------------ --------
none, full pattern set PASS 1 0
bit order reversed BIT_ORDER 1 0
bit order reversed, NAIVE PASS 0 0
stuck at 1 STUCK_HIGH 1 0
stuck at 0 STUCK_LOW 1 0
no echo at all NO_ECHO 1 4
one echo lost PARTIAL 1 1
bit 3 inverted CORRUPT 1 0
none, NAIVE pattern set PASS 0 0NO_ECHO and PARTIAL are separated by the timeout count, and the distinction is worth the register. Four timeouts means the loop is open — nothing is coming back and the problem is structural. One timeout means the loop is intermittent, which points at a connector, a marginal level, or a baud error severe enough that only some frames survive. Those are different investigations.
5. The Row That Matters
Read the second and third rows together.
bit order reversed BIT_ORDER 1
bit order reversed, NAIVE PASS 0The board is identically broken in both. The loopback is assembling bytes backwards in each case. The only thing that changed is the pattern set.
The full set is 0x55, 0xAA, 0x00, 0xFF. The naive set is 0x00, 0xFF, 0xA5, 0x3C — and every one of those four is a bit-palindrome, unchanged by reversal. The self-test sends them, gets them back unchanged, and correctly reports PASS, because from its evidence nothing is wrong.
The same reversed loopback, under two pattern sets
11 cyclesorder_proven_o is what stops that PASS from being a lie. The block tracks whether any pattern it sent differed from its own mirror, and reports 0 when none did. A PASS with order_proven_o = 0 means: this ran clean, and it could not have run any other way.
6. The Testbench
The loopback is a channel model with an injectable fault, so each diagnosis can be produced in isolation and checked.
Verilog
`timescale 1ns/1ps
// ---------------------------------------------------------------------------
// Testbench for uart_bringup_selftest.
//
// A loopback channel model sits between the block's transmit and receive
// interfaces and can be given a fault. Each test injects one fault, runs the
// self-test, and asserts the verdict the evidence supports.
//
// T3 is the point of the chapter: the SAME bit-order fault as T2, probed with
// a pattern set made entirely of bit-palindromes. The board is just as broken
// and the self-test returns PASS -- while reporting order_proven_o = 0, which
// is the only honest thing it can say about a suite that could not have failed.
// ---------------------------------------------------------------------------
module tb_uart_bringup_selftest;
localparam TIMEOUT = 200;
localparam ECHO_LAT = 20;
localparam [3:0] V_NONE=4'd0, V_PASS=4'd1, V_NO_ECHO=4'd2, V_PARTIAL=4'd3,
V_STUCK_HI=4'd4, V_STUCK_LO=4'd5, V_BIT_ORDER=4'd6,
V_CORRUPT=4'd7;
// loopback fault injection
localparam [2:0] F_NONE=3'd0, F_REVERSE=3'd1, F_STUCK_HI=3'd2,
F_STUCK_LO=3'd3, F_DEAD=3'd4, F_INTERMITTENT=3'd5,
F_CORRUPT=3'd6;
reg clk = 1'b0;
reg rst_n = 1'b0;
reg start = 1'b0;
reg use_naive = 1'b0;
reg [2:0] fault = F_NONE;
wire [7:0] tx_data;
wire tx_valid;
reg tx_ready = 1'b1;
reg [7:0] rx_data = 8'd0;
reg rx_valid = 1'b0;
wire busy, done, order_proven;
wire [3:0] verdict, n_timeout;
wire [2:0] stage;
integer checks = 0;
integer fails = 0;
always #5 clk = ~clk;
uart_bringup_selftest #(.TIMEOUT(TIMEOUT)) dut (
.clk(clk), .rst_n(rst_n), .start_i(start), .use_naive_i(use_naive),
.tx_data_o(tx_data), .tx_valid_o(tx_valid), .tx_ready_i(tx_ready),
.rx_data_i(rx_data), .rx_valid_i(rx_valid),
.busy_o(busy), .done_o(done), .verdict_o(verdict),
.stage_o(stage), .n_timeout_o(n_timeout), .order_proven_o(order_proven));
function [7:0] bitrev;
input [7:0] b;
integer i;
begin
for (i = 0; i < 8; i = i + 1) bitrev[i] = b[7-i];
end
endfunction
function [7:0] faulted;
input [7:0] b;
begin
case (fault)
F_REVERSE: faulted = bitrev(b);
F_STUCK_HI: faulted = 8'hFF;
F_STUCK_LO: faulted = 8'h00;
F_CORRUPT: faulted = b ^ 8'h08;
default: faulted = b;
endcase
end
endfunction
function [12*8:1] vname;
input [3:0] v;
begin
case (v)
V_PASS: vname = "PASS";
V_NO_ECHO: vname = "NO_ECHO";
V_PARTIAL: vname = "PARTIAL";
V_STUCK_HI: vname = "STUCK_HIGH";
V_STUCK_LO: vname = "STUCK_LOW";
V_BIT_ORDER: vname = "BIT_ORDER";
V_CORRUPT: vname = "CORRUPT";
default: vname = "NONE";
endcase
end
endfunction
// ---- the loopback channel model ----
reg [7:0] pend_data;
reg pending = 1'b0;
reg [15:0] lat_cnt = 16'd0;
reg [3:0] echo_idx = 4'd0;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
rx_valid <= 1'b0; pending <= 1'b0; lat_cnt <= 16'd0;
echo_idx <= 4'd0; rx_data <= 8'd0;
end else begin
rx_valid <= 1'b0;
if (tx_valid && tx_ready) begin
// F_DEAD echoes nothing; F_INTERMITTENT drops the second byte
if (fault != F_DEAD && !(fault == F_INTERMITTENT && echo_idx == 4'd1)) begin
pend_data <= faulted(tx_data);
pending <= 1'b1;
lat_cnt <= ECHO_LAT;
end
echo_idx <= echo_idx + 4'd1;
end
if (pending) begin
if (lat_cnt == 16'd0) begin
rx_data <= pend_data;
rx_valid <= 1'b1;
pending <= 1'b0;
end else begin
lat_cnt <= lat_cnt - 16'd1;
end
end
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
task run_test;
input [2:0] f;
input naive;
begin
rst_n = 1'b0;
@(negedge clk); fault = f; use_naive = naive;
repeat (3) @(posedge clk);
@(negedge clk); rst_n = 1'b1;
repeat (2) @(posedge clk);
@(negedge clk); start = 1'b1;
@(posedge clk);
@(negedge clk); start = 1'b0;
wait (done == 1'b1);
@(posedge clk); #1;
end
endtask
initial begin
// ---------------- T1: a healthy board, full pattern set -----------
run_test(F_NONE, 1'b0);
$display("T1 healthy, full set : verdict=%0s order_proven=%0b",
vname(verdict), order_proven);
chk("T1 verdict PASS", verdict, V_PASS);
chk("T1 bit order was proven", order_proven, 1);
chk("T1 no timeouts", n_timeout, 0);
// ---------------- T2: bit order reversed, full pattern set --------
run_test(F_REVERSE, 1'b0);
$display("T2 reversed, full set : verdict=%0s order_proven=%0b",
vname(verdict), order_proven);
chk("T2 verdict BIT_ORDER", verdict, V_BIT_ORDER);
chk("T2 bit order was proven", order_proven, 1);
// ---------------- T3: the SAME fault, naive pattern set -----------
// 0x00, 0xFF, 0xA5 and 0x3C are all their own bit-mirror. The board is
// exactly as broken as in T2 and the self-test cannot see it.
run_test(F_REVERSE, 1'b1);
$display("T3 reversed, NAIVE set : verdict=%0s order_proven=%0b <-- broken board, clean test",
vname(verdict), order_proven);
chk("T3 the broken board PASSES", verdict, V_PASS);
chk("T3 but bit order was NOT proven", order_proven, 0);
// ---------------- T4/T5: a stuck line -----------------------------
run_test(F_STUCK_HI, 1'b0);
$display("T4 stuck high : verdict=%0s", vname(verdict));
chk("T4 verdict STUCK_HIGH", verdict, V_STUCK_HI);
run_test(F_STUCK_LO, 1'b0);
$display("T5 stuck low : verdict=%0s", vname(verdict));
chk("T5 verdict STUCK_LOW", verdict, V_STUCK_LO);
// ---------------- T6: nothing comes back at all -------------------
run_test(F_DEAD, 1'b0);
$display("T6 dead link : verdict=%0s timeouts=%0d",
vname(verdict), n_timeout);
chk("T6 verdict NO_ECHO", verdict, V_NO_ECHO);
chk("T6 every pattern timed out", n_timeout, 4);
// ---------------- T7: one echo lost -------------------------------
run_test(F_INTERMITTENT, 1'b0);
$display("T7 one echo lost : verdict=%0s timeouts=%0d",
vname(verdict), n_timeout);
chk("T7 verdict PARTIAL", verdict, V_PARTIAL);
chk("T7 exactly one timeout", n_timeout, 1);
// ---------------- T8: corruption that is not a mirror -------------
run_test(F_CORRUPT, 1'b0);
$display("T8 bit 3 corrupted : verdict=%0s", vname(verdict));
chk("T8 verdict CORRUPT", verdict, V_CORRUPT);
// ---------------- T9: healthy board, naive set --------------------
// PASS is correct here. order_proven_o = 0 still warns that the run
// says nothing about bit order.
run_test(F_NONE, 1'b1);
$display("T9 healthy, NAIVE set : verdict=%0s order_proven=%0b",
vname(verdict), order_proven);
chk("T9 verdict PASS", verdict, V_PASS);
chk("T9 bit order still not proven", order_proven, 0);
$display("");
$display("== %0d checks, %0d failures ==", checks, fails);
if (fails == 0) $display(" RESULT: ALL VERILOG BRINGUP-SELFTEST TESTS PASSED");
else $display(" RESULT: %0d FAILURE(S)", fails);
$finish;
end
endmoduleSystemVerilog
`timescale 1ns/1ps
// ---------------------------------------------------------------------------
// Testbench for uart_bringup_selftest.
//
// A loopback channel model sits between the block's transmit and receive
// interfaces and can be given a fault. Each test injects one fault, runs the
// self-test, and asserts the verdict the evidence supports.
//
// T3 is the point of the chapter: the SAME bit-order fault as T2, probed with
// a pattern set made entirely of bit-palindromes. The board is just as broken
// and the self-test returns PASS -- while reporting order_proven_o = 0, which
// is the only honest thing it can say about a suite that could not have failed.
// ---------------------------------------------------------------------------
module tb_uart_bringup_selftest;
localparam TIMEOUT = 200;
localparam ECHO_LAT = 20;
localparam [3:0] V_NONE=4'd0, V_PASS=4'd1, V_NO_ECHO=4'd2, V_PARTIAL=4'd3,
V_STUCK_HI=4'd4, V_STUCK_LO=4'd5, V_BIT_ORDER=4'd6,
V_CORRUPT=4'd7;
// loopback fault injection
localparam [2:0] F_NONE=3'd0, F_REVERSE=3'd1, F_STUCK_HI=3'd2,
F_STUCK_LO=3'd3, F_DEAD=3'd4, F_INTERMITTENT=3'd5,
F_CORRUPT=3'd6;
logic clk = 1'b0;
logic rst_n = 1'b0;
logic start = 1'b0;
logic use_naive = 1'b0;
logic [2:0] fault = F_NONE;
logic [7:0] tx_data;
logic tx_valid;
logic tx_ready = 1'b1;
logic [7:0] rx_data = 8'd0;
logic rx_valid = 1'b0;
logic busy, done, order_proven;
logic [3:0] verdict, n_timeout;
logic [2:0] stage;
integer checks = 0;
integer fails = 0;
always #5 clk = ~clk;
uart_bringup_selftest #(.TIMEOUT(TIMEOUT)) dut (
.clk(clk), .rst_n(rst_n), .start_i(start), .use_naive_i(use_naive),
.tx_data_o(tx_data), .tx_valid_o(tx_valid), .tx_ready_i(tx_ready),
.rx_data_i(rx_data), .rx_valid_i(rx_valid),
.busy_o(busy), .done_o(done), .verdict_o(verdict),
.stage_o(stage), .n_timeout_o(n_timeout), .order_proven_o(order_proven));
function automatic logic [7:0] bitrev(input logic [7:0] b);
int i;
begin
for (i = 0; i < 8; i = i + 1) bitrev[i] = b[7-i];
end
endfunction
function automatic logic [7:0] faulted(input logic [7:0] b);
begin
case (fault)
F_REVERSE: faulted = bitrev(b);
F_STUCK_HI: faulted = 8'hFF;
F_STUCK_LO: faulted = 8'h00;
F_CORRUPT: faulted = b ^ 8'h08;
default: faulted = b;
endcase
end
endfunction
function automatic string vname(input logic [3:0] v);
begin
case (v)
V_PASS: vname = "PASS";
V_NO_ECHO: vname = "NO_ECHO";
V_PARTIAL: vname = "PARTIAL";
V_STUCK_HI: vname = "STUCK_HIGH";
V_STUCK_LO: vname = "STUCK_LOW";
V_BIT_ORDER: vname = "BIT_ORDER";
V_CORRUPT: vname = "CORRUPT";
default: vname = "NONE";
endcase
end
endfunction
// ---- the loopback channel model ----
logic [7:0] pend_data;
logic pending = 1'b0;
logic [15:0] lat_cnt = 16'd0;
logic [3:0] echo_idx = 4'd0;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
rx_valid <= 1'b0; pending <= 1'b0; lat_cnt <= 16'd0;
echo_idx <= 4'd0; rx_data <= 8'd0;
end else begin
rx_valid <= 1'b0;
if (tx_valid && tx_ready) begin
// F_DEAD echoes nothing; F_INTERMITTENT drops the second byte
if (fault != F_DEAD && !(fault == F_INTERMITTENT && echo_idx == 4'd1)) begin
pend_data <= faulted(tx_data);
pending <= 1'b1;
lat_cnt <= ECHO_LAT;
end
echo_idx <= echo_idx + 4'd1;
end
if (pending) begin
if (lat_cnt == 16'd0) begin
rx_data <= pend_data;
rx_valid <= 1'b1;
pending <= 1'b0;
end else begin
lat_cnt <= lat_cnt - 16'd1;
end
end
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
task automatic run_test(input logic [2:0] f, input logic naive);
begin
rst_n = 1'b0;
@(negedge clk); fault = f; use_naive = naive;
repeat (3) @(posedge clk);
@(negedge clk); rst_n = 1'b1;
repeat (2) @(posedge clk);
@(negedge clk); start = 1'b1;
@(posedge clk);
@(negedge clk); start = 1'b0;
wait (done == 1'b1);
@(posedge clk); #1;
end
endtask
initial begin
// ---------------- T1: a healthy board, full pattern set -----------
run_test(F_NONE, 1'b0);
$display("T1 healthy, full set : verdict=%0s order_proven=%0b",
vname(verdict), order_proven);
chk("T1 verdict PASS", verdict, V_PASS);
chk("T1 bit order was proven", order_proven, 1);
chk("T1 no timeouts", n_timeout, 0);
// ---------------- T2: bit order reversed, full pattern set --------
run_test(F_REVERSE, 1'b0);
$display("T2 reversed, full set : verdict=%0s order_proven=%0b",
vname(verdict), order_proven);
chk("T2 verdict BIT_ORDER", verdict, V_BIT_ORDER);
chk("T2 bit order was proven", order_proven, 1);
// ---------------- T3: the SAME fault, naive pattern set -----------
// 0x00, 0xFF, 0xA5 and 0x3C are all their own bit-mirror. The board is
// exactly as broken as in T2 and the self-test cannot see it.
run_test(F_REVERSE, 1'b1);
$display("T3 reversed, NAIVE set : verdict=%0s order_proven=%0b <-- broken board, clean test",
vname(verdict), order_proven);
chk("T3 the broken board PASSES", verdict, V_PASS);
chk("T3 but bit order was NOT proven", order_proven, 0);
// ---------------- T4/T5: a stuck line -----------------------------
run_test(F_STUCK_HI, 1'b0);
$display("T4 stuck high : verdict=%0s", vname(verdict));
chk("T4 verdict STUCK_HIGH", verdict, V_STUCK_HI);
run_test(F_STUCK_LO, 1'b0);
$display("T5 stuck low : verdict=%0s", vname(verdict));
chk("T5 verdict STUCK_LOW", verdict, V_STUCK_LO);
// ---------------- T6: nothing comes back at all -------------------
run_test(F_DEAD, 1'b0);
$display("T6 dead link : verdict=%0s timeouts=%0d",
vname(verdict), n_timeout);
chk("T6 verdict NO_ECHO", verdict, V_NO_ECHO);
chk("T6 every pattern timed out", n_timeout, 4);
// ---------------- T7: one echo lost -------------------------------
run_test(F_INTERMITTENT, 1'b0);
$display("T7 one echo lost : verdict=%0s timeouts=%0d",
vname(verdict), n_timeout);
chk("T7 verdict PARTIAL", verdict, V_PARTIAL);
chk("T7 exactly one timeout", n_timeout, 1);
// ---------------- T8: corruption that is not a mirror -------------
run_test(F_CORRUPT, 1'b0);
$display("T8 bit 3 corrupted : verdict=%0s", vname(verdict));
chk("T8 verdict CORRUPT", verdict, V_CORRUPT);
// ---------------- T9: healthy board, naive set --------------------
// PASS is correct here. order_proven_o = 0 still warns that the run
// says nothing about bit order.
run_test(F_NONE, 1'b1);
$display("T9 healthy, NAIVE set : verdict=%0s order_proven=%0b",
vname(verdict), order_proven);
chk("T9 verdict PASS", verdict, V_PASS);
chk("T9 bit order still not proven", order_proven, 0);
$display("");
$display("== %0d checks, %0d failures ==", checks, fails);
if (fails == 0) $display(" RESULT: ALL SYSTEMVERILOG BRINGUP-SELFTEST TESTS PASSED");
else $display(" RESULT: %0d FAILURE(S)", fails);
$finish;
end
endmoduleVHDL
-- ---------------------------------------------------------------------------
-- Testbench for uart_bringup_selftest.
--
-- A loopback channel model sits between the block's transmit and receive
-- interfaces and can be given a fault. Each test injects one fault, runs the
-- self-test, and asserts the verdict the evidence supports.
--
-- T3 is the point of the chapter: the SAME bit-order fault as T2, probed with
-- a pattern set made entirely of bit-palindromes. The board is just as broken
-- and the self-test returns PASS -- while reporting order_proven_o = '0', which
-- is the only honest thing it can say about a suite that could not have failed.
-- ---------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity tb_uart_bringup_selftest is
end entity tb_uart_bringup_selftest;
architecture sim of tb_uart_bringup_selftest is
constant TIMEOUT : natural := 200;
constant ECHO_LAT : natural := 20;
constant TCLK : time := 10 ns;
constant V_PASS : unsigned(3 downto 0) := x"1";
constant V_NO_ECHO : unsigned(3 downto 0) := x"2";
constant V_PARTIAL : unsigned(3 downto 0) := x"3";
constant V_STUCK_HI : unsigned(3 downto 0) := x"4";
constant V_STUCK_LO : unsigned(3 downto 0) := x"5";
constant V_BIT_ORDER : unsigned(3 downto 0) := x"6";
constant V_CORRUPT : unsigned(3 downto 0) := x"7";
-- loopback fault injection
constant F_NONE : integer := 0;
constant F_REVERSE : integer := 1;
constant F_STUCK_HI : integer := 2;
constant F_STUCK_LO : integer := 3;
constant F_DEAD : integer := 4;
constant F_INTERMITTENT : integer := 5;
constant F_CORRUPT : integer := 6;
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal start : std_logic := '0';
signal use_naive : std_logic := '0';
signal fault : integer := F_NONE;
signal sim_done : boolean := false;
signal tx_data : std_logic_vector(7 downto 0);
signal tx_valid : std_logic;
signal tx_ready : std_logic := '1';
signal rx_data : std_logic_vector(7 downto 0) := (others => '0');
signal rx_valid : std_logic := '0';
signal busy, done, order_proven : std_logic;
signal verdict, n_timeout : unsigned(3 downto 0);
signal stage : unsigned(2 downto 0);
function bitrev (b : std_logic_vector(7 downto 0))
return std_logic_vector is
variable r : std_logic_vector(7 downto 0);
begin
for i in 0 to 7 loop
r(i) := b(7-i);
end loop;
return r;
end function;
function vname (v : unsigned(3 downto 0)) return string is
begin
case to_integer(v) is
when 1 => return "PASS";
when 2 => return "NO_ECHO";
when 3 => return "PARTIAL";
when 4 => return "STUCK_HIGH";
when 5 => return "STUCK_LOW";
when 6 => return "BIT_ORDER";
when 7 => return "CORRUPT";
when others => return "NONE";
end case;
end function;
begin
clk <= '0' when sim_done else not clk after TCLK/2;
dut : entity work.uart_bringup_selftest
generic map (TIMEOUT => TIMEOUT)
port map (clk => clk, rst_n => rst_n, start_i => start,
use_naive_i => use_naive,
tx_data_o => tx_data, tx_valid_o => tx_valid,
tx_ready_i => tx_ready, rx_data_i => rx_data,
rx_valid_i => rx_valid,
busy_o => busy, done_o => done, verdict_o => verdict,
stage_o => stage, n_timeout_o => n_timeout,
order_proven_o => order_proven);
-- ---- the loopback channel model ----
channel : process (clk, rst_n)
variable pend_data : std_logic_vector(7 downto 0) := (others => '0');
variable pending : boolean := false;
variable lat_cnt : integer := 0;
variable echo_idx : integer := 0;
variable faulted : std_logic_vector(7 downto 0);
begin
if rst_n = '0' then
rx_valid <= '0'; pending := false; lat_cnt := 0;
echo_idx := 0; rx_data <= (others => '0');
elsif rising_edge(clk) then
rx_valid <= '0';
if tx_valid = '1' and tx_ready = '1' then
case fault is
when F_REVERSE => faulted := bitrev(tx_data);
when F_STUCK_HI => faulted := x"FF";
when F_STUCK_LO => faulted := x"00";
when F_CORRUPT => faulted := tx_data xor x"08";
when others => faulted := tx_data;
end case;
-- F_DEAD echoes nothing; F_INTERMITTENT drops the second byte
if fault /= F_DEAD and
not (fault = F_INTERMITTENT and echo_idx = 1) then
pend_data := faulted;
pending := true;
lat_cnt := ECHO_LAT;
end if;
echo_idx := echo_idx + 1;
end if;
if pending then
if lat_cnt = 0 then
rx_data <= pend_data;
rx_valid <= '1';
pending := false;
else
lat_cnt := lat_cnt - 1;
end if;
end if;
end if;
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;
procedure run_test (f : integer; naive : std_logic) is
begin
rst_n <= '0';
wait until falling_edge(clk);
fault <= f; use_naive <= naive;
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;
wait until falling_edge(clk); start <= '1';
wait until rising_edge(clk);
wait until falling_edge(clk); start <= '0';
wait until done = '1';
wait until rising_edge(clk);
wait for 1 ns;
end procedure;
begin
-- ---------------- T1: a healthy board, full pattern set -----------
run_test(F_NONE, '0');
report "T1 healthy, full set : verdict=" & vname(verdict) &
" order_proven=" & std_logic'image(order_proven)(2);
chk("T1 verdict PASS", to_integer(verdict), to_integer(V_PASS));
chk("T1 bit order was proven", to_integer(unsigned'("" & order_proven)), 1);
chk("T1 no timeouts", to_integer(n_timeout), 0);
-- ---------------- T2: bit order reversed, full pattern set --------
run_test(F_REVERSE, '0');
report "T2 reversed, full set : verdict=" & vname(verdict) &
" order_proven=" & std_logic'image(order_proven)(2);
chk("T2 verdict BIT_ORDER", to_integer(verdict), to_integer(V_BIT_ORDER));
chk("T2 bit order was proven", to_integer(unsigned'("" & order_proven)), 1);
-- ---------------- T3: the SAME fault, naive pattern set -----------
-- 0x00, 0xFF, 0xA5 and 0x3C are all their own bit-mirror. The board is
-- exactly as broken as in T2 and the self-test cannot see it.
run_test(F_REVERSE, '1');
report "T3 reversed, NAIVE set : verdict=" & vname(verdict) &
" order_proven=" & std_logic'image(order_proven)(2) &
" <-- broken board, clean test";
chk("T3 the broken board PASSES", to_integer(verdict), to_integer(V_PASS));
chk("T3 but bit order was NOT proven", to_integer(unsigned'("" & order_proven)), 0);
-- ---------------- T4/T5: a stuck line -----------------------------
run_test(F_STUCK_HI, '0');
report "T4 stuck high : verdict=" & vname(verdict);
chk("T4 verdict STUCK_HIGH", to_integer(verdict), to_integer(V_STUCK_HI));
run_test(F_STUCK_LO, '0');
report "T5 stuck low : verdict=" & vname(verdict);
chk("T5 verdict STUCK_LOW", to_integer(verdict), to_integer(V_STUCK_LO));
-- ---------------- T6: nothing comes back at all -------------------
run_test(F_DEAD, '0');
report "T6 dead link : verdict=" & vname(verdict) &
" timeouts=" & integer'image(to_integer(n_timeout));
chk("T6 verdict NO_ECHO", to_integer(verdict), to_integer(V_NO_ECHO));
chk("T6 every pattern timed out", to_integer(n_timeout), 4);
-- ---------------- T7: one echo lost -------------------------------
run_test(F_INTERMITTENT, '0');
report "T7 one echo lost : verdict=" & vname(verdict) &
" timeouts=" & integer'image(to_integer(n_timeout));
chk("T7 verdict PARTIAL", to_integer(verdict), to_integer(V_PARTIAL));
chk("T7 exactly one timeout", to_integer(n_timeout), 1);
-- ---------------- T8: corruption that is not a mirror -------------
run_test(F_CORRUPT, '0');
report "T8 bit 3 corrupted : verdict=" & vname(verdict);
chk("T8 verdict CORRUPT", to_integer(verdict), to_integer(V_CORRUPT));
-- ---------------- T9: healthy board, naive set --------------------
-- PASS is correct here. order_proven_o = '0' still warns that the run
-- says nothing about bit order.
run_test(F_NONE, '1');
report "T9 healthy, NAIVE set : verdict=" & vname(verdict) &
" order_proven=" & std_logic'image(order_proven)(2);
chk("T9 verdict PASS", to_integer(verdict), to_integer(V_PASS));
chk("T9 bit order still not proven", to_integer(unsigned'("" & order_proven)), 0);
report "";
report "== " & integer'image(checks) & " checks, " &
integer'image(fails) & " failures ==";
if fails = 0 then
report " RESULT: ALL VHDL BRINGUP-SELFTEST TESTS PASSED";
else
report " RESULT: " & integer'image(fails) & " FAILURE(S)" severity error;
end if;
sim_done <= true;
wait;
end process;
end architecture sim;Sixteen checks per language, identical verdicts and identical simulation end times across Verilog, SystemVerilog and VHDL.
7. Proving the Tests Can Fail
mutation checks failed verdict
--------------------------------------------------- ------------- -------
M1 drop "and it actually differed" from the reversal 2 killed
M2 claim every pattern set can prove bit order 2 killed
M3 remove the intermittent-link verdict 1 killedM1 is subtle and worth reading twice. Without the any_mismatch term, a healthy board running the naive pattern set reports BIT_ORDER: every byte comes back equal to its own mirror, because every byte is its own mirror. The qualifier is what separates "arrived mirrored" from "arrived unchanged, and unchanged happens to equal mirrored".
M2 removes order_proven_o's only reason to exist, and is killed by the two tests that assert a naive run reports 0.
8. First Light, In Order
The bring-up sequence, with the reason each step precedes the next:
- Frequency-count the clock at the UART's input. Everything downstream is derived from it; a wrong clock invalidates every later measurement.
- Confirm the device is out of reset, and that release was clean. A UART held in reset drives an idle line and answers nothing — indistinguishable at the pin from a dead transmitter, a wrong divider, or an unconfigured far end. Check the release itself too: Chapter 12.4 shows that a reset released asynchronously with respect to the baud tick corrupts the byte in flight — it arrives as
0xFEor0xFFdepending on exactly when release landed — and always with no error flag. A first byte that arrives wrong but unflagged after every reset is therefore a reset-release problem, not a framing one. - Check the idle level. With nothing transmitting, TX must sit at MARK. An idle-SPACE output means an inverted transceiver, and the far end is seeing a permanent break.
- Transmit
0x55continuously and scope the pin. No receiver involved. Measure the bit period directly with the instrument from Chapter 17.1 —0x55alternates, so every bit is its own run and the period is measurable from a single frame. - Compare the measured period against the intended baud. A mismatch here is a clock or divider fault and stops the investigation going any further afield.
- Close the loopback at the pad and run the self-test. Check
order_proven_o, not just the verdict. - Open the loopback and connect the far end. Read the candidate counter from Chapter 17.4: zero candidates means nothing is arriving and the receiver is not the problem.
- Then apply load, and watch the high-water mark from Chapter 17.5.
Steps 1 through 5 involve no receiver at all. That is deliberate: a bidirectional link has two ends that can each be wrong, and a test that exercises both at once cannot tell you which. Proving the transmitter alone halves the search space before the receiver is ever involved.
Continue learning
Related tutorials
- Related topic
FPGA Debug, ILA Capture and Board Bring-Up
A capture buffer whose trigger schedules the stop rather than the start, the off-by-one in the ring unwrap that only unique probe data exposes, and a bring-up sequence that orders the measurements so each one is interpretable.
- Related topic
Missing Start Bits, False Starts and Noise
A naive start detector and a majority-vote qualifier racing on one wire, a rejection boundary measured rather than assumed, and the counter that separates a transmitter that never sent from a receiver that never listened.
- Related topic
Case Study: SoC Boot and Debug Console UART
The 16550 interrupt identification register in three HDLs, the one extra register read that permanently hangs a console, and the counter imbalance that turns a silent board into a measurement.
- Related topic
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.
Where this fits
Part of the UART curriculum.
