Skip to content
VLSI Mentor

UART · Module 14

Verifying the Receiver

Driving a receiver with legal, marginal and deliberately illegal traffic, and checking sampling behaviour, reported status and recovery against the specification.

The transmitter had one job and a predictable output, so Chapter 14.3 could compute the correct answer and compare. The receiver is harder, because its input is the part that varies — and most of the interesting inputs are ones no correct transmitter would ever produce.

Three categories, and a plan that omits any of them has a hole:

CategoryWhat it establishesStimulus
legalthe receiver worksnominal baud, well-formed frames
marginalthe tolerance the specification claimsbaud offsets at and past the budget
illegalthe status the specification requiresinjected corruption

Eight patterns chosen by shape, at exactly nominal baud:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- 1. legal: eight patterns at exactly nominal baud
  pass all eight delivered
  pass all eight match the prediction

This is the least informative test in the chapter and it must still be run first. It establishes that the environment and the design agree at all, so that a failure anywhere below is attributable to the condition being tested rather than to a broken connection.

2. Marginal Traffic

Chapter 14.2 §5 measured the receiver's tolerance window as −4.5% to +5.5%. A verification plan should test at the claimed boundary and past it, because both directions are informative and only one of them is usually tested.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- 2. marginal: inside and outside the measured window
       +4.0% : 8 of 8 correct
  pass inside the window: all eight correct
       -4.5% : 8 of 8 correct
  pass inside the window: all eight correct
       +6.5% : 5 of 8 correct
  pass outside the window: data is lost
       -7.0% : 1 of 8 correct
  pass outside the window: data is lost

The two checks assert opposite things, and both matter. Inside the window, everything must arrive — that is the specification's promise. Outside it, data must be lost — because a test that only ever asserts success cannot distinguish a receiver with a 10% window from one with a 40% window, and the second would mean the sampling point is not where it is supposed to be.

3. Illegal Traffic

Corruption the receiver must report rather than merely survive. The prediction comes from the specification, not from watching the design:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Predict what a correct receiver must report for a frame the test drove.
task automatic predict_rx(input logic [8:0] data, input int nbits,
                          input int parity_mode, input int err);
    begin
        exp_data[exp_n] = data & ((9'd1 << nbits) - 9'd1);
        // A flipped parity bit must be REPORTED, and the byte still
        // delivered — Chapter 6.4's policy, restated independently here.
        exp_pe[exp_n] = (err == ERR_PARITY) && (parity_mode != P_NONE);
        // A stop interval held at SPACE is a framing error.
        exp_fe[exp_n] = (err == ERR_STOP);
        exp_n++;
    end
endtask
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- 3. illegal: injected corruption, 8E1
  pass both corrupted frames were still delivered
  pass status matched the prediction for both

"Still delivered" is half the requirement and it is the half that gets dropped. Chapter 14.1 §2's R5 says a parity error must be reported and the byte delivered, because the receiver cannot know whether the consumer wants it (Chapter 6.4 §2). A plan that checks only "a parity error is reported" passes a receiver that silently discards the byte, and the resulting data loss is invisible until a consumer that wanted the byte complains.

The predictor encodes both halves, which is why the check catches it: exp_data is populated for a corrupted frame exactly as for a clean one, and only the status bits differ.

A derivation showing three categories of receiver stimulus producing checkable outcomes. Legal traffic is driven at the nominal bit period with well-formed frames, and the predictor states that every byte must be delivered with clean status, which the scoreboard confirms. Marginal traffic is driven at bit periods inside and outside the measured tolerance window; inside the window the predictor requires every byte to arrive, and outside it the check instead requires that data is lost, because a test that only asserts success cannot distinguish a correct window from an unexpectedly wide one. Illegal traffic carries injected corruption, and the predictor states both halves of the requirement: the error must be reported in the per-byte status and the byte must still be delivered. All three paths feed the same scoreboard, and a fourth check follows each error to confirm that the next frame is received cleanly.StimulusReceiverPredictorScoreboardlegal: nominal baud,well-formedevery byte, cleanstatusmarginal: baudinside the windowevery byte muststill arrivemarginal: baudoutside the windowdata MUST be lost —the negative halfillegal: injectedcorruptionerror reported ANDbyte deliveredobserved byte andstatus
Figure 1 — how each category of stimulus turns into a checkable outcome. The three paths use the same predictor and the same scoreboard; only the stimulus differs, which is what keeps the categories comparable.

Framing error, then recovery

16 cycles
A trace of sixteen bit intervals showing an injected framing error followed by a clean frame. The first frame begins with a start bit at the space level, followed by eight data bits and a parity bit. The stop interval is then driven at the space level instead of mark, which is the injected corruption. The receiver's valid output asserts at the end of that frame and its framing error output asserts with it, so the byte is delivered together with a verdict rather than being discarded. The line then returns to mark and the receiver returns to idle rather than attempting to resynchronise within the corrupted frame. A second, well-formed frame follows and is received with both the parity error and framing error outputs low, demonstrating that the damage was confined to the frame in flight.byte delivered WITH its verdictbytedeliver…stop at SPACE — framing errorstop at SPACE — framingerrorback to idle, not huntingback to idle, not huntingnext frame cleannext frame cleanintervalidlestartd0d1d2d3d4d5d6d7parSTOPidlestartstoprx_irx_valid_orx_frame_err_orx_active_ot0t1t2t3t4t5t6t7t8t9t10t11t12t13t14t15
Figure 2 — an injected framing error and the frame that follows. Columns are bit intervals. The stop interval is driven at space rather than mark; the receiver reports a framing error, delivers the byte anyway, and returns to idle rather than hunting mid-frame — so the next frame is clean.

4. Recovery

An error that corrupts one frame is a nuisance; an error that wedges the receiver is a failure. Chapter 14.1's R8 requires the next frame to be clean, and it is a separate check from the error itself:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- 4. recovery: the frame after each error must be clean
  pass frame after a framing error is clean
  pass receiver resynchronises after a break

Break recovery is the one worth testing explicitly. A break holds the line at space for longer than a whole frame, so the receiver sees a start bit, samples nothing but zeros, and finds no stop bit — and then the line is still low. A receiver that tries to resynchronise immediately will keep finding start edges inside the break and produce a stream of bogus frames, which is exactly the cascade Chapter 14.2 §8 had to fix in the monitor.

The design returns to idle and waits for mark (Chapter 6.2's decision that a bad stop is not retried), so a single clean frame after the break is the whole test.

5. What the Receiver Is Not Required To Do

A verification plan has to be as clear about non-requirements, or it will report defects that are not.

It is not required to reject a narrow glitch. This receiver samples once at the bit centre (Chapter 6.3), so it has no glitch immunity by construction. Chapter 5.4 describes majority voting as the alternative and this design does not implement it. A test that injects a spike and demands clean data is testing a feature the design does not claim — and Chapter 14.6 §3 measures exactly what the omission costs.

It is not required to detect every parity error. Chapter 3.3 established that single-bit parity catches odd numbers of errors and misses even ones. A test that flips two bits and expects a parity error is asserting something false.

It is not required to keep the byte that overran. When the queue is full the incoming character is dropped and reported as overrun (Chapter 9.3); which byte is lost — the oldest or the newest — is a design decision that the plan must state rather than assume.

6. The Monitor Is a Receiver, Written From the Specification

Verifying a receiver needs a second receiver — one built from the standard rather than from the design — and that is exactly what the line monitor is. It finds a start edge, waits one and a half of its own bit periods, samples at one-bit intervals, and checks the stop. The same algorithm the DUT implements, written independently.

That independence is the whole value, and it is enforced by the port list:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ONE input, and it is the wire.
module uart_line_monitor (input logic rx_i);

No clock. No oversample tick. No hierarchical path into the design. A monitor that shared the DUT's timing could not report that the DUT's timing was wrong, because it would have followed it there.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
//===========================================================================
//  uart_line_monitor — the monitor of Chapter 14.2/14.5, in SystemVerilog
//
//  NOT SYNTHESIZABLE. It recovers frames from the wire, KNOWING ONLY THE
//  WIRE.
//
//  ONE INPUT PORT, AND IT IS THE LINE. No clock, no oversample tick, no
//  hierarchical path into the design. That is not minimalism -- it is the
//  property that makes a mismatch informative. A monitor with access to the
//  DUT's timing cannot report that the DUT's timing is wrong, because it
//  followed it there.
//
//  IT RUNS ON ITS OWN NOMINAL BIT PERIOD. It finds a start edge, waits one
//  and a half of ITS OWN bit periods, and samples at one-bit intervals --
//  the same algorithm a real receiver uses, implemented from the
//  specification rather than from the RTL.
//
//  IT DEFINES ITS OWN PARITY. It does not import the design's parity
//  function, because if that function were wrong, importing it would make
//  the checker agree with the bug.
//
//  IT BUFFERS RATHER THAN DEMANDING TO BE CONSUMED. A monitor that raises an
//  event per frame and expects the test to take it immediately couples the
//  checker to the driver's timing -- and that coupling produced a real
//  failure in Chapter 14.2: the test resumed as soon as a frame was
//  OBSERVED, which is before the driver has finished its stop bit, so the
//  next send overlapped the previous one.
//===========================================================================
`timescale 1ns/1ps

module uart_line_monitor (input wire rx_i);

    localparam P_NONE = 0, P_EVEN = 1, P_ODD = 2, P_MARK = 3;

    // Configuration. These are variables, not parameters, so a test can
    // change the monitor's idea of the bit rate mid-run -- which is how the
    // baud-tolerance sweep is done.
    real    nominal_tbit_ns = 8680.5556;   // 115200 baud
    int nbits           = 8;
    int parity_mode     = P_NONE;
    logic     enabled         = 1'b0;

    // The observation buffer.
    logic [8:0] q_data [0:255];
    logic       q_pe   [0:255];             // parity error, as THIS monitor sees it
    logic       q_fe   [0:255];             // framing error: stop was not MARK
    int   q_n = 0;
    int   n_start_edges = 0;          // every falling edge while enabled

    // Parity by reduction XOR over the recovered bits. Independent of both
    // the design and the line driver.
    function exp_parity;
        input [8:0] d;
        input int n;
        input int mode;
        int i;
        logic x;
        begin
            x = 1'b0;
            for (i = 0; i < n; i = i + 1) x = x ^ d[i];
            case (mode)
                P_EVEN : exp_parity = x;
                P_ODD  : exp_parity = ~x;
                P_MARK : exp_parity = 1'b1;
                default: exp_parity = 1'b0;
            endcase
        end
    endfunction

    task clear;
        begin q_n = 0; n_start_edges = 0; end
    endtask

    logic [8:0] d;
    logic       pbit, sbit;
    int   i;

    always begin
        @(negedge rx_i);
        if (!enabled) begin
            // not listening: ignore this edge entirely
        end else begin
            n_start_edges++;

            // One and a half of the monitor's OWN bit periods lands in the
            // middle of the first data bit -- if the far end agrees about
            // what a bit period is. When it does not, this is exactly where
            // the disagreement shows up.
            #(nominal_tbit_ns * 1.5);

            d = 9'b0;
            for (i = 0; i < nbits; i = i + 1) begin
                d[i] = rx_i;
                if (i < nbits - 1) #(nominal_tbit_ns);
            end

            pbit = 1'b0;
            if (parity_mode != P_NONE) begin
                #(nominal_tbit_ns);
                pbit = rx_i;
            end

            #(nominal_tbit_ns);
            sbit = rx_i;                  // should be MARK

            if (q_n < 256) begin
                q_data[q_n] = d;
                q_pe[q_n]   = (parity_mode != P_NONE) &&
                              (pbit !== exp_parity(d, nbits, parity_mode));
                q_fe[q_n]   = (sbit !== 1'b1);
                q_n++;
            end

            // Wait for the line to return to MARK before re-arming, so a
            // break -- which holds SPACE for many bit times -- is reported
            // once rather than re-triggering on its own level.
            //
            // THIS GUARD IS REDUNDANT WITH THE EDGE-TRIGGERED ARMING ABOVE,
            // and the mutation campaign is what established that. Waiting on
            // a falling EDGE already cannot re-trigger during a continuous
            // SPACE, because there is no second falling edge to trigger on.
            // Removing this loop alone changes nothing; arming on a LEVEL
            // instead of an edge alone changes nothing either. Remove BOTH
            // and the break is reported once per quarter bit -- four checks
            // fail.
            //
            // Both are kept: two cheap guards on a property worth having is
            // a reasonable trade. But it is worth knowing that a suite of
            // single-point mutations cannot tell you which one is
            // load-bearing, because neither one is.
            while (rx_i !== 1'b1) #(nominal_tbit_ns / 4.0);
        end
    end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
//===========================================================================
//  uart_line_monitor_v — the monitor of Chapter 14.2/14.5, in Verilog-2001
//
//  NOT SYNTHESIZABLE. It recovers frames from the wire, KNOWING ONLY THE
//  WIRE.
//
//  ONE INPUT PORT, AND IT IS THE LINE. No clock, no oversample tick, no
//  hierarchical path into the design. That is not minimalism -- it is the
//  property that makes a mismatch informative. A monitor with access to the
//  DUT's timing cannot report that the DUT's timing is wrong, because it
//  followed it there.
//
//  IT RUNS ON ITS OWN NOMINAL BIT PERIOD. It finds a start edge, waits one
//  and a half of ITS OWN bit periods, and samples at one-bit intervals --
//  the same algorithm a real receiver uses, implemented from the
//  specification rather than from the RTL.
//
//  IT DEFINES ITS OWN PARITY. It does not import the design's parity
//  function, because if that function were wrong, importing it would make
//  the checker agree with the bug.
//
//  IT BUFFERS RATHER THAN DEMANDING TO BE CONSUMED. A monitor that raises an
//  event per frame and expects the test to take it immediately couples the
//  checker to the driver's timing -- and that coupling produced a real
//  failure in Chapter 14.2: the test resumed as soon as a frame was
//  OBSERVED, which is before the driver has finished its stop bit, so the
//  next send overlapped the previous one.
//===========================================================================
`timescale 1ns/1ps

module uart_line_monitor_v (input wire rx_i);

    localparam P_NONE = 0, P_EVEN = 1, P_ODD = 2, P_MARK = 3;

    // Configuration. These are variables, not parameters, so a test can
    // change the monitor's idea of the bit rate mid-run -- which is how the
    // baud-tolerance sweep is done.
    real    nominal_tbit_ns = 8680.5556;   // 115200 baud
    integer nbits           = 8;
    integer parity_mode     = P_NONE;
    reg     enabled         = 1'b0;

    // The observation buffer.
    reg [8:0] q_data [0:255];
    reg       q_pe   [0:255];             // parity error, as THIS monitor sees it
    reg       q_fe   [0:255];             // framing error: stop was not MARK
    integer   q_n = 0;
    integer   n_start_edges = 0;          // every falling edge while enabled

    // Parity by reduction XOR over the recovered bits. Independent of both
    // the design and the line driver.
    function exp_parity;
        input [8:0] d;
        input integer n;
        input integer mode;
        integer i;
        reg x;
        begin
            x = 1'b0;
            for (i = 0; i < n; i = i + 1) x = x ^ d[i];
            case (mode)
                P_EVEN : exp_parity = x;
                P_ODD  : exp_parity = ~x;
                P_MARK : exp_parity = 1'b1;
                default: exp_parity = 1'b0;
            endcase
        end
    endfunction

    task clear;
        begin q_n = 0; n_start_edges = 0; end
    endtask

    reg [8:0] d;
    reg       pbit, sbit;
    integer   i;

    always begin
        @(negedge rx_i);
        if (!enabled) begin
            // not listening: ignore this edge entirely
        end else begin
            n_start_edges = n_start_edges + 1;

            // One and a half of the monitor's OWN bit periods lands in the
            // middle of the first data bit -- if the far end agrees about
            // what a bit period is. When it does not, this is exactly where
            // the disagreement shows up.
            #(nominal_tbit_ns * 1.5);

            d = 9'b0;
            for (i = 0; i < nbits; i = i + 1) begin
                d[i] = rx_i;
                if (i < nbits - 1) #(nominal_tbit_ns);
            end

            pbit = 1'b0;
            if (parity_mode != P_NONE) begin
                #(nominal_tbit_ns);
                pbit = rx_i;
            end

            #(nominal_tbit_ns);
            sbit = rx_i;                  // should be MARK

            if (q_n < 256) begin
                q_data[q_n] = d;
                q_pe[q_n]   = (parity_mode != P_NONE) &&
                              (pbit !== exp_parity(d, nbits, parity_mode));
                q_fe[q_n]   = (sbit !== 1'b1);
                q_n = q_n + 1;
            end

            // Wait for the line to return to MARK before re-arming, so a
            // break -- which holds SPACE for many bit times -- is reported
            // once rather than re-triggering on its own level.
            //
            // THIS GUARD IS REDUNDANT WITH THE EDGE-TRIGGERED ARMING ABOVE,
            // and the mutation campaign is what established that. Waiting on
            // a falling EDGE already cannot re-trigger during a continuous
            // SPACE, because there is no second falling edge to trigger on.
            // Removing this loop alone changes nothing; arming on a LEVEL
            // instead of an edge alone changes nothing either. Remove BOTH
            // and the break is reported once per quarter bit -- four checks
            // fail.
            //
            // Both are kept: two cheap guards on a property worth having is
            // a reasonable trade. But it is worth knowing that a suite of
            // single-point mutations cannot tell you which one is
            // load-bearing, because neither one is.
            while (rx_i !== 1'b1) #(nominal_tbit_ns / 4.0);
        end
    end
endmodule

VHDL changes the shape again, and for a reason worth naming. Verilog and SystemVerilog testbenches reach into the monitor hierarchically — mon.q_data[i]. VHDL has no such idiom for an entity's internals, so the observation buffer is exposed through an explicit read port: drive an index, read the frame back. More ceremony, and a clearer statement of what is meant to be observable from outside.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
--===========================================================================
--  uart_line_monitor — the monitor of Chapter 14.2/14.5, in VHDL-2008
--
--  NOT SYNTHESIZABLE. It recovers frames from the wire, KNOWING ONLY THE
--  WIRE.
--
--  ONE LINE INPUT, AND IT IS THE WIRE. No clock, no oversample tick, no
--  path into the design. That is not minimalism -- it is the property that
--  makes a mismatch informative. A monitor with access to the DUT's timing
--  cannot report that the DUT's timing is wrong, because it followed it
--  there.
--
--  IT RUNS ON ITS OWN NOMINAL BIT PERIOD, supplied as a port so a test can
--  change it mid-run. It finds a start edge, waits one and a half of ITS OWN
--  bit periods, and samples at one-bit intervals -- the same algorithm a
--  real receiver uses, implemented from the specification.
--
--  IT DEFINES ITS OWN PARITY, by reduction XOR over the bits it recovered.
--  It does not use the design's parity function, because if that function
--  were wrong, using it would make the checker agree with the bug.
--
--  IT BUFFERS RATHER THAN DEMANDING TO BE CONSUMED. A monitor that raises an
--  event per frame and expects the test to take it immediately couples the
--  checker to the driver's timing.
--
--  VHDL note on SHAPE. Verilog and SystemVerilog testbenches reach into the
--  monitor hierarchically -- `mon.q_data[i]`. VHDL has no such idiom for an
--  entity's internals (VHDL-2008 external names could do it, but they force
--  the array type into the testbench's namespace). So the buffer is exposed
--  through an explicit READ PORT: drive rd_index_i, read the frame back.
--  More ceremony, and a clearer contract about what is observable.
--===========================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

use work.uart_bfm_pkg.all;

entity uart_line_monitor is
    generic (
        DEPTH : positive := 512
    );
    port (
        rx_i           : in  std_logic;

        -- configuration: ports, not generics, so a test can sweep them
        nominal_tbit_i : in  time;
        nbits_i        : in  natural;
        parity_mode_i  : in  natural;
        enabled_i      : in  std_logic;
        clear_i        : in  std_logic;   -- rising edge empties the buffer

        -- the observation buffer, read by index
        rd_index_i     : in  natural;
        rd_data_o      : out std_logic_vector(8 downto 0);
        rd_pe_o        : out std_logic;
        rd_fe_o        : out std_logic;

        n_frames_o     : out natural;
        n_edges_o      : out natural
    );
end entity uart_line_monitor;

architecture model of uart_line_monitor is

    type byte_arr is array (0 to DEPTH-1) of std_logic_vector(8 downto 0);
    type bit_arr  is array (0 to DEPTH-1) of std_logic;

    signal q_data : byte_arr := (others => (others => '0'));
    signal q_pe   : bit_arr  := (others => '0');
    signal q_fe   : bit_arr  := (others => '0');
    signal q_n    : natural  := 0;
    signal n_edge : natural  := 0;

    -- Parity by reduction XOR over the RECOVERED bits. Independent of both
    -- the design and the line driver.
    function exp_parity(d : std_logic_vector; n : natural; mode : natural)
        return std_logic is
        variable x : std_logic := '0';
    begin
        for i in 0 to n-1 loop
            x := x xor d(i);
        end loop;
        case mode is
            when P_EVEN => return x;
            when P_ODD  => return not x;
            when P_MARK => return '1';
            when others => return '0';
        end case;
    end function exp_parity;

begin

    rd_data_o  <= q_data(rd_index_i) when rd_index_i < DEPTH
                  else (others => '0');
    rd_pe_o    <= q_pe(rd_index_i)   when rd_index_i < DEPTH else '0';
    rd_fe_o    <= q_fe(rd_index_i)   when rd_index_i < DEPTH else '0';
    n_frames_o <= q_n;
    n_edges_o  <= n_edge;

    recover : process
        variable d    : std_logic_vector(8 downto 0);
        variable pbit : std_logic;
        variable sbit : std_logic;
    begin
        -- Waiting on clear_i as well as the line means the buffer can be
        -- emptied between tests without a second process driving q_n, which
        -- an unresolved type would forbid.
        wait until falling_edge(rx_i) or rising_edge(clear_i);

        if clear_i = '1' then
            q_n    <= 0;
            n_edge <= 0;
        elsif enabled_i = '1' then
            n_edge <= n_edge + 1;

            -- One and a half of the monitor's OWN bit periods lands in the
            -- middle of the first data bit -- if the far end agrees about
            -- what a bit period is. When it does not, this is exactly where
            -- the disagreement shows up.
            wait for nominal_tbit_i * 1.5;

            d := (others => '0');
            for i in 0 to nbits_i-1 loop
                d(i) := rx_i;
                if i < nbits_i-1 then wait for nominal_tbit_i; end if;
            end loop;

            pbit := '0';
            if parity_mode_i /= P_NONE then
                wait for nominal_tbit_i;
                pbit := rx_i;
            end if;

            wait for nominal_tbit_i;
            sbit := rx_i;                      -- should be MARK

            if q_n < DEPTH then
                q_data(q_n) <= d;
                if parity_mode_i /= P_NONE
                   and pbit /= exp_parity(d, nbits_i, parity_mode_i) then
                    q_pe(q_n) <= '1';
                else
                    q_pe(q_n) <= '0';
                end if;
                if sbit /= '1' then q_fe(q_n) <= '1'; else q_fe(q_n) <= '0'; end if;
                q_n <= q_n + 1;
            end if;

            -- Wait for MARK before re-arming, so a break -- which holds
            -- SPACE for many bit times -- is reported once rather than
            -- retriggering on its own level.
            --
            -- THIS GUARD IS REDUNDANT WITH THE EDGE-TRIGGERED ARMING ABOVE,
            -- and the mutation campaign is what established that. Waiting on
            -- a falling EDGE already cannot re-trigger during a continuous
            -- SPACE, because there is no second falling edge to trigger on.
            -- Removing this loop alone changes nothing; arming on a LEVEL
            -- instead of an edge alone changes nothing either. Remove BOTH
            -- and the break is reported once per quarter bit -- four checks
            -- fail.
            --
            -- Both are kept: two cheap guards on a property worth having is
            -- a reasonable trade. But it is worth knowing that a suite of
            -- single-point mutations cannot tell you which one is
            -- load-bearing, because neither one is.
            while rx_i /= '1' loop
                wait for nominal_tbit_i / 4;
            end loop;
        end if;
    end process recover;

end architecture model;

7. Measuring the Tolerance Instead of Assuming It

Chapter 2.5 derived the receiver's timing budget on paper. This measures it.

The driver's bit period is swept from −10% to +10% of nominal while the monitor's stays fixed, and each offset is scored on whether the byte comes back intact. The prediction is arithmetic: the monitor samples the last data bit 8.5 bit periods after the start edge, so accumulated error must stay under half a bit —

0.5 / 8.5 = 5.88%

and the measurement:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  [info] recovery window: -6% to 6% (13 of 21 offsets OK)
  PASS the sweep found BOTH working and failing rates -- a real edge exists
  PASS recovery survives at least +/-4% of baud error
  PASS and fails beyond +/-7%, as 0.5/8.5 = 5.9% predicts

±6%, from a prediction of ±5.88%, and the same number falls out of the Verilog, SystemVerilog and VHDL runs independently. That agreement is worth more than either number alone: the arithmetic and three separate implementations all land in the same place.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
//===========================================================================
//  tb_uart_line_monitor — self-checking SystemVerilog testbench
//
//  Driver into monitor, NO DUT ANYWHERE (Chapter 14.2 section 4).
//
//  This is the test that has to run before the environment is ever pointed
//  at a design, and the reason is blunt: if the checker is wrong, every
//  subsequent result is noise. Two of the three defects found while building
//  this BFM were in the BFM, and both would have presented as DUT failures.
//
//  WHAT A CLOSED LOOP CAN AND CANNOT PROVE, stated up front:
//    CAN    -- that the driver and monitor agree about the frame format, the
//              bit order, the parity rule and the error conditions; and that
//              the monitor's timing window is what it claims to be.
//    CANNOT -- that both are right. Two components wrong in the SAME way
//              agree perfectly. That gap is closed elsewhere: the driver is
//              checked slot-by-slot against an independent reference model
//              in tb_uart_line_driver, and the model is checked exhaustively
//              against the frame definition in tb_uart_predictor.
//
//  The sharpest test here is the last one: a monitor given the WRONG bit
//  period must FAIL to recover. A monitor that still succeeds is following
//  the driver rather than measuring it, and would follow a DUT into any
//  timing error it made.
//===========================================================================
`timescale 1ns/1ps

module tb_uart_line_monitor;

    localparam P_NONE = 0, P_EVEN = 1, P_ODD = 2, P_MARK = 3;
    localparam ERR_NONE = 0, ERR_PARITY = 1, ERR_STOP = 2,
               ERR_SHORT_STOP = 3, ERR_GLITCH = 4, ERR_BREAK = 5;

    wire tx;
    uart_line_driver  drv (.tx_o(tx));
    uart_line_monitor mon (.rx_i(tx));

    real TB = 1000.0;                  // 1 us per bit

    int checks = 0, failures = 0;
    task automatic check(input logic cond, input string name);
        checks++;
        if (cond) $display("  PASS %0s", name);
        else begin failures++; $display("  FAIL %0s", name); end
    endtask

    int i, dv, bad, ok_lo, ok_hi, n_ok, n_bad;
    real    e, tdrv;

    // Drive one frame and let the line settle before the next.
    task drive;
        input [8:0] d; input int nb; input int pm;
        input int sh; input real tbit; input int err;
        begin
            drv.send_frame(d, nb, pm, sh, tbit, err);
            drv.idle_for(tbit, 4.0);
        end
    endtask

    initial begin
        #500_000_000;
        $display("  FAIL watchdog: simulation did not finish");
        $display("== %0d checks, %0d failures ==", checks+1, failures+1);
        $display("   RESULT: SYSTEMVERILOG MONITOR TESTS FAILED (timeout)");
        $finish;
    end

    initial begin
        $display("== uart_line_monitor : self-checking SystemVerilog testbench ==");
        mon.nominal_tbit_ns = TB;
        mon.nbits           = 8;
        mon.parity_mode     = P_NONE;
        mon.enabled         = 1'b1;
        mon.clear;
        #100;

        //=== 8N1, clean ====================================================
        drive(9'h055, 8, P_NONE, 2, TB, ERR_NONE);
        drive(9'h000, 8, P_NONE, 2, TB, ERR_NONE);
        drive(9'h0FF, 8, P_NONE, 2, TB, ERR_NONE);
        drive(9'h0A5, 8, P_NONE, 2, TB, ERR_NONE);
        check(mon.q_n == 4, "the monitor observed all four frames driven");
        check(mon.q_data[0] == 9'h055 && mon.q_data[1] == 9'h000 &&
              mon.q_data[2] == 9'h0FF && mon.q_data[3] == 9'h0A5,
              "8N1: every byte recovered, in order");
        bad = 0;
        for (i = 0; i < 4; i = i + 1) if (mon.q_fe[i] || mon.q_pe[i]) bad++;
        check(bad == 0, "and none of them reported an error");

        //=== every 8-bit value round-trips ==================================
        mon.clear;
        for (dv = 0; dv < 256; dv = dv + 1)
            drive(dv[8:0], 8, P_NONE, 2, TB, ERR_NONE);
        check(mon.q_n == 256, "all 256 byte values were observed");
        bad = 0;
        for (i = 0; i < 256; i = i + 1)
            if (mon.q_data[i] !== i[8:0] || mon.q_fe[i] || mon.q_pe[i]) bad++;
        check(bad == 0, "and every one of the 256 recovered exactly, error-free");

        //=== 8E1, clean and corrupted ========================================
        mon.parity_mode = P_EVEN; mon.clear;
        drive(9'h0A5, 8, P_EVEN, 2, TB, ERR_NONE);
        check(mon.q_n == 1 && mon.q_data[0] == 9'h0A5 && !mon.q_pe[0],
              "8E1 clean: byte recovered, no parity error");
        drive(9'h0A5, 8, P_EVEN, 2, TB, ERR_PARITY);
        check(mon.q_n == 2 && mon.q_data[1] == 9'h0A5,
              "8E1 with a flipped parity bit: the DATA still recovers");
        check(mon.q_pe[1] === 1'b1, "and the monitor reports the parity error");
        check(mon.q_fe[1] === 1'b0, "and does NOT confuse it with a framing error");

        //=== a framing error ==================================================
        mon.parity_mode = P_NONE; mon.clear;
        drive(9'h03C, 8, P_NONE, 2, TB, ERR_STOP);
        check(mon.q_n == 1 && mon.q_fe[0] === 1'b1,
              "stop held at SPACE is reported as a framing error");
        check(mon.q_data[0] == 9'h03C, "with the data bits still recovered");

        //=== other frame formats ==============================================
        mon.nbits = 7; mon.parity_mode = P_ODD; mon.clear;
        drive(9'h02A, 7, P_ODD, 2, TB, ERR_NONE);
        check(mon.q_n == 1 && mon.q_data[0] == 9'h02A && !mon.q_pe[0],
              "7O1 0x2A recovered with correct odd parity");

        mon.nbits = 5; mon.parity_mode = P_NONE; mon.clear;
        drive(9'h015, 5, P_NONE, 2, TB, ERR_NONE);
        check(mon.q_n == 1 && mon.q_data[0] == 9'h015, "5N1 0x15 recovered");

        mon.nbits = 9; mon.parity_mode = P_NONE; mon.clear;
        drive(9'h1AA, 9, P_NONE, 2, TB, ERR_NONE);
        check(mon.q_n == 1 && mon.q_data[0] == 9'h1AA,
              "9N1 0x1AA recovered -- the ninth bit is carried");

        //=== a break is reported ONCE =========================================
        // The monitor waits for MARK before re-arming, so a long SPACE does
        // not retrigger on its own level.
        mon.nbits = 8; mon.parity_mode = P_NONE; mon.clear;
        drive(9'h000, 8, P_NONE, 2, TB, ERR_BREAK);
        check(mon.n_start_edges == 1,
              "a break produced exactly ONE start edge, not one per bit time");
        check(mon.q_n == 1 && mon.q_fe[0] === 1'b1,
              "and is reported as a single framing error");

        //=== THE baud-tolerance measurement ===================================
        // The driver's rate is swept while the monitor's stays nominal. The
        // monitor samples the last data bit 8.5 bit periods after the edge,
        // so accumulated error must stay under half a bit: 0.5/8.5 = 5.9%.
        mon.clear;
        ok_lo = 0; ok_hi = 0; n_ok = 0; n_bad = 0;
        for (i = -10; i <= 10; i = i + 1) begin
            e     = i / 100.0;
            tdrv  = TB * (1.0 + e);
            mon.clear;
            drive(9'h0A5, 8, P_NONE, 2, tdrv, ERR_NONE);
            if (mon.q_n == 1 && mon.q_data[0] == 9'h0A5 && !mon.q_fe[0]) begin
                n_ok++;
                if (i < ok_lo) ok_lo = i;
                if (i > ok_hi) ok_hi = i;
            end else n_bad++;
        end
        $display("  [info] recovery window: %0d%% to %0d%% (%0d of 21 offsets OK)",
                 ok_lo, ok_hi, n_ok);
        check(n_ok > 0 && n_bad > 0,
              "the sweep found BOTH working and failing rates -- a real edge exists");
        check(ok_lo <= -4 && ok_hi >= 4,
              "recovery survives at least +/-4% of baud error");
        check(ok_lo >= -7 && ok_hi <= 7,
              "and fails beyond +/-7%, as 0.5/8.5 = 5.9% predicts");

        //=== the monitor is NOT following the driver ===========================
        // Give the monitor a badly wrong idea of the bit period while the
        // driver stays nominal. It must FAIL. A monitor that still recovers
        // is synchronising to the wire it is supposed to be judging.
        mon.nominal_tbit_ns = TB * 1.30;
        mon.clear;
        drive(9'h0A5, 8, P_NONE, 2, TB, ERR_NONE);
        check(!(mon.q_n == 1 && mon.q_data[0] == 9'h0A5 && !mon.q_fe[0]),
              "a monitor told the WRONG bit period fails -- it measures, not follows");
        mon.nominal_tbit_ns = TB;
        mon.clear;
        drive(9'h0A5, 8, P_NONE, 2, TB, ERR_NONE);
        check(mon.q_n == 1 && mon.q_data[0] == 9'h0A5,
              "and recovers again once told the truth");

        $display("== %0d checks, %0d failures ==", checks, failures);
        if (failures == 0) $display("   RESULT: ALL SYSTEMVERILOG MONITOR TESTS PASSED");
        else               $display("   RESULT: SYSTEMVERILOG MONITOR TESTS FAILED");
        $finish;
    end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
//===========================================================================
//  tb_uart_line_monitor_v — self-checking Verilog-2001 testbench
//
//  Driver into monitor, NO DUT ANYWHERE (Chapter 14.2 section 4).
//
//  This is the test that has to run before the environment is ever pointed
//  at a design, and the reason is blunt: if the checker is wrong, every
//  subsequent result is noise. Two of the three defects found while building
//  this BFM were in the BFM, and both would have presented as DUT failures.
//
//  WHAT A CLOSED LOOP CAN AND CANNOT PROVE, stated up front:
//    CAN    -- that the driver and monitor agree about the frame format, the
//              bit order, the parity rule and the error conditions; and that
//              the monitor's timing window is what it claims to be.
//    CANNOT -- that both are right. Two components wrong in the SAME way
//              agree perfectly. That gap is closed elsewhere: the driver is
//              checked slot-by-slot against an independent reference model
//              in tb_uart_line_driver, and the model is checked exhaustively
//              against the frame definition in tb_uart_predictor.
//
//  The sharpest test here is the last one: a monitor given the WRONG bit
//  period must FAIL to recover. A monitor that still succeeds is following
//  the driver rather than measuring it, and would follow a DUT into any
//  timing error it made.
//===========================================================================
`timescale 1ns/1ps

module tb_uart_line_monitor_v;

    localparam P_NONE = 0, P_EVEN = 1, P_ODD = 2, P_MARK = 3;
    localparam ERR_NONE = 0, ERR_PARITY = 1, ERR_STOP = 2,
               ERR_SHORT_STOP = 3, ERR_GLITCH = 4, ERR_BREAK = 5;

    wire tx;
    uart_line_driver_v  drv (.tx_o(tx));
    uart_line_monitor_v mon (.rx_i(tx));

    real TB = 1000.0;                  // 1 us per bit

    integer checks = 0, failures = 0;
    task check;
        input cond;
        input [8*80-1:0] name;
        begin
            checks = checks + 1;
            if (cond) $display("  PASS %0s", name);
            else begin failures = failures + 1; $display("  FAIL %0s", name); end
        end
    endtask

    integer i, dv, bad, ok_lo, ok_hi, n_ok, n_bad;
    real    e, tdrv;

    // Drive one frame and let the line settle before the next.
    task drive;
        input [8:0] d; input integer nb; input integer pm;
        input integer sh; input real tbit; input integer err;
        begin
            drv.send_frame(d, nb, pm, sh, tbit, err);
            drv.idle_for(tbit, 4.0);
        end
    endtask

    initial begin
        #500_000_000;
        $display("  FAIL watchdog: simulation did not finish");
        $display("== %0d checks, %0d failures ==", checks+1, failures+1);
        $display("   RESULT: VERILOG MONITOR TESTS FAILED (timeout)");
        $finish;
    end

    initial begin
        $display("== uart_line_monitor_v : self-checking Verilog testbench ==");
        mon.nominal_tbit_ns = TB;
        mon.nbits           = 8;
        mon.parity_mode     = P_NONE;
        mon.enabled         = 1'b1;
        mon.clear;
        #100;

        //=== 8N1, clean ====================================================
        drive(9'h055, 8, P_NONE, 2, TB, ERR_NONE);
        drive(9'h000, 8, P_NONE, 2, TB, ERR_NONE);
        drive(9'h0FF, 8, P_NONE, 2, TB, ERR_NONE);
        drive(9'h0A5, 8, P_NONE, 2, TB, ERR_NONE);
        check(mon.q_n == 4, "the monitor observed all four frames driven");
        check(mon.q_data[0] == 9'h055 && mon.q_data[1] == 9'h000 &&
              mon.q_data[2] == 9'h0FF && mon.q_data[3] == 9'h0A5,
              "8N1: every byte recovered, in order");
        bad = 0;
        for (i = 0; i < 4; i = i + 1) if (mon.q_fe[i] || mon.q_pe[i]) bad = bad + 1;
        check(bad == 0, "and none of them reported an error");

        //=== every 8-bit value round-trips ==================================
        mon.clear;
        for (dv = 0; dv < 256; dv = dv + 1)
            drive(dv[8:0], 8, P_NONE, 2, TB, ERR_NONE);
        check(mon.q_n == 256, "all 256 byte values were observed");
        bad = 0;
        for (i = 0; i < 256; i = i + 1)
            if (mon.q_data[i] !== i[8:0] || mon.q_fe[i] || mon.q_pe[i]) bad = bad + 1;
        check(bad == 0, "and every one of the 256 recovered exactly, error-free");

        //=== 8E1, clean and corrupted ========================================
        mon.parity_mode = P_EVEN; mon.clear;
        drive(9'h0A5, 8, P_EVEN, 2, TB, ERR_NONE);
        check(mon.q_n == 1 && mon.q_data[0] == 9'h0A5 && !mon.q_pe[0],
              "8E1 clean: byte recovered, no parity error");
        drive(9'h0A5, 8, P_EVEN, 2, TB, ERR_PARITY);
        check(mon.q_n == 2 && mon.q_data[1] == 9'h0A5,
              "8E1 with a flipped parity bit: the DATA still recovers");
        check(mon.q_pe[1] === 1'b1, "and the monitor reports the parity error");
        check(mon.q_fe[1] === 1'b0, "and does NOT confuse it with a framing error");

        //=== a framing error ==================================================
        mon.parity_mode = P_NONE; mon.clear;
        drive(9'h03C, 8, P_NONE, 2, TB, ERR_STOP);
        check(mon.q_n == 1 && mon.q_fe[0] === 1'b1,
              "stop held at SPACE is reported as a framing error");
        check(mon.q_data[0] == 9'h03C, "with the data bits still recovered");

        //=== other frame formats ==============================================
        mon.nbits = 7; mon.parity_mode = P_ODD; mon.clear;
        drive(9'h02A, 7, P_ODD, 2, TB, ERR_NONE);
        check(mon.q_n == 1 && mon.q_data[0] == 9'h02A && !mon.q_pe[0],
              "7O1 0x2A recovered with correct odd parity");

        mon.nbits = 5; mon.parity_mode = P_NONE; mon.clear;
        drive(9'h015, 5, P_NONE, 2, TB, ERR_NONE);
        check(mon.q_n == 1 && mon.q_data[0] == 9'h015, "5N1 0x15 recovered");

        mon.nbits = 9; mon.parity_mode = P_NONE; mon.clear;
        drive(9'h1AA, 9, P_NONE, 2, TB, ERR_NONE);
        check(mon.q_n == 1 && mon.q_data[0] == 9'h1AA,
              "9N1 0x1AA recovered -- the ninth bit is carried");

        //=== a break is reported ONCE =========================================
        // The monitor waits for MARK before re-arming, so a long SPACE does
        // not retrigger on its own level.
        mon.nbits = 8; mon.parity_mode = P_NONE; mon.clear;
        drive(9'h000, 8, P_NONE, 2, TB, ERR_BREAK);
        check(mon.n_start_edges == 1,
              "a break produced exactly ONE start edge, not one per bit time");
        check(mon.q_n == 1 && mon.q_fe[0] === 1'b1,
              "and is reported as a single framing error");

        //=== THE baud-tolerance measurement ===================================
        // The driver's rate is swept while the monitor's stays nominal. The
        // monitor samples the last data bit 8.5 bit periods after the edge,
        // so accumulated error must stay under half a bit: 0.5/8.5 = 5.9%.
        mon.clear;
        ok_lo = 0; ok_hi = 0; n_ok = 0; n_bad = 0;
        for (i = -10; i <= 10; i = i + 1) begin
            e     = i / 100.0;
            tdrv  = TB * (1.0 + e);
            mon.clear;
            drive(9'h0A5, 8, P_NONE, 2, tdrv, ERR_NONE);
            if (mon.q_n == 1 && mon.q_data[0] == 9'h0A5 && !mon.q_fe[0]) begin
                n_ok = n_ok + 1;
                if (i < ok_lo) ok_lo = i;
                if (i > ok_hi) ok_hi = i;
            end else n_bad = n_bad + 1;
        end
        $display("  [info] recovery window: %0d%% to %0d%% (%0d of 21 offsets OK)",
                 ok_lo, ok_hi, n_ok);
        check(n_ok > 0 && n_bad > 0,
              "the sweep found BOTH working and failing rates -- a real edge exists");
        check(ok_lo <= -4 && ok_hi >= 4,
              "recovery survives at least +/-4% of baud error");
        check(ok_lo >= -7 && ok_hi <= 7,
              "and fails beyond +/-7%, as 0.5/8.5 = 5.9% predicts");

        //=== the monitor is NOT following the driver ===========================
        // Give the monitor a badly wrong idea of the bit period while the
        // driver stays nominal. It must FAIL. A monitor that still recovers
        // is synchronising to the wire it is supposed to be judging.
        mon.nominal_tbit_ns = TB * 1.30;
        mon.clear;
        drive(9'h0A5, 8, P_NONE, 2, TB, ERR_NONE);
        check(!(mon.q_n == 1 && mon.q_data[0] == 9'h0A5 && !mon.q_fe[0]),
              "a monitor told the WRONG bit period fails -- it measures, not follows");
        mon.nominal_tbit_ns = TB;
        mon.clear;
        drive(9'h0A5, 8, P_NONE, 2, TB, ERR_NONE);
        check(mon.q_n == 1 && mon.q_data[0] == 9'h0A5,
              "and recovers again once told the truth");

        $display("== %0d checks, %0d failures ==", checks, failures);
        if (failures == 0) $display("   RESULT: ALL VERILOG MONITOR TESTS PASSED");
        else               $display("   RESULT: VERILOG MONITOR TESTS FAILED");
        $finish;
    end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
--===========================================================================
--  tb_uart_line_monitor — self-checking VHDL-2008 testbench
--
--  Driver into monitor, NO DUT ANYWHERE (Chapter 14.2 section 4).
--
--  This is the test that has to run before the environment is ever pointed
--  at a design, and the reason is blunt: if the checker is wrong, every
--  subsequent result is noise.
--
--  WHAT A CLOSED LOOP CAN AND CANNOT PROVE, stated up front:
--    CAN    -- that the driver and monitor agree about the frame format, the
--              bit order, the parity rule and the error conditions; and that
--              the monitor's timing window is what it claims to be.
--    CANNOT -- that both are right. Two components wrong in the SAME way
--              agree perfectly. That gap is closed elsewhere: the driver is
--              checked slot-by-slot against an independent reference model,
--              and the model is checked exhaustively against the frame
--              definition.
--
--  The sharpest test here is the last one: a monitor given the WRONG bit
--  period must FAIL to recover. A monitor that still succeeds is following
--  the driver rather than measuring it, and would follow a DUT into any
--  timing error it made.
--
--  Same 21 counted checks as the Verilog and SystemVerilog twins.
--===========================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

use work.uart_bfm_pkg.all;

entity tb_uart_line_monitor is
end entity tb_uart_line_monitor;

architecture sim of tb_uart_line_monitor is

    constant TB : time := 1000 ns;          -- 1 us per bit

    signal tx : std_logic := '1';

    signal mon_tbit   : time      := TB;
    signal mon_nbits  : natural   := 8;
    signal mon_parity : natural   := P_NONE;
    signal mon_en     : std_logic := '0';
    signal mon_clear  : std_logic := '0';

    signal rd_index : natural := 0;
    signal rd_data  : std_logic_vector(8 downto 0);
    signal rd_pe, rd_fe : std_logic;
    signal n_frames, n_edges : natural;

begin

    mon : entity work.uart_line_monitor
        generic map (DEPTH => 512)
        port map (rx_i => tx,
                  nominal_tbit_i => mon_tbit, nbits_i => mon_nbits,
                  parity_mode_i => mon_parity, enabled_i => mon_en,
                  clear_i => mon_clear,
                  rd_index_i => rd_index, rd_data_o => rd_data,
                  rd_pe_o => rd_pe, rd_fe_o => rd_fe,
                  n_frames_o => n_frames, n_edges_o => n_edges);

    watchdog : process
    begin
        wait for 500 ms;
        report "watchdog: simulation did not finish" severity failure;
    end process watchdog;

    stim : process
        variable checks, failures : natural := 0;
        variable bad   : natural;
        variable ok_lo, ok_hi, n_ok, n_bad : integer;
        variable tdrv  : time;
        variable good  : boolean;

        procedure check(cond : boolean; name : string) is
        begin
            checks := checks + 1;
            if cond then
                report "  PASS " & name severity note;
            else
                failures := failures + 1;
                report "  FAIL " & name severity error;
            end if;
        end procedure check;

        procedure drive(constant d  : std_logic_vector(8 downto 0);
                        constant nb : natural; constant pm : natural;
                        constant sh : natural; constant tbp : time;
                        constant er : natural) is
        begin
            send_frame(tx, d, nb, pm, sh, tbp, er);
            idle_for(tx, tbp, 4.0);
        end procedure drive;

        procedure mon_reset is
        begin
            mon_clear <= '1';
            wait for 1 ns;
            mon_clear <= '0';
            wait for 1 ns;
        end procedure mon_reset;

        -- Read one observation out of the monitor's buffer through its
        -- read port. Verilog reaches into the instance hierarchically;
        -- VHDL asks through the interface, which is more ceremony and a
        -- clearer statement of what is meant to be observable.
        procedure q_read(constant i : natural;
                         variable d : out std_logic_vector(8 downto 0);
                         variable pe : out std_logic;
                         variable fe : out std_logic) is
        begin
            rd_index <= i;
            wait for 1 ns;
            d  := rd_data;
            pe := rd_pe;
            fe := rd_fe;
        end procedure q_read;

        variable dv : std_logic_vector(8 downto 0);
        variable pe, fe : std_logic;
    begin
        report "== uart_line_monitor : self-checking VHDL testbench ==" severity note;
        mon_tbit   <= TB;
        mon_nbits  <= 8;
        mon_parity <= P_NONE;
        mon_en     <= '1';
        wait for 100 ns;
        mon_reset;

        --=== 8N1, clean ====================================================
        drive('0' & x"55", 8, P_NONE, 2, TB, ERR_NONE);
        drive('0' & x"00", 8, P_NONE, 2, TB, ERR_NONE);
        drive('0' & x"FF", 8, P_NONE, 2, TB, ERR_NONE);
        drive('0' & x"A5", 8, P_NONE, 2, TB, ERR_NONE);
        check(n_frames = 4, "the monitor observed all four frames driven");
        bad := 0;
        q_read(0, dv, pe, fe); if dv /= '0' & x"55" then bad := bad + 1; end if;
        q_read(1, dv, pe, fe); if dv /= '0' & x"00" then bad := bad + 1; end if;
        q_read(2, dv, pe, fe); if dv /= '0' & x"FF" then bad := bad + 1; end if;
        q_read(3, dv, pe, fe); if dv /= '0' & x"A5" then bad := bad + 1; end if;
        check(bad = 0, "8N1: every byte recovered, in order");
        bad := 0;
        for i in 0 to 3 loop
            q_read(i, dv, pe, fe);
            if pe = '1' or fe = '1' then bad := bad + 1; end if;
        end loop;
        check(bad = 0, "and none of them reported an error");

        --=== every 8-bit value round-trips ==================================
        mon_reset;
        for d in 0 to 255 loop
            drive(std_logic_vector(to_unsigned(d, 9)), 8, P_NONE, 2, TB, ERR_NONE);
        end loop;
        check(n_frames = 256, "all 256 byte values were observed");
        bad := 0;
        for i in 0 to 255 loop
            q_read(i, dv, pe, fe);
            if dv /= std_logic_vector(to_unsigned(i, 9)) or pe = '1' or fe = '1' then
                bad := bad + 1;
            end if;
        end loop;
        check(bad = 0, "and every one of the 256 recovered exactly, error-free");

        --=== 8E1, clean and corrupted ========================================
        mon_parity <= P_EVEN; mon_reset;
        drive('0' & x"A5", 8, P_EVEN, 2, TB, ERR_NONE);
        q_read(0, dv, pe, fe);
        check(n_frames = 1 and dv = '0' & x"A5" and pe = '0',
              "8E1 clean: byte recovered, no parity error");
        drive('0' & x"A5", 8, P_EVEN, 2, TB, ERR_PARITY);
        q_read(1, dv, pe, fe);
        check(n_frames = 2 and dv = '0' & x"A5",
              "8E1 with a flipped parity bit: the DATA still recovers");
        check(pe = '1', "and the monitor reports the parity error");
        check(fe = '0', "and does NOT confuse it with a framing error");

        --=== a framing error ==================================================
        mon_parity <= P_NONE; mon_reset;
        drive('0' & x"3C", 8, P_NONE, 2, TB, ERR_STOP);
        q_read(0, dv, pe, fe);
        check(n_frames = 1 and fe = '1',
              "stop held at SPACE is reported as a framing error");
        check(dv = '0' & x"3C", "with the data bits still recovered");

        --=== other frame formats ==============================================
        mon_nbits <= 7; mon_parity <= P_ODD; mon_reset;
        drive('0' & x"2A", 7, P_ODD, 2, TB, ERR_NONE);
        q_read(0, dv, pe, fe);
        check(n_frames = 1 and dv = '0' & x"2A" and pe = '0',
              "7O1 0x2A recovered with correct odd parity");

        mon_nbits <= 5; mon_parity <= P_NONE; mon_reset;
        drive('0' & x"15", 5, P_NONE, 2, TB, ERR_NONE);
        q_read(0, dv, pe, fe);
        check(n_frames = 1 and dv = '0' & x"15", "5N1 0x15 recovered");

        mon_nbits <= 9; mon_parity <= P_NONE; mon_reset;
        drive('1' & x"AA", 9, P_NONE, 2, TB, ERR_NONE);
        q_read(0, dv, pe, fe);
        check(n_frames = 1 and dv = '1' & x"AA",
              "9N1 0x1AA recovered -- the ninth bit is carried");

        --=== a break is reported ONCE =========================================
        mon_nbits <= 8; mon_parity <= P_NONE; mon_reset;
        drive('0' & x"00", 8, P_NONE, 2, TB, ERR_BREAK);
        check(n_edges = 1,
              "a break produced exactly ONE start edge, not one per bit time");
        q_read(0, dv, pe, fe);
        check(n_frames = 1 and fe = '1',
              "and is reported as a single framing error");

        --=== THE baud-tolerance measurement ===================================
        -- The driver's rate is swept while the monitor's stays nominal. The
        -- monitor samples the last data bit 8.5 bit periods after the edge,
        -- so accumulated error must stay under half a bit: 0.5/8.5 = 5.9%.
        ok_lo := 0; ok_hi := 0; n_ok := 0; n_bad := 0;
        for i in -10 to 10 loop
            tdrv := (TB * (100 + i)) / 100;
            mon_reset;
            drive('0' & x"A5", 8, P_NONE, 2, tdrv, ERR_NONE);
            q_read(0, dv, pe, fe);
            good := (n_frames = 1) and (dv = '0' & x"A5") and (fe = '0');
            if good then
                n_ok := n_ok + 1;
                if i < ok_lo then ok_lo := i; end if;
                if i > ok_hi then ok_hi := i; end if;
            else
                n_bad := n_bad + 1;
            end if;
        end loop;
        report "  [info] recovery window: " & integer'image(ok_lo) & "% to "
             & integer'image(ok_hi) & "% (" & integer'image(n_ok)
             & " of 21 offsets OK)" severity note;
        check(n_ok > 0 and n_bad > 0,
              "the sweep found BOTH working and failing rates -- a real edge exists");
        check(ok_lo <= -4 and ok_hi >= 4,
              "recovery survives at least +/-4% of baud error");
        check(ok_lo >= -7 and ok_hi <= 7,
              "and fails beyond +/-7%, as 0.5/8.5 = 5.9% predicts");

        --=== the monitor is NOT following the driver ===========================
        mon_tbit <= (TB * 130) / 100;
        mon_reset;
        drive('0' & x"A5", 8, P_NONE, 2, TB, ERR_NONE);
        q_read(0, dv, pe, fe);
        check(not ((n_frames = 1) and (dv = '0' & x"A5") and (fe = '0')),
              "a monitor told the WRONG bit period fails -- it measures, not follows");
        mon_tbit <= TB;
        mon_reset;
        drive('0' & x"A5", 8, P_NONE, 2, TB, ERR_NONE);
        q_read(0, dv, pe, fe);
        check(n_frames = 1 and dv = '0' & x"A5",
              "and recovers again once told the truth");

        report "== " & integer'image(checks) & " checks, "
                     & integer'image(failures) & " failures ==" severity note;
        if failures = 0 then
            report "   RESULT: ALL VHDL MONITOR TESTS PASSED" severity note;
        else
            report "   RESULT: VHDL MONITOR TESTS FAILED" severity error;
        end if;
        wait;
    end process stim;

end architecture sim;

Twenty-one checks, and all three languages agree — including the measured window:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  PASS all 256 byte values were observed
  PASS and every one of the 256 recovered exactly, error-free
  PASS 8E1 with a flipped parity bit: the DATA still recovers
  PASS and the monitor reports the parity error
  PASS and does NOT confuse it with a framing error
  PASS stop held at SPACE is reported as a framing error
  PASS 7O1 0x2A recovered with correct odd parity
  PASS 9N1 0x1AA recovered -- the ninth bit is carried
  PASS a break produced exactly ONE start edge, not one per bit time
  PASS a monitor told the WRONG bit period fails -- it measures, not follows
  PASS and recovers again once told the truth
== 21 checks, 0 failures ==

Verilog-2001    : 21 checks, 0 failures   window -6% .. +6%
SystemVerilog   : 21 checks, 0 failures   window -6% .. +6%
VHDL-2008       : 21 checks, 0 failures   window -6% .. +6%

8. Verification

Run the legal case first, always. It is the cheapest way to distinguish "the condition under test failed" from "nothing was connected".

Assert the negative half of every tolerance claim. §2 — a bound that is never exceeded has not been demonstrated to be a bound.

Use several patterns at every marginal point. The graded failure in §2 means a single byte's transition density dominates the result. Eight patterns of differing shape is the minimum that says anything.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Assertion — a delivered byte always carries a verdict. The status bits
// are only meaningful in the cycle valid is asserted, and a checker that
// samples them at any other time is reading stale state.
property p_status_valid_with_data;
    @(posedge clk) disable iff (!rst_n)
        rx_valid_o |-> !$isunknown({rx_parity_err_o, rx_frame_err_o});
endproperty

// Assertion — the receiver always returns to idle. If it can be left
// mid-frame by any input, a single corrupted frame wedges the link.
property p_always_returns_to_idle;
    @(posedge clk) disable iff (!rst_n)
        rx_active_o |-> ##[1:$] !rx_active_o;
endproperty

Check recovery after every error class, not just one. Framing, parity and break reach the state machine by different paths, and a receiver can recover from one and not another.

9. Debugging

10. What This Means in Practice

The tolerance window is the number to know on hardware. It is what determines whether a link works with a cheap RC oscillator at the far end, and Chapter 4.5's budget is only useful once it has been checked against the RTL that implements it.

Test at the far end's worst case, not at your own. The budget is shared: both endpoints' errors add. A receiver with a 10% window facing a transmitter that is 3% fast has 2% of margin left for its own clock, not 5%.

Bench-test with a deliberately wrong peer. The equivalent of §2 on hardware is a peer configured one standard rate away, or a signal generator. A link that only ever talks to an identically-configured twin has had its tolerance untested for the same reason loopback cannot check baud.

Keep the illegal cases in the regression. They are the ones that stop working silently when someone simplifies the error path, and they cost nothing to run.

11. Understanding Check

12. Summary

Three categories, and a plan missing one has a hole: legal traffic shows it works, marginal traffic measures the tolerance, illegal traffic exercises the status.

Run legal first so that later failures are attributable.

Assert the negative half of the tolerance claim. Inside the window all eight patterns arrive; outside it, data must be lost — 5 of 8 at +6.5%, 1 of 8 at −7.0% — and without that half a 40% window would pass as a 10% one.

The failure is graded, not binary, because the receiver re-anchors only on the start edge and long runs of identical bits drift furthest. A one-byte tolerance test measures transition density.

A parity error must be reported and the byte delivered. The predictor populates the expected data identically for clean and corrupted frames, so only the status differs — which is what catches a receiver that silently discards.

Recovery is a separate check from the error. Framing, parity and break reach the state machine by different paths, and break is the one worth testing explicitly because the line is still low when the frame ends.

State the non-requirements too — no glitch immunity, no even-error parity detection, a stated overrun policy — or the plan will generate defects that are not.

13. What Comes Next

Both halves have been checked, and both checks leaned on the same two components: a predictor that computes what must happen, and a scoreboard that compares. Chapter 14.5 builds them properly.

The subject is independence: how a monitor reconstructs frames without the design's timing, how a predictor computes expectations without the design's definitions, and what is actually lost when either of those boundaries is crossed for convenience.

Browse the full path on the UART tutorials index. For the timing budget this chapter measured against, read back to Chapter 4.5.

Continue learning

Where this fits

Part of the UART curriculum.