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:
| Category | What it establishes | Stimulus |
|---|---|---|
| legal | the receiver works | nominal baud, well-formed frames |
| marginal | the tolerance the specification claims | baud offsets at and past the budget |
| illegal | the status the specification requires | injected corruption |
1. Legal Traffic
Eight patterns chosen by shape, at exactly nominal baud:
-- 1. legal: eight patterns at exactly nominal baud
pass all eight delivered
pass all eight match the predictionThis 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.
-- 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 lostThe 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:
// 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-- 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.
Framing error, then recovery
16 cycles4. 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:
-- 4. recovery: the frame after each error must be clean
pass frame after a framing error is clean
pass receiver resynchronises after a breakBreak 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:
// 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.
//===========================================================================
// 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//===========================================================================
// 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
endmoduleVHDL 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.
--===========================================================================
-- 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:
[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.
//===========================================================================
// 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//===========================================================================
// 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--===========================================================================
-- 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:
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.
// 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;
endpropertyCheck 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
Related tutorials
- Related topic
Complete RX RTL Architecture
One synthesizable receiver assembled from the module's five preceding chapters — assumptions stated first, walked block by block with the invariant each maintains, then reviewed the way a reviewer would, including the defects found during its own development.
- Related topic
Verifying the Transmitter
Checking bit timing, frame structure, ordering and the ready/busy contract against the specification rather than the RTL — and measuring the fractional baud generator from outside.
- Related topic
Why Receiving Is Harder Than Transmitting
A transmitter executes a schedule it wrote itself. A receiver must decide whether something is happening, whether it was real, where the positions are, and what value was there — four judgements from one edge on an input it does not control.
- Related topic
The RX FSM
Five states, eight transitions, and a rule that keeps them enumerable: the next state depends on the current state, the configuration and one timing event — never on a data bit. Specified as a table first, with the RTL derived from it and checked against it.
Where this fits
Part of the UART curriculum.
