Skip to content
VLSI Mentor

UART · Module 17

Baud Mismatch and Sampling-Error Signatures

Why sampling error accumulates across a frame and corrupts the high bits first, the arithmetic that fixes the tolerance at 5.26 percent, and the measured drift table for five receiver dividers sharing one wire.

A UART has no clock line. The receiver finds one edge — the start bit — and from then on it is navigating by dead reckoning, stepping forward by its own idea of a bit period and hoping it stays inside the transmitter's. Every bit it samples is further from the last known-good reference than the one before.

That is why a baud mismatch has such a specific signature, and why the signature is so often misread. The corruption is not random and it is not uniform: it concentrates in the high bits of the byte, it depends on the payload, and it can leave some bytes completely intact while destroying others. This chapter measures all three effects with five receivers running on one wire.

1. The Lever Arm

A mid-bit-sampling receiver that has detected a falling edge at time zero samples data bit k at

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    t(k) = DIV_RX / 2  +  (k + 1) * DIV_RX

and the stop bit at DIV_RX/2 + 9 * DIV_RX. In bit periods, that is 1.5, 2.5, 3.5 … up to 8.5 for d7 and 9.5 for the stop bit.

Now suppose the receiver's bit period is wrong by a fraction e. Its sample for d7 lands 8.5 * e bit periods away from where it should, and the stop sample lands 9.5 * e away. The error the receiver makes is not e — it is e multiplied by how far into the frame it has walked.

A sample stays inside its intended bit as long as it has not drifted by half a bit period. So the constraint is

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    9.5 * |e|  <  0.5        =>        |e|  <  5.26%

That is the budget for this receiver, at 8N1, and it is shared between both ends. It is also why UART framing is what it is: the start bit re-synchronises the receiver on every frame, which resets the lever arm to zero and stops the error accumulating across bytes. Without that, a 1% error would destroy the link within a hundred bits.

2. Where Dividers Actually Go Wrong

The divider is round(f_clk / baud), and the rounding is almost never the problem:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  f_clk MHz     baud   ideal div     div   truncation error
  ---------   ------   ---------   -----   ----------------
       50    115200      434.03     434    -0.006%
      100    115200      868.06     868    -0.006%
       12    115200      104.17     104    -0.160%
       16      9600     1666.67    1667    +0.020%
       25    460800       54.25      54    -0.467%
       48    921600       52.08      52    -0.160%

Every one of those is comfortably inside 5.26%. Integer truncation on a sane clock is a fraction of a percent, and it is not what breaks links.

What breaks links is the assumed clock frequency being wrong. A board respun with a 24 MHz oscillator instead of 25 MHz, a PLL that did not lock and left the design on its reference, a divider constant copied from a project with a different clock — these produce errors of 4%, 20%, 100%, not 0.1%. When a UART is corrupting data, the divider constant is worth checking, but the actual clock frequency is worth measuring.

3. Five Receivers, One Wire

The experiment puts five instances of the same receiver on a single line driven at DIV_TX = 32 clocks per bit. Nothing about any receiver is faulty; they differ only in the divider each was told to use.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  instance   DIV_RX   divider error
  --------   ------   -------------
  rx32           32     0.0%
  rx31           31    -3.1%
  rx33           33    +3.1%
  rx30           30    -6.2%
  rx34           34    +6.2%

The ±3.1% pair sits inside the 5.26% budget and must work. The ±6.2% pair sits outside it and must not. Here is the receiver:

Verilog

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------------
// uart_skew_rx -- an ordinary mid-bit-sampling UART receiver, parameterised on
// its OWN divider so it can be run deliberately mismatched against the
// transmitter that is driving it.
//
// Nothing here is faulty. This is what every UART receiver does: find the
// falling edge, wait half a bit to land mid-start, then step one bit period at
// a time. The failure signature of a baud mismatch is produced entirely by the
// arithmetic of that stepping -- the sample point drifts by (DIV - DIV_TX)
// clocks per bit, and the drift ACCUMULATES. Bit 7 is sampled nine and a half
// receiver bit periods after the edge, so it carries nineteen times the error
// of the start bit. That is why the high bits of a byte corrupt first.
// ---------------------------------------------------------------------------
module uart_skew_rx #(
    parameter DIV = 32               // this receiver's clocks per bit
)(
    input  wire       clk,
    input  wire       rst_n,
    input  wire       line_i,
    output reg  [7:0] data_o,        // last byte assembled
    output reg        valid_o,       // one-clock strobe: a frame completed
    output reg        frame_err_o,   // stop bit was not MARK
    output reg        false_start_o  // start bit vanished before mid-bit
);

    localparam S_IDLE  = 2'd0,
               S_START = 2'd1,
               S_DATA  = 2'd2,
               S_STOP  = 2'd3;

    reg [1:0]  state;
    reg [15:0] cnt;
    reg [3:0]  idx;
    reg        line_q;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            state         <= S_IDLE;
            cnt           <= 16'd0;
            idx           <= 4'd0;
            line_q        <= 1'b1;
            data_o        <= 8'd0;
            valid_o       <= 1'b0;
            frame_err_o   <= 1'b0;
            false_start_o <= 1'b0;
        end else begin
            line_q        <= line_i;
            valid_o       <= 1'b0;          // strobes are one clock wide
            false_start_o <= 1'b0;

            case (state)
                S_IDLE: begin
                    if (line_q && !line_i) begin   // falling edge = candidate start
                        state <= S_START;
                        cnt   <= 16'd0;
                    end
                end

                S_START: begin
                    if (cnt == (DIV/2) - 1) begin
                        // Mid-start. If the line has already recovered, this
                        // was never a start bit.
                        if (line_i) begin
                            state         <= S_IDLE;
                            false_start_o <= 1'b1;
                        end else begin
                            state <= S_DATA;
                            cnt   <= 16'd0;
                            idx   <= 4'd0;
                        end
                    end else begin
                        cnt <= cnt + 16'd1;
                    end
                end

                S_DATA: begin
                    if (cnt == DIV - 1) begin
                        data_o <= {line_i, data_o[7:1]};   // LSB first
                        cnt    <= 16'd0;
                        if (idx == 4'd7) state <= S_STOP;
                        else             idx   <= idx + 4'd1;
                    end else begin
                        cnt <= cnt + 16'd1;
                    end
                end

                S_STOP: begin
                    if (cnt == DIV - 1) begin
                        valid_o     <= 1'b1;
                        frame_err_o <= ~line_i;            // stop must be MARK
                        state       <= S_IDLE;
                        cnt         <= 16'd0;
                    end else begin
                        cnt <= cnt + 16'd1;
                    end
                end

                default: state <= S_IDLE;
            endcase
        end
    end

endmodule

SystemVerilog

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ---------------------------------------------------------------------------
// uart_skew_rx -- an ordinary mid-bit-sampling UART receiver, parameterised on
// its OWN divider so it can be run deliberately mismatched against the
// transmitter that is driving it.
//
// Nothing here is faulty. This is what every UART receiver does: find the
// falling edge, wait half a bit to land mid-start, then step one bit period at
// a time. The failure signature of a baud mismatch is produced entirely by the
// arithmetic of that stepping -- the sample point drifts by (DIV - DIV_TX)
// clocks per bit, and the drift ACCUMULATES. Bit 7 is sampled nine and a half
// receiver bit periods after the edge, so it carries nineteen times the error
// of the start bit. That is why the high bits of a byte corrupt first.
// ---------------------------------------------------------------------------
module uart_skew_rx #(
    parameter int DIV = 32               // this receiver's clocks per bit
)(
    input  logic      clk,
    input  logic      rst_n,
    input  logic      line_i,
    output logic [7:0] data_o,        // last byte assembled
    output logic       valid_o,       // one-clock strobe: a frame completed
    output logic       frame_err_o,   // stop bit was not MARK
    output logic       false_start_o  // start bit vanished before mid-bit
);

    localparam S_IDLE  = 2'd0,
               S_START = 2'd1,
               S_DATA  = 2'd2,
               S_STOP  = 2'd3;

    logic [1:0]  state;
    logic [15:0] cnt;
    logic [3:0]  idx;
    logic       line_q;

    always_ff @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            state         <= S_IDLE;
            cnt           <= '0;
            idx           <= '0;
            line_q        <= 1'b1;
            data_o        <= '0;
            valid_o       <= 1'b0;
            frame_err_o   <= 1'b0;
            false_start_o <= 1'b0;
        end else begin
            line_q        <= line_i;
            valid_o       <= 1'b0;          // strobes are one clock wide
            false_start_o <= 1'b0;

            case (state)
                S_IDLE: begin
                    if (line_q && !line_i) begin   // falling edge = candidate start
                        state <= S_START;
                        cnt   <= '0;
                    end
                end

                S_START: begin
                    if (cnt == (DIV/2) - 1) begin
                        // Mid-start. If the line has already recovered, this
                        // was never a start bit.
                        if (line_i) begin
                            state         <= S_IDLE;
                            false_start_o <= 1'b1;
                        end else begin
                            state <= S_DATA;
                            cnt   <= '0;
                            idx   <= '0;
                        end
                    end else begin
                        cnt <= cnt + 1'b1;
                    end
                end

                S_DATA: begin
                    if (cnt == DIV - 1) begin
                        data_o <= {line_i, data_o[7:1]};   // LSB first
                        cnt    <= '0;
                        if (idx == 4'd7) state <= S_STOP;
                        else             idx   <= idx + 1'b1;
                    end else begin
                        cnt <= cnt + 1'b1;
                    end
                end

                S_STOP: begin
                    if (cnt == DIV - 1) begin
                        valid_o     <= 1'b1;
                        frame_err_o <= ~line_i;            // stop must be MARK
                        state       <= S_IDLE;
                        cnt         <= '0;
                    end else begin
                        cnt <= cnt + 1'b1;
                    end
                end

                default: state <= S_IDLE;
            endcase
        end
    end

endmodule

VHDL

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- ---------------------------------------------------------------------------
-- uart_skew_rx -- an ordinary mid-bit-sampling UART receiver, parameterised on
-- its OWN divider so it can be run deliberately mismatched against the
-- transmitter that is driving it.
--
-- Nothing here is faulty. This is what every UART receiver does: find the
-- falling edge, wait half a bit to land mid-start, then step one bit period at
-- a time. The failure signature of a baud mismatch is produced entirely by the
-- arithmetic of that stepping -- the sample point drifts by (DIV - DIV_TX)
-- clocks per bit, and the drift ACCUMULATES. Bit 7 is sampled nine and a half
-- receiver bit periods after the edge, so it carries nineteen times the error
-- of the start bit. That is why the high bits of a byte corrupt first.
-- ---------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity uart_skew_rx is
    generic (
        DIV : natural := 32                    -- this receiver's clocks per bit
    );
    port (
        clk           : in  std_logic;
        rst_n         : in  std_logic;
        line_i        : in  std_logic;
        data_o        : out std_logic_vector(7 downto 0);  -- last byte assembled
        valid_o       : out std_logic;                     -- frame-complete strobe
        frame_err_o   : out std_logic;                     -- stop bit was not MARK
        false_start_o : out std_logic                      -- start vanished by mid-bit
    );
end entity uart_skew_rx;

architecture rtl of uart_skew_rx is

    type state_t is (S_IDLE, S_START, S_DATA, S_STOP);

    signal state  : state_t := S_IDLE;
    signal cnt    : unsigned(15 downto 0) := (others => '0');
    signal idx    : unsigned(3 downto 0)  := (others => '0');
    signal line_q : std_logic := '1';

    -- outputs are mirrored internally: an entity may not read its own outputs
    signal data_r  : std_logic_vector(7 downto 0) := (others => '0');
    signal valid_r : std_logic := '0';
    signal ferr_r  : std_logic := '0';
    signal fstart_r: std_logic := '0';

begin

    data_o        <= data_r;
    valid_o       <= valid_r;
    frame_err_o   <= ferr_r;
    false_start_o <= fstart_r;

    process (clk, rst_n)
    begin
        if rst_n = '0' then
            state    <= S_IDLE;
            cnt      <= (others => '0');
            idx      <= (others => '0');
            line_q   <= '1';
            data_r   <= (others => '0');
            valid_r  <= '0';
            ferr_r   <= '0';
            fstart_r <= '0';
        elsif rising_edge(clk) then
            line_q   <= line_i;
            valid_r  <= '0';                   -- strobes are one clock wide
            fstart_r <= '0';

            case state is

                when S_IDLE =>
                    if line_q = '1' and line_i = '0' then   -- candidate start
                        state <= S_START;
                        cnt   <= (others => '0');
                    end if;

                when S_START =>
                    if cnt = to_unsigned((DIV/2) - 1, cnt'length) then
                        -- Mid-start. If the line has already recovered, this
                        -- was never a start bit.
                        if line_i = '1' then
                            state    <= S_IDLE;
                            fstart_r <= '1';
                        else
                            state <= S_DATA;
                            cnt   <= (others => '0');
                            idx   <= (others => '0');
                        end if;
                    else
                        cnt <= cnt + 1;
                    end if;

                when S_DATA =>
                    if cnt = to_unsigned(DIV - 1, cnt'length) then
                        data_r <= line_i & data_r(7 downto 1);   -- LSB first
                        cnt    <= (others => '0');
                        if idx = to_unsigned(7, idx'length) then
                            state <= S_STOP;
                        else
                            idx <= idx + 1;
                        end if;
                    else
                        cnt <= cnt + 1;
                    end if;

                when S_STOP =>
                    if cnt = to_unsigned(DIV - 1, cnt'length) then
                        valid_r <= '1';
                        ferr_r  <= not line_i;               -- stop must be MARK
                        state   <= S_IDLE;
                        cnt     <= (others => '0');
                    else
                        cnt <= cnt + 1;
                    end if;

            end case;
        end if;
    end process;

end architecture rtl;

4. The Measured Drift Table

Before looking at any data, the testbench prints where each receiver's sample points actually land relative to the true bit centres. A bit is mis-sampled once the drift reaches half a bit period, which at DIV_TX = 32 is 16 clocks:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  Sample-point drift from the true bit centre, in clocks
    (DIV_TX = 32; a bit is mis-sampled once |drift| reaches 16)
    bit :   d0   d1   d2   d3   d4   d5   d6   d7  stop
    -3.1% (DIV=31):   -2   -3   -4   -5   -6   -7   -8   -9  -10
    +3.1% (DIV=33):    1    2    3    4    5    6    7    8    9
    -6.2% (DIV=30):   -3   -5   -7   -9  -11  -13  -15  -17  -19
    +6.2% (DIV=34):    3    5    7    9   11   13   15   17   19

Read the rows left to right and the lever arm of §1 is visible as a straight line: the drift grows by a constant amount per bit, because each bit adds one more DIV_RX - DIV_TX of error.

Read the columns and the tolerance budget appears as a measurement rather than a formula. The ±3.1% rows reach 9 and 10 clocks at the stop bit — inside 16, so those links work. The ±6.2% rows cross 16 between d6 and d7, reaching 17 at d7 and 19 at the stop bit. The arithmetic of §1 predicted failure beyond 5.26%; the table shows where in the frame it arrives.

Sample-point drift at -6.2% divider error

11 cycles
A timing trace comparing where a correct receiver and a slow receiver take their samples across one frame. The top row shows the transmitted intervals: start, then eight data bits, then stop. The row labelled correct shows sample points landing at the centre of each transmitted bit interval. The row labelled drifting shows a receiver whose bit period is six point two percent short, so each of its sample points falls progressively earlier within its interval. By data bit six the sample is near the leading edge of its interval, and by data bit seven the sample has moved out of the intended interval entirely and lands inside the previous bit. The drift row records the accumulating offset in clocks: minus three, minus five, minus seven, minus nine, minus eleven, minus thirteen, minus fifteen, minus seventeen, and minus nineteen at the stop bit, crossing the half-bit threshold of sixteen clocks between data bit six and data bit seven.samples still inside their bitssamples still inside their bitssamples have walked outsamples have walkedoutdrift 17 exceeds half a bit (16)drift 17 exceeds half a bit(16)stop sampled in the wrong intervalstop sampled in the wrongintervalintervalstartd0d1d2d3d4d5d6d7stopidledrift0-3-5-7-9-11-13-15-17-19..in bit?t0t1t2t3t4t5t6t7t8t9t10

5. The Corruption Sweep

Three payloads, each sent once, read simultaneously by all five receivers:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  sent    DIV=32   DIV=31   DIV=33   DIV=30   DIV=34
  ----    ------   ------   ------   ------   ------
  0x55      0x55     0x55     0x55     0xd5     0xd5
  0xAA      0xaa     0xaa     0xaa     0x2a     0xaa
  0x3C      0x3c     0x3c     0x3c     0x3c     0xbc

The ±3.1% columns are clean on every payload, as the budget requires. The ±6.2% columns are where it gets interesting, and there are two separate lessons in that small table.

The corruption is in the top bit. 0x55 became 0xd5 under both ±6.2% receivers: bit 7 flipped from 0 to 1, and nothing else changed. The testbench computes the index of the lowest differing bit and reports 7 for both. This is the lever arm again — d7 is the only data bit whose drift exceeded half a period.

The same error does not corrupt every payload. 0xAA survived +6.2% intact but became 0x2a at -6.2%. 0x3C did the exact opposite: intact at -6.2%, corrupted to 0xbc at +6.2%.

6. The Oracle Is a Model of Time, Not a Second Receiver

Testing a receiver against another receiver would prove only that two implementations of the same idea agree. The oracle here is instead a closed-form model of when a mid-bit sampler looks:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    sample clock of data bit k   =   DIV_RX/2 + (k+1) * DIV_RX
    transmitted symbol at t      =   floor(t / DIV_TX)

The predicted bit is simply whichever symbol is on the wire at that instant. Because the second line maps an arbitrary time to a transmitted symbol, the model naturally predicts reading a neighbouring bit — which is exactly the failure under study — without containing any notion of "drift" or "error" at all. It shares no logic with the design.

Verilog

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
`timescale 1ns/1ps
// ---------------------------------------------------------------------------
// Testbench for uart_skew_rx -- the baud-mismatch signature, measured.
//
// Five receivers share one wire. The wire is driven at DIV_TX = 32 clocks per
// bit; the receivers run at 32, 31, 33, 30 and 34 -- that is 0%, -3.1%, +3.1%,
// -6.25% and +6.25% divider error.
//
// The oracle is NOT a second receiver. It is a closed-form model of WHEN a
// mid-bit sampler looks:
//
//     sample clock of data bit k  =  DIV_RX/2 + (k+1)*DIV_RX     (after the edge)
//     transmitted symbol at t     =  floor(t / DIV_TX)
//
// so the predicted bit is simply whichever symbol happens to be on the wire at
// that instant. This predicts reading a NEIGHBOURING bit, which is exactly what
// a baud mismatch does, and it does so without sharing a line of logic with
// the DUT.
// ---------------------------------------------------------------------------
module tb_uart_skew_rx;

    localparam DIV_TX = 32;

    reg clk = 1'b0;
    reg rst_n = 1'b0;
    reg line = 1'b1;

    integer checks = 0;
    integer fails  = 0;

    always #5 clk = ~clk;

    // ---- five receivers, one wire ----
    wire [7:0] d_32, d_31, d_33, d_30, d_34;
    wire       v_32, v_31, v_33, v_30, v_34;
    wire       e_32, e_31, e_33, e_30, e_34;
    wire       f_32;

    uart_skew_rx #(.DIV(32)) rx32 (.clk(clk), .rst_n(rst_n), .line_i(line),
        .data_o(d_32), .valid_o(v_32), .frame_err_o(e_32), .false_start_o(f_32));
    uart_skew_rx #(.DIV(31)) rx31 (.clk(clk), .rst_n(rst_n), .line_i(line),
        .data_o(d_31), .valid_o(v_31), .frame_err_o(e_31), .false_start_o());
    uart_skew_rx #(.DIV(33)) rx33 (.clk(clk), .rst_n(rst_n), .line_i(line),
        .data_o(d_33), .valid_o(v_33), .frame_err_o(e_33), .false_start_o());
    uart_skew_rx #(.DIV(30)) rx30 (.clk(clk), .rst_n(rst_n), .line_i(line),
        .data_o(d_30), .valid_o(v_30), .frame_err_o(e_30), .false_start_o());
    uart_skew_rx #(.DIV(34)) rx34 (.clk(clk), .rst_n(rst_n), .line_i(line),
        .data_o(d_34), .valid_o(v_34), .frame_err_o(e_34), .false_start_o());

    // ---- the transmitted symbol sequence: 0=start, 1..8=data, 9=stop ------
    reg [7:0] tx_byte;

    function sym_level;
        input integer j;
        begin
            if      (j == 0)            sym_level = 1'b0;            // start
            else if (j >= 1 && j <= 8)  sym_level = tx_byte[j-1];    // LSB first
            else                        sym_level = 1'b1;            // stop / idle
        end
    endfunction

    // ---- closed-form oracle: what WILL this divider read? ----------------
    function [7:0] oracle_data;
        input integer div_rx;
        integer k, t;
        begin
            oracle_data = 8'd0;
            for (k = 0; k < 8; k = k + 1) begin
                t = (div_rx/2) + (k+1)*div_rx;
                oracle_data[k] = sym_level(t / DIV_TX);
            end
        end
    endfunction

    function oracle_stop;
        input integer div_rx;
        integer t;
        begin
            t = (div_rx/2) + 9*div_rx;
            oracle_stop = sym_level(t / DIV_TX);
        end
    endfunction

    // drift of bit k's sample point away from the true bit centre, in clocks
    function integer drift_at;
        input integer div_rx;
        input integer k;
        begin
            drift_at = ((div_rx/2) + (k+1)*div_rx) - ((DIV_TX/2) + (k+1)*DIV_TX);
        end
    endfunction

    function integer first_diff;
        input [7:0] a;
        input [7:0] b;
        integer k, r;
        begin
            r = -1;
            for (k = 7; k >= 0; k = k - 1) if (a[k] !== b[k]) r = k;
            first_diff = r;
        end
    endfunction

    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

    // Drive one 8N1 frame at the TRANSMITTER's bit period.
    task send_byte;
        input [7:0] b;
        integer j, i;
        begin
            tx_byte = b;
            for (j = 0; j < 10; j = j + 1) begin
                @(negedge clk);
                line = sym_level(j);
                for (i = 0; i < DIV_TX; i = i + 1) @(posedge clk);
            end
            @(negedge clk); line = 1'b1;
            repeat (DIV_TX) @(posedge clk);          // one idle bit-time
        end
    endtask

    task do_reset;
        begin
            line = 1'b1; rst_n = 1'b0;
            repeat (4) @(posedge clk);
            @(negedge clk); rst_n = 1'b1;
            repeat (DIV_TX) @(posedge clk);
        end
    endtask

    integer k;

    initial begin
        do_reset;

        // ---------------- the drift table ---------------------------------
        $display("Sample-point drift from the true bit centre, in clocks");
        $display("  (DIV_TX = %0d; a bit is mis-sampled once |drift| reaches %0d)", DIV_TX, DIV_TX/2);
        $display("  bit :   d0   d1   d2   d3   d4   d5   d6   d7  stop");
        $write("  -3.1%% (DIV=31):");
        for (k = 0; k < 9; k = k + 1) $write("%5d", drift_at(31, k));
        $display("");
        $write("  +3.1%% (DIV=33):");
        for (k = 0; k < 9; k = k + 1) $write("%5d", drift_at(33, k));
        $display("");
        $write("  -6.2%% (DIV=30):");
        for (k = 0; k < 9; k = k + 1) $write("%5d", drift_at(30, k));
        $display("");
        $write("  +6.2%% (DIV=34):");
        for (k = 0; k < 9; k = k + 1) $write("%5d", drift_at(34, k));
        $display("");
        $display("");

        // ---------------- T1: matched divider is exact --------------------
        send_byte(8'h55);
        chk("T1 0x55 matched data", d_32, 8'h55);
        chk("T1 0x55 matched no framing error", e_32, 0);
        send_byte(8'hAA);
        chk("T1 0xAA matched data", d_32, 8'hAA);
        send_byte(8'h3C);
        chk("T1 0x3C matched data", d_32, 8'h3C);
        chk("T1 0x3C matched no framing error", e_32, 0);

        // ---------------- T2/T3/T4: the sweep, three payloads -------------
        send_byte(8'h55);
        $display("0x55 sent:  DIV=32 -> %02h   31 -> %02h   33 -> %02h   30 -> %02h   34 -> %02h",
                 d_32, d_31, d_33, d_30, d_34);
        chk("T2 DIV=32 matches oracle", d_32, oracle_data(32));
        chk("T2 DIV=31 matches oracle", d_31, oracle_data(31));
        chk("T2 DIV=33 matches oracle", d_33, oracle_data(33));
        chk("T2 DIV=30 matches oracle", d_30, oracle_data(30));
        chk("T2 DIV=34 matches oracle", d_34, oracle_data(34));
        chk("T2 DIV=30 stop matches oracle", e_30, oracle_stop(30) ? 0 : 1);
        chk("T2 DIV=34 stop matches oracle", e_34, oracle_stop(34) ? 0 : 1);

        send_byte(8'hAA);
        $display("0xAA sent:  DIV=32 -> %02h   31 -> %02h   33 -> %02h   30 -> %02h   34 -> %02h",
                 d_32, d_31, d_33, d_30, d_34);
        chk("T3 DIV=32 matches oracle", d_32, oracle_data(32));
        chk("T3 DIV=31 matches oracle", d_31, oracle_data(31));
        chk("T3 DIV=33 matches oracle", d_33, oracle_data(33));
        chk("T3 DIV=30 matches oracle", d_30, oracle_data(30));
        chk("T3 DIV=34 matches oracle", d_34, oracle_data(34));

        send_byte(8'h3C);
        $display("0x3C sent:  DIV=32 -> %02h   31 -> %02h   33 -> %02h   30 -> %02h   34 -> %02h",
                 d_32, d_31, d_33, d_30, d_34);
        chk("T4 DIV=32 matches oracle", d_32, oracle_data(32));
        chk("T4 DIV=31 matches oracle", d_31, oracle_data(31));
        chk("T4 DIV=33 matches oracle", d_33, oracle_data(33));
        chk("T4 DIV=30 matches oracle", d_30, oracle_data(30));
        chk("T4 DIV=34 matches oracle", d_34, oracle_data(34));

        // ---------------- T5: the tolerance boundary ----------------------
        // Theory: the stop bit is sampled 9.5 receiver bit periods after the
        // edge, so the error budget is 0.5/9.5 = 5.26%. 3.1% must survive;
        // 6.25% must not.
        send_byte(8'h55);
        chk("T5 -3.1% survives 0x55", (d_31 == 8'h55) ? 1 : 0, 1);
        chk("T5 +3.1% survives 0x55", (d_33 == 8'h55) ? 1 : 0, 1);
        chk("T5 -6.2% corrupts 0x55", (d_30 != 8'h55) ? 1 : 0, 1);
        chk("T5 +6.2% corrupts 0x55", (d_34 != 8'h55) ? 1 : 0, 1);

        // ---------------- T6: it is the HIGH bits that go first ------------
        $display("first corrupted bit index:  DIV=30 -> %0d   DIV=34 -> %0d",
                 first_diff(d_30, 8'h55), first_diff(d_34, 8'h55));
        chk("T6 DIV=30 first failure is a high bit", (first_diff(d_30, 8'h55) >= 5) ? 1 : 0, 1);
        chk("T6 DIV=34 first failure is a high bit", (first_diff(d_34, 8'h55) >= 5) ? 1 : 0, 1);

        // ---------------- T7: a glitch is not a start bit ------------------
        do_reset;
        @(negedge clk); line = 1'b0;
        repeat (DIV_TX/4) @(posedge clk);       // a quarter-bit spike
        @(negedge clk); line = 1'b1;
        repeat (DIV_TX) @(posedge clk);
        chk("T7 spike raised false_start", f_32, 0);   // strobe already passed
        chk("T7 spike produced no frame", v_32, 0);

        $display("");
        $display("== %0d checks, %0d failures ==", checks, fails);
        if (fails == 0) $display("   RESULT: ALL VERILOG SKEW-RX 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_skew_rx -- the baud-mismatch signature, measured.
//
// Five receivers share one wire. The logic is driven at DIV_TX = 32 clocks per
// bit; the receivers run at 32, 31, 33, 30 and 34 -- that is 0%, -3.1%, +3.1%,
// -6.25% and +6.25% divider error.
//
// The oracle is NOT a second receiver. It is a closed-form model of WHEN a
// mid-bit sampler looks:
//
//     sample clock of data bit k  =  DIV_RX/2 + (k+1)*DIV_RX     (after the edge)
//     transmitted symbol at t     =  floor(t / DIV_TX)
//
// so the predicted bit is simply whichever symbol happens to be on the logic at
// that instant. This predicts reading a NEIGHBOURING bit, which is exactly what
// a baud mismatch does, and it does so without sharing a line of logic with
// the DUT.
// ---------------------------------------------------------------------------
module tb_uart_skew_rx;

    localparam DIV_TX = 32;

    logic clk = 1'b0;
    logic rst_n = 1'b0;
    logic line = 1'b1;

    integer checks = 0;
    integer fails  = 0;

    always #5 clk = ~clk;

    // ---- five receivers, one logic ----
    logic [7:0] d_32, d_31, d_33, d_30, d_34;
    logic       v_32, v_31, v_33, v_30, v_34;
    logic       e_32, e_31, e_33, e_30, e_34;
    logic       f_32;

    uart_skew_rx #(.DIV(32)) rx32 (.clk(clk), .rst_n(rst_n), .line_i(line),
        .data_o(d_32), .valid_o(v_32), .frame_err_o(e_32), .false_start_o(f_32));
    uart_skew_rx #(.DIV(31)) rx31 (.clk(clk), .rst_n(rst_n), .line_i(line),
        .data_o(d_31), .valid_o(v_31), .frame_err_o(e_31), .false_start_o());
    uart_skew_rx #(.DIV(33)) rx33 (.clk(clk), .rst_n(rst_n), .line_i(line),
        .data_o(d_33), .valid_o(v_33), .frame_err_o(e_33), .false_start_o());
    uart_skew_rx #(.DIV(30)) rx30 (.clk(clk), .rst_n(rst_n), .line_i(line),
        .data_o(d_30), .valid_o(v_30), .frame_err_o(e_30), .false_start_o());
    uart_skew_rx #(.DIV(34)) rx34 (.clk(clk), .rst_n(rst_n), .line_i(line),
        .data_o(d_34), .valid_o(v_34), .frame_err_o(e_34), .false_start_o());

    // ---- the transmitted symbol sequence: 0=start, 1..8=data, 9=stop ------
    logic [7:0] tx_byte;

    function automatic logic sym_level(input int j);
        begin
            if      (j == 0)            sym_level = 1'b0;            // start
            else if (j >= 1 && j <= 8)  sym_level = tx_byte[j-1];    // LSB first
            else                        sym_level = 1'b1;            // stop / idle
        end
    endfunction

    // ---- closed-form oracle: what WILL this divider read? ----------------
    function automatic logic [7:0] oracle_data(input int div_rx);
        int k, t;
        begin
            oracle_data = 8'd0;
            for (k = 0; k < 8; k = k + 1) begin
                t = (div_rx/2) + (k+1)*div_rx;
                oracle_data[k] = sym_level(t / DIV_TX);
            end
        end
    endfunction

    function automatic logic oracle_stop(input int div_rx);
        int t;
        begin
            t = (div_rx/2) + 9*div_rx;
            oracle_stop = sym_level(t / DIV_TX);
        end
    endfunction

    // drift of bit k's sample point away from the true bit centre, in clocks
    function automatic int drift_at(input int div_rx, input int k);
        begin
            drift_at = ((div_rx/2) + (k+1)*div_rx) - ((DIV_TX/2) + (k+1)*DIV_TX);
        end
    endfunction

    function automatic int first_diff(input logic [7:0] a, input logic [7:0] b);
        int k, r;
        begin
            r = -1;
            for (k = 7; k >= 0; k = k - 1) if (a[k] !== b[k]) r = k;
            first_diff = r;
        end
    endfunction

    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

    // Drive one 8N1 frame at the TRANSMITTER's bit period.
    task automatic send_byte(input logic [7:0] b);
        int j, i;
        begin
            tx_byte = b;
            for (j = 0; j < 10; j = j + 1) begin
                @(negedge clk);
                line = sym_level(j);
                for (i = 0; i < DIV_TX; i = i + 1) @(posedge clk);
            end
            @(negedge clk); line = 1'b1;
            repeat (DIV_TX) @(posedge clk);          // one idle bit-time
        end
    endtask

    task automatic do_reset();
        begin
            line = 1'b1; rst_n = 1'b0;
            repeat (4) @(posedge clk);
            @(negedge clk); rst_n = 1'b1;
            repeat (DIV_TX) @(posedge clk);
        end
    endtask

    integer k;

    initial begin
        do_reset;

        // ---------------- the drift table ---------------------------------
        $display("Sample-point drift from the true bit centre, in clocks");
        $display("  (DIV_TX = %0d; a bit is mis-sampled once |drift| reaches %0d)", DIV_TX, DIV_TX/2);
        $display("  bit :   d0   d1   d2   d3   d4   d5   d6   d7  stop");
        $write("  -3.1%% (DIV=31):");
        for (k = 0; k < 9; k = k + 1) $write("%5d", drift_at(31, k));
        $display("");
        $write("  +3.1%% (DIV=33):");
        for (k = 0; k < 9; k = k + 1) $write("%5d", drift_at(33, k));
        $display("");
        $write("  -6.2%% (DIV=30):");
        for (k = 0; k < 9; k = k + 1) $write("%5d", drift_at(30, k));
        $display("");
        $write("  +6.2%% (DIV=34):");
        for (k = 0; k < 9; k = k + 1) $write("%5d", drift_at(34, k));
        $display("");
        $display("");

        // ---------------- T1: matched divider is exact --------------------
        send_byte(8'h55);
        chk("T1 0x55 matched data", d_32, 8'h55);
        chk("T1 0x55 matched no framing error", e_32, 0);
        send_byte(8'hAA);
        chk("T1 0xAA matched data", d_32, 8'hAA);
        send_byte(8'h3C);
        chk("T1 0x3C matched data", d_32, 8'h3C);
        chk("T1 0x3C matched no framing error", e_32, 0);

        // ---------------- T2/T3/T4: the sweep, three payloads -------------
        send_byte(8'h55);
        $display("0x55 sent:  DIV=32 -> %02h   31 -> %02h   33 -> %02h   30 -> %02h   34 -> %02h",
                 d_32, d_31, d_33, d_30, d_34);
        chk("T2 DIV=32 matches oracle", d_32, oracle_data(32));
        chk("T2 DIV=31 matches oracle", d_31, oracle_data(31));
        chk("T2 DIV=33 matches oracle", d_33, oracle_data(33));
        chk("T2 DIV=30 matches oracle", d_30, oracle_data(30));
        chk("T2 DIV=34 matches oracle", d_34, oracle_data(34));
        chk("T2 DIV=30 stop matches oracle", e_30, oracle_stop(30) ? 0 : 1);
        chk("T2 DIV=34 stop matches oracle", e_34, oracle_stop(34) ? 0 : 1);

        send_byte(8'hAA);
        $display("0xAA sent:  DIV=32 -> %02h   31 -> %02h   33 -> %02h   30 -> %02h   34 -> %02h",
                 d_32, d_31, d_33, d_30, d_34);
        chk("T3 DIV=32 matches oracle", d_32, oracle_data(32));
        chk("T3 DIV=31 matches oracle", d_31, oracle_data(31));
        chk("T3 DIV=33 matches oracle", d_33, oracle_data(33));
        chk("T3 DIV=30 matches oracle", d_30, oracle_data(30));
        chk("T3 DIV=34 matches oracle", d_34, oracle_data(34));

        send_byte(8'h3C);
        $display("0x3C sent:  DIV=32 -> %02h   31 -> %02h   33 -> %02h   30 -> %02h   34 -> %02h",
                 d_32, d_31, d_33, d_30, d_34);
        chk("T4 DIV=32 matches oracle", d_32, oracle_data(32));
        chk("T4 DIV=31 matches oracle", d_31, oracle_data(31));
        chk("T4 DIV=33 matches oracle", d_33, oracle_data(33));
        chk("T4 DIV=30 matches oracle", d_30, oracle_data(30));
        chk("T4 DIV=34 matches oracle", d_34, oracle_data(34));

        // ---------------- T5: the tolerance boundary ----------------------
        // Theory: the stop bit is sampled 9.5 receiver bit periods after the
        // edge, so the error budget is 0.5/9.5 = 5.26%. 3.1% must survive;
        // 6.25% must not.
        send_byte(8'h55);
        chk("T5 -3.1% survives 0x55", (d_31 == 8'h55) ? 1 : 0, 1);
        chk("T5 +3.1% survives 0x55", (d_33 == 8'h55) ? 1 : 0, 1);
        chk("T5 -6.2% corrupts 0x55", (d_30 != 8'h55) ? 1 : 0, 1);
        chk("T5 +6.2% corrupts 0x55", (d_34 != 8'h55) ? 1 : 0, 1);

        // ---------------- T6: it is the HIGH bits that go first ------------
        $display("first corrupted bit index:  DIV=30 -> %0d   DIV=34 -> %0d",
                 first_diff(d_30, 8'h55), first_diff(d_34, 8'h55));
        chk("T6 DIV=30 first failure is a high bit", (first_diff(d_30, 8'h55) >= 5) ? 1 : 0, 1);
        chk("T6 DIV=34 first failure is a high bit", (first_diff(d_34, 8'h55) >= 5) ? 1 : 0, 1);

        // ---------------- T7: a glitch is not a start bit ------------------
        do_reset;
        @(negedge clk); line = 1'b0;
        repeat (DIV_TX/4) @(posedge clk);       // a quarter-bit spike
        @(negedge clk); line = 1'b1;
        repeat (DIV_TX) @(posedge clk);
        chk("T7 spike raised false_start", f_32, 0);   // strobe already passed
        chk("T7 spike produced no frame", v_32, 0);

        $display("");
        $display("== %0d checks, %0d failures ==", checks, fails);
        if (fails == 0) $display("   RESULT: ALL SYSTEMVERILOG SKEW-RX TESTS PASSED");
        else            $display("   RESULT: %0d FAILURE(S)", fails);
        $finish;
    end

endmodule

VHDL

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- ---------------------------------------------------------------------------
-- Testbench for uart_skew_rx -- the baud-mismatch signature, measured.
--
-- Five receivers share one wire. The wire is driven at DIV_TX = 32 clocks per
-- bit; the receivers run at 32, 31, 33, 30 and 34 -- that is 0%, -3.1%, +3.1%,
-- -6.25% and +6.25% divider error.
--
-- The oracle is NOT a second receiver. It is a closed-form model of WHEN a
-- mid-bit sampler looks:
--
--     sample clock of data bit k  =  DIV_RX/2 + (k+1)*DIV_RX    (after the edge)
--     transmitted symbol at t     =  t / DIV_TX                 (integer divide)
--
-- so the predicted bit is simply whichever symbol happens to be on the wire at
-- that instant. This predicts reading a NEIGHBOURING bit, which is exactly what
-- a baud mismatch does, and it does so without sharing a line of logic with
-- the DUT.
-- ---------------------------------------------------------------------------
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity tb_uart_skew_rx is
end entity tb_uart_skew_rx;

architecture sim of tb_uart_skew_rx is

    constant DIV_TX : natural := 32;
    constant TCLK   : time    := 10 ns;

    signal clk   : std_logic := '0';
    signal rst_n : std_logic := '0';
    signal line  : std_logic := '1';
    signal done  : boolean   := false;

    signal d_32, d_31, d_33, d_30, d_34 : std_logic_vector(7 downto 0);
    signal v_32, v_31, v_33, v_30, v_34 : std_logic;
    signal e_32, e_31, e_33, e_30, e_34 : std_logic;
    signal f_32                          : std_logic;
    signal open_fs                       : std_logic;

    -- ---- the transmitted symbol sequence: 0=start, 1..8=data, 9=stop ------
    function sym_level (j : integer; data : std_logic_vector(7 downto 0))
        return std_logic is
    begin
        if j = 0 then
            return '0';                                   -- start
        elsif j >= 1 and j <= 8 then
            return data(j-1);                             -- LSB first
        else
            return '1';                                   -- stop / idle
        end if;
    end function;

    -- ---- closed-form oracle: what WILL this divider read? ----------------
    function oracle_data (div_rx : integer; data : std_logic_vector(7 downto 0))
        return std_logic_vector is
        variable r : std_logic_vector(7 downto 0) := (others => '0');
        variable t : integer;
    begin
        for k in 0 to 7 loop
            t    := (div_rx/2) + (k+1)*div_rx;
            r(k) := sym_level(t / DIV_TX, data);
        end loop;
        return r;
    end function;

    function oracle_stop (div_rx : integer; data : std_logic_vector(7 downto 0))
        return std_logic is
        variable t : integer;
    begin
        t := (div_rx/2) + 9*div_rx;
        return sym_level(t / DIV_TX, data);
    end function;

    -- drift of bit k's sample point away from the true bit centre, in clocks
    function drift_at (div_rx : integer; k : integer) return integer is
    begin
        return ((div_rx/2) + (k+1)*div_rx) - ((DIV_TX/2) + (k+1)*DIV_TX);
    end function;

    function first_diff (a : std_logic_vector(7 downto 0);
                         b : std_logic_vector(7 downto 0)) return integer is
        variable r : integer := -1;
    begin
        for k in 7 downto 0 loop
            if a(k) /= b(k) then r := k; end if;
        end loop;
        return r;
    end function;

    function hex2 (v : std_logic_vector(7 downto 0)) return string is
        constant D : string(1 to 16) := "0123456789abcdef";
        variable u : integer := to_integer(unsigned(v));
    begin
        return D(u/16 + 1) & D(u mod 16 + 1);
    end function;

    function pad5 (n : integer) return string is
        variable s : string(1 to 5) := (others => ' ');
        variable t : string(1 to 20);
        variable l : integer;
    begin
        t := (others => ' ');
        l := integer'image(n)'length;
        s(6-l to 5) := integer'image(n);
        return s;
    end function;

begin

    clk <= '0' when done else not clk after TCLK/2;

    rx32 : entity work.uart_skew_rx generic map (DIV => 32)
        port map (clk => clk, rst_n => rst_n, line_i => line, data_o => d_32,
                  valid_o => v_32, frame_err_o => e_32, false_start_o => f_32);
    rx31 : entity work.uart_skew_rx generic map (DIV => 31)
        port map (clk => clk, rst_n => rst_n, line_i => line, data_o => d_31,
                  valid_o => v_31, frame_err_o => e_31, false_start_o => open);
    rx33 : entity work.uart_skew_rx generic map (DIV => 33)
        port map (clk => clk, rst_n => rst_n, line_i => line, data_o => d_33,
                  valid_o => v_33, frame_err_o => e_33, false_start_o => open);
    rx30 : entity work.uart_skew_rx generic map (DIV => 30)
        port map (clk => clk, rst_n => rst_n, line_i => line, data_o => d_30,
                  valid_o => v_30, frame_err_o => e_30, false_start_o => open);
    rx34 : entity work.uart_skew_rx generic map (DIV => 34)
        port map (clk => clk, rst_n => rst_n, line_i => line, data_o => d_34,
                  valid_o => v_34, frame_err_o => e_34, false_start_o => open);

    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 chkv (name : string; got : std_logic_vector(7 downto 0);
                                       exp : std_logic_vector(7 downto 0)) is
        begin
            checks := checks + 1;
            if got /= exp then
                fails := fails + 1;
                report "  FAIL " & name & ": got " & hex2(got) &
                       " expected " & hex2(exp) severity error;
            end if;
        end procedure;

        -- Drive one 8N1 frame at the TRANSMITTER's bit period.
        procedure send_byte (b : std_logic_vector(7 downto 0)) is
        begin
            for j in 0 to 9 loop
                wait until falling_edge(clk);
                line <= sym_level(j, b);
                for i in 0 to DIV_TX-1 loop
                    wait until rising_edge(clk);
                end loop;
            end loop;
            wait until falling_edge(clk);
            line <= '1';
            for i in 0 to DIV_TX-1 loop
                wait until rising_edge(clk);
            end loop;
        end procedure;

        procedure do_reset is
        begin
            line <= '1'; rst_n <= '0';
            for i in 0 to 3 loop wait until rising_edge(clk); end loop;
            wait until falling_edge(clk);
            rst_n <= '1';
            for i in 0 to DIV_TX-1 loop wait until rising_edge(clk); end loop;
        end procedure;

        variable row : string(1 to 45);
    begin
        do_reset;

        -- ---------------- the drift table ---------------------------------
        report "Sample-point drift from the true bit centre, in clocks";
        report "  (DIV_TX = " & integer'image(DIV_TX) &
               "; a bit is mis-sampled once |drift| reaches " &
               integer'image(DIV_TX/2) & ")";
        report "  bit :   d0   d1   d2   d3   d4   d5   d6   d7  stop";
        for sel in 0 to 3 loop
            row := (others => ' ');
            for k in 0 to 8 loop
                case sel is
                    when 0 => row(k*5+1 to k*5+5) := pad5(drift_at(31, k));
                    when 1 => row(k*5+1 to k*5+5) := pad5(drift_at(33, k));
                    when 2 => row(k*5+1 to k*5+5) := pad5(drift_at(30, k));
                    when others => row(k*5+1 to k*5+5) := pad5(drift_at(34, k));
                end case;
            end loop;
            case sel is
                when 0 => report "  -3.1% (DIV=31):" & row;
                when 1 => report "  +3.1% (DIV=33):" & row;
                when 2 => report "  -6.2% (DIV=30):" & row;
                when others => report "  +6.2% (DIV=34):" & row;
            end case;
        end loop;

        -- ---------------- T1: matched divider is exact --------------------
        send_byte(x"55");
        chkv("T1 0x55 matched data", d_32, x"55");
        chk ("T1 0x55 matched no framing error", to_integer(unsigned'("" & e_32)), 0);
        send_byte(x"AA");
        chkv("T1 0xAA matched data", d_32, x"AA");
        send_byte(x"3C");
        chkv("T1 0x3C matched data", d_32, x"3C");
        chk ("T1 0x3C matched no framing error", to_integer(unsigned'("" & e_32)), 0);

        -- ---------------- T2/T3/T4: the sweep, three payloads -------------
        send_byte(x"55");
        report "0x55 sent:  DIV=32 -> " & hex2(d_32) & "   31 -> " & hex2(d_31) &
               "   33 -> " & hex2(d_33) & "   30 -> " & hex2(d_30) &
               "   34 -> " & hex2(d_34);
        chkv("T2 DIV=32 matches oracle", d_32, oracle_data(32, x"55"));
        chkv("T2 DIV=31 matches oracle", d_31, oracle_data(31, x"55"));
        chkv("T2 DIV=33 matches oracle", d_33, oracle_data(33, x"55"));
        chkv("T2 DIV=30 matches oracle", d_30, oracle_data(30, x"55"));
        chkv("T2 DIV=34 matches oracle", d_34, oracle_data(34, x"55"));
        if oracle_stop(30, x"55") = '1' then
            chk("T2 DIV=30 stop matches oracle", to_integer(unsigned'("" & e_30)), 0);
        else
            chk("T2 DIV=30 stop matches oracle", to_integer(unsigned'("" & e_30)), 1);
        end if;
        if oracle_stop(34, x"55") = '1' then
            chk("T2 DIV=34 stop matches oracle", to_integer(unsigned'("" & e_34)), 0);
        else
            chk("T2 DIV=34 stop matches oracle", to_integer(unsigned'("" & e_34)), 1);
        end if;

        send_byte(x"AA");
        report "0xAA sent:  DIV=32 -> " & hex2(d_32) & "   31 -> " & hex2(d_31) &
               "   33 -> " & hex2(d_33) & "   30 -> " & hex2(d_30) &
               "   34 -> " & hex2(d_34);
        chkv("T3 DIV=32 matches oracle", d_32, oracle_data(32, x"AA"));
        chkv("T3 DIV=31 matches oracle", d_31, oracle_data(31, x"AA"));
        chkv("T3 DIV=33 matches oracle", d_33, oracle_data(33, x"AA"));
        chkv("T3 DIV=30 matches oracle", d_30, oracle_data(30, x"AA"));
        chkv("T3 DIV=34 matches oracle", d_34, oracle_data(34, x"AA"));

        send_byte(x"3C");
        report "0x3C sent:  DIV=32 -> " & hex2(d_32) & "   31 -> " & hex2(d_31) &
               "   33 -> " & hex2(d_33) & "   30 -> " & hex2(d_30) &
               "   34 -> " & hex2(d_34);
        chkv("T4 DIV=32 matches oracle", d_32, oracle_data(32, x"3C"));
        chkv("T4 DIV=31 matches oracle", d_31, oracle_data(31, x"3C"));
        chkv("T4 DIV=33 matches oracle", d_33, oracle_data(33, x"3C"));
        chkv("T4 DIV=30 matches oracle", d_30, oracle_data(30, x"3C"));
        chkv("T4 DIV=34 matches oracle", d_34, oracle_data(34, x"3C"));

        -- ---------------- T5: the tolerance boundary ----------------------
        -- Theory: the stop bit is sampled 9.5 receiver bit periods after the
        -- edge, so the error budget is 0.5/9.5 = 5.26%. 3.1% must survive;
        -- 6.25% must not.
        send_byte(x"55");
        if d_31 = x"55" then chk("T5 -3.1% survives 0x55", 1, 1);
        else                 chk("T5 -3.1% survives 0x55", 0, 1); end if;
        if d_33 = x"55" then chk("T5 +3.1% survives 0x55", 1, 1);
        else                 chk("T5 +3.1% survives 0x55", 0, 1); end if;
        if d_30 /= x"55" then chk("T5 -6.2% corrupts 0x55", 1, 1);
        else                  chk("T5 -6.2% corrupts 0x55", 0, 1); end if;
        if d_34 /= x"55" then chk("T5 +6.2% corrupts 0x55", 1, 1);
        else                  chk("T5 +6.2% corrupts 0x55", 0, 1); end if;

        -- ---------------- T6: it is the HIGH bits that go first ------------
        report "first corrupted bit index:  DIV=30 -> " &
               integer'image(first_diff(d_30, x"55")) & "   DIV=34 -> " &
               integer'image(first_diff(d_34, x"55"));
        if first_diff(d_30, x"55") >= 5 then
            chk("T6 DIV=30 first failure is a high bit", 1, 1);
        else
            chk("T6 DIV=30 first failure is a high bit", 0, 1);
        end if;
        if first_diff(d_34, x"55") >= 5 then
            chk("T6 DIV=34 first failure is a high bit", 1, 1);
        else
            chk("T6 DIV=34 first failure is a high bit", 0, 1);
        end if;

        -- ---------------- T7: a glitch is not a start bit ------------------
        do_reset;
        wait until falling_edge(clk); line <= '0';
        for i in 0 to DIV_TX/4 - 1 loop wait until rising_edge(clk); end loop;
        wait until falling_edge(clk); line <= '1';
        for i in 0 to DIV_TX-1 loop wait until rising_edge(clk); end loop;
        chk("T7 spike raised false_start", to_integer(unsigned'("" & f_32)), 0);
        chk("T7 spike produced no frame",  to_integer(unsigned'("" & v_32)), 0);

        report "";
        report "== " & integer'image(checks) & " checks, " &
               integer'image(fails) & " failures ==";
        if fails = 0 then
            report "   RESULT: ALL VHDL SKEW-RX TESTS PASSED";
        else
            report "   RESULT: " & integer'image(fails) & " FAILURE(S)" severity error;
        end if;
        done <= true;
        wait;
    end process;

end architecture sim;

Thirty checks per language, and the three implementations agree on every received byte, the whole drift table, and the simulation end time of 25,755 ns:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  test  what it establishes                                          result
  ----  -----------------------------------------------------------  --------------
  T1    a matched divider is exact on three payloads                  0x55/0xAA/0x3C
  T2    all five receivers match the closed-form oracle on 0x55       5/5 agree
  T3    the same, on 0xAA                                             5/5 agree
  T4    the same, on 0x3C                                             5/5 agree
  T5    +/-3.1% survives and +/-6.2% corrupts, as 5.26% predicts      4 checks
  T6    the first corrupted bit index is high, not low                d7 in both
  T7    a quarter-bit spike starts no frame                           no frame

T2 also checks the framing outcome against the oracle's prediction for the stop bit, which is the constraint §1 identified as tightest.

7. Proving the Tests Can Fail

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  mutation                                          checks failed   verdict
  -----------------------------------------------   -------------   -------
  M4  wait DIV/4 instead of DIV/2 after the edge                11    killed
  M5  assemble the byte MSB-first instead of LSB                17    killed

M4 moves every sample point a quarter of a bit off centre, which halves the timing margin; eleven checks notice. M5 is the bit-order error that Chapter 17.3 is about, and it fails seventeen checks here — which is worth noticing, because it means this chapter's suite would catch a reversal even though reversal is not what it was written to test.

8. Recognising a Divider Error from a Capture

Putting the chapter together, here is what separates a baud problem from the other things that corrupt bytes:

ObservationPoints towardWhy
Corruption concentrated in the high bitsbaud errorthe lever arm of §1
Framing errors on bytes that still decodebaud errorthe stop bit is sampled furthest out
Corruption that depends on the payloadbaud error§5 — a drifted sample may land on an identical neighbour
Every byte wrong in the same way regardless of valuebit order or parity config — 17.3a systematic remap, not a timing walk
Corruption spread evenly across bit positionsnoise — 17.4noise has no lever arm
Bytes missing entirely rather than wrongoverrun or flow control — 17.5nothing was mis-sampled; it was never stored

And the direct measurement, if you can capture the line: recover the bit period with the instrument from Chapter 17.1, then compare it against the period the receiver believes in. A mismatch there is the whole diagnosis, and it does not require decoding a single byte.

Continue learning

Where this fits

Part of the UART curriculum.