Skip to content
VLSI Mentor

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.

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

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

QuestionWhy it mattersWhat to write
Is the RX pin synchronised?it is asynchronous to your clock by definition; a raw pin into an FSM is a metastability pathtwo 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 comparisonkeep it registered and stable; see 18.3
Is TX registered at the pad?an unregistered output makes the bit period depend on routing delayregister the output in the IOB
A block diagram of an FPGA UART bring-up rig. The receive pin feeds a two-stage synchroniser before reaching the UART receiver, which is the only safe way to bring an asynchronous input into the clock domain. The receiver and the transmitter both connect to the self-test block, which drives test patterns into the transmitter and compares what returns from the receiver. The transmit output passes through an output register at the pad before leaving the device. A loopback path, shown returning from the transmit pin to the receive pin, closes the circuit during bring-up either through a wire on the board or inside the device. The self-test block produces a verdict and a separate signal reporting whether the pattern set it used was capable of detecting a bit-order fault.RX PINasynchronous2-FF SYNCfalse pathUART RXframe assemblySELF-TESTpattern setUART TXdividerOUT REGregisteredTX PINto the wirerawsafebytepatternbitoutloopback12

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

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

endmodule

SystemVerilog

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

endmodule

VHDL

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

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

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  bit order reversed         BIT_ORDER               1
  bit order reversed, NAIVE  PASS                    0

The 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 cycles
A comparison of two bring-up frames sent through a loopback that assembles bytes in the wrong bit order. In the upper pair the self-test sends fifty-five hexadecimal and receives A A hexadecimal, so the sent and received rows differ visibly and the fault is detected. In the lower pair the self-test sends A five hexadecimal and receives A five hexadecimal, because A five is unchanged by bit reversal, so the sent and received rows are identical and the same fault produces no evidence whatsoever. The verdict row reads bit order for the first case and pass for the second, and the order proven row reads one and zero respectively.the eight data bitsthe eight data bits0x55 mirrors to 0xAA — caught0x55 mirrors to 0xAA —caught0xA5 mirrors to itself — missed0xA5 mirrors to itself —missedintervalstartd0d1d2d3d4d5d6d7stopidlesent 55got AAsent A5got A5t0t1t2t3t4t5t6t7t8t9t10

order_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

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

endmodule

SystemVerilog

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

endmodule

VHDL

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

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

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

  1. Frequency-count the clock at the UART's input. Everything downstream is derived from it; a wrong clock invalidates every later measurement.
  2. 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 0xFE or 0xFF depending 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.
  3. 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.
  4. Transmit 0x55 continuously and scope the pin. No receiver involved. Measure the bit period directly with the instrument from Chapter 17.1 — 0x55 alternates, so every bit is its own run and the period is measurable from a single frame.
  5. 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.
  6. Close the loopback at the pad and run the self-test. Check order_proven_o, not just the verdict.
  7. 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.
  8. 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

Where this fits

Part of the UART curriculum.