UART · Module 14
Monitor, Reference Model and Scoreboard
Reconstructing frames independently of the design, predicting expected results, and comparing without importing the design's own assumptions — demonstrated with a mutant.
Chapters 14.3 and 14.4 both leaned on the same three components. This chapter builds them properly, and its subject is a single word: independence.
The claim is easy to state and easy to dismiss as purity: a checker must not share definitions, timing or state with the design it checks. §4 makes it concrete by taking a design with a deliberately injected defect and checking it two ways — one independent, one convenient — and the two checkers disagree completely about whether the design works.
1. Three Roles, Three Boundaries
| Component | Input | Output | Must not know |
|---|---|---|---|
| monitor | the wire | observed transactions | the design's timing or state |
| reference model | the stimulus description | expected transactions | the design at all |
| scoreboard | both streams | a verdict | either one's internals |
The reference model is the strictest. It never sees the design's outputs — it is given what the test drove and computes what must result. That is what makes it a model rather than a mirror.
The monitor sees only the design's outputs, and only through the same interface a real peer would use: a wire.
The scoreboard sees both and understands neither. It compares tuples.
2. The Monitor Reconstructs, It Does Not Read
module uart_line_monitor (input logic rx_i);One port. No clock, no oversample tick, no hierarchical path. It finds a start edge, waits one and a half of its own nominal bit periods, and samples at one-bit intervals — the same algorithm a receiver uses, implemented from Chapter 5.2 rather than from uart_rx.
That constraint is what lets it detect a timing error at all. A monitor that sampled when the design's os_tick fired would follow the design into any recovery error it made, and would report perfect agreement with a receiver sampling in entirely the wrong place.
It also resynchronises independently:
// Resynchronise: wait for the line to return to mark before hunting
// for the next start edge, so a framing error does not cascade.
if (!rx_i) @(posedge rx_i);That is the monitor's own policy, chosen for the same reason Chapter 6.2 chose it for the design — and choosing it independently is the point. If the design's recovery policy were wrong, a monitor that copied it would produce a matching cascade and nothing would look unusual.
3. The Reference Model Predicts From the Specification
// 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
endtaskIts arguments are the stimulus, not the response. It is told what was driven and computes what must come out. Nothing in it can be influenced by what the design actually did.
Note what it is not. It is not a second implementation of the receiver — there is no state machine, no sampling, no oversampling grid. It is a statement of the contract: this input must produce that output. A reference model that reimplements the design tends to reproduce its misunderstandings, and it is also far more work than the checking requires.
A UART's reference model is unusually simple, which is worth saying because it is not the norm. For a processor or a cache the model is a substantial piece of software. Here the entire predictor is two short tasks, because the contract is small — and the effort saved belongs in stimulus and corners instead.
4. Does Independence Actually Matter?
The argument so far is plausible and untested. So: take the receiver, inject a defect, and check it two ways.
The defect swaps even and odd parity in the design's own parity function:
// INJECTED DEFECT — even and odd swapped. Chapter 14.5 uses this
// to show which checkers can and cannot detect it.
PARITY_EVEN: expected_parity = ~acc;
PARITY_ODD: expected_parity = acc;The stimulus is eight clean, well-formed 8E1 frames. A correct receiver reports no parity error on any of them.
Checker A — independent. It computes the expected parity with the BFM's own function, written from Chapter 3.3, and expects a clean frame to be reported clean.
Checker B — convenient. It computes the expected parity by reusing the design's function, which is the single most natural piece of reuse in any testbench: the logic already exists, it is right there, and copying it avoids duplicating a definition.
DUT: uart_rx_mutant — even and odd parity SWAPPED
driving 8 CLEAN 8E1 frames; a correct receiver reports no parity error
frames delivered: 8
INDEPENDENT checker (own parity, from the spec) : 8 mismatches
LAZY checker (reuses the design's function) : 0 mismatches
RESULT: the defect is caught by one and INVISIBLE to the other.The same argument applies to timing and to state, and the mechanism is identical in all three cases:
| Shared thing | What becomes undetectable |
|---|---|
| the parity function | any parity defect — §4, measured |
| the oversample tick | any sampling or baud defect |
| the receiver's state | any frame-boundary or recovery defect |
Duplication is the price, and it is the right price. Writing the parity rule twice — once in the design, once in the BFM — feels wasteful until the two disagree, which is the only moment either of them is worth anything.
5. The Scoreboard Compares and Does Not Interpret
// NOTE: a plain task rather than an automatic one. Icarus Verilog 13.0
// rejects a string argument on an automatic task. The scoreboard is
// called sequentially, so static storage is harmless here.
task compare(input string what,
input logic [8:0] got_d, input bit got_pe, input bit got_fe,
input logic [8:0] exp_d, input bit exp_pe, input bit exp_fe);
begin
if (got_d === exp_d && got_pe === exp_pe && got_fe === exp_fe) begin
n_match++;
end else begin
n_mismatch++;
$display(" MISMATCH %s: got %03h pe=%0b fe=%0b, expected %03h pe=%0b fe=%0b",
what, got_d, got_pe, got_fe, exp_d, exp_pe, exp_fe);
end
end
endtaskIt compares the whole tuple, data and both status bits together. A scoreboard that compares only data passes a design that reports the wrong status, which is Chapter 14.4 §3's requirement quietly dropped.
It reports both values on a mismatch. A message saying only "mismatch at frame 5" costs an hour; one that prints got and expected side by side usually identifies the defect immediately.
It counts matches as well as mismatches, which is how a scoreboard that never ran is distinguished from one that ran and agreed. Zero mismatches and zero matches is not a pass.
6. Decoupling the Monitor From the Driver
Chapter 14.2 §3 described the race this avoids; it belongs here because it is a scoreboard property rather than a monitor one.
// A small observation buffer. A monitor that forces the test to consume
// each frame the instant it appears couples the checker to the driver's
// timing; buffering decouples them, which is what a scoreboard needs.
logic [8:0] q_data [0:255];
bit q_pe [0:255];
bit q_fe [0:255];A scoreboard should be able to run at the end. Drive everything, then compare everything — which makes the check independent of when frames happen to arrive and removes an entire class of testbench race. In a long-running environment the comparison happens continuously instead, but the buffer is still what allows the two sides to proceed at their own rates.
7. The Reference Model in Three Languages
The predictor is the smallest of the three roles and the one that carries the most weight, because every other check in the environment is measured against it. A wrong predictor does not make a test fail — it makes a test agree with a wrong design, or disagree with a correct one, and both cost more than the bug would have.
It emits the frame at half-bit resolution, which is not decoration. A 1.5-bit stop is a legal configuration, and a model working in whole bits has to special-case it. In half-bits it is three slots instead of two and the special case disappears.
// ===========================================================================
// uart_predictor — the reference model of Chapter 14.5, in SystemVerilog
//
// NOT SYNTHESIZABLE, and not meant to be. This is a verification component:
// it answers the question "what must a correct design put on the wire?"
// and it answers it FROM THE SPECIFICATION, never from the design.
//
// It emits the frame at HALF-BIT resolution. That is not decoration — a
// 1.5-bit stop is a legal configuration (Chapter 3.4), and a model that
// works in whole bits has to special-case it. In half-bits it is just three
// slots instead of two, and the special case disappears.
//
// Slot 0 is the first half of the start bit. Index order is time order.
//
// INDEPENDENCE IS THE POINT. This file must not import the design's parity
// package, must not instantiate any part of the design, and must not be
// written by transcribing the RTL. It is written from the frame definition:
//
// start (1 bit, SPACE) | data LSB-first | parity (optional) | stop (MARK)
//
// If the design's parity function is wrong and the predictor imported it,
// the predictor would agree with the design and the bug would ship.
// ===========================================================================
module uart_predictor #(
parameter int unsigned MAXHALF = 32
) (
input logic [8:0] data_i,
input logic [3:0] nbits_i, // 5..9 data bits
input logic [1:0] parity_i, // 0 none, 1 even, 2 odd, 3 mark
input logic [2:0] stop_halves_i, // 2 = 1 bit, 3 = 1.5, 4 = 2
output logic [MAXHALF-1:0] halves_o, // the expected line, slot 0 first
output logic [5:0] nhalf_o, // how many slots are valid
output logic parity_bit_o // the expected parity bit
);
localparam logic [1:0] P_NONE = 2'd0, P_EVEN = 2'd1, P_ODD = 2'd2, P_MARK = 2'd3;
// The parity of the data word, defined here and nowhere else in this file
// hierarchy. A reduction XOR is 1 when the number of ones is ODD.
logic data_xor;
always_comb begin
data_xor = 1'b0;
for (int b = 0; b < 9; b++)
if (b < int'(nbits_i)) data_xor ^= data_i[b];
end
always_comb begin
// Plain `case`, not `unique case`: the default arm already covers
// every value of a 2-bit selector, so `unique` would assert nothing.
// It is also silently ignored by Icarus Verilog 13.0, which makes it
// worse than useless here -- it would imply a check that is not running.
case (parity_i)
P_EVEN : parity_bit_o = data_xor; // make the total count even
P_ODD : parity_bit_o = ~data_xor; // make the total count odd
P_MARK : parity_bit_o = 1'b1; // always 1, carries no information
default: parity_bit_o = 1'b0; // P_NONE: no parity slot at all
endcase
end
always_comb begin
int k;
halves_o = '1; // everything past the frame is MARK
k = 0;
// start bit: SPACE, one whole bit = two half slots
halves_o[k] = 1'b0; k++;
halves_o[k] = 1'b0; k++;
// data bits, LEAST SIGNIFICANT FIRST
for (int b = 0; b < 9; b++) begin
if (b < int'(nbits_i)) begin
halves_o[k] = data_i[b]; k++;
halves_o[k] = data_i[b]; k++;
end
end
// parity slot, present only when a parity mode is selected
if (parity_i != P_NONE) begin
halves_o[k] = parity_bit_o; k++;
halves_o[k] = parity_bit_o; k++;
end
// stop: MARK, for however many half-bits were configured
for (int h = 0; h < 4; h++) begin
if (h < int'(stop_halves_i)) begin
halves_o[k] = 1'b1; k++;
end
end
nhalf_o = 6'(k);
end
endmodule//===========================================================================
// uart_predictor_v — the reference model of Chapter 14.5, in Verilog-2001
//
// NOT SYNTHESIZABLE, and not meant to be. This is a verification component:
// it answers "what must a correct design put on the wire?" and it answers
// it FROM THE SPECIFICATION, never from the design.
//
// It emits the frame at HALF-BIT resolution. That is not decoration -- a
// 1.5-bit stop is a legal configuration, and a model that works in whole
// bits has to special-case it. In half-bits it is three slots instead of
// two and the special case disappears.
//
// Slot 0 is the first half of the start bit. Index order is time order.
//
// INDEPENDENCE IS THE POINT. This file must not import the design's parity
// definition, must not instantiate any part of the design, and must not be
// written by transcribing the RTL. It is written from the frame definition:
//
// start (1 bit, SPACE) | data LSB-first | parity (optional) | stop (MARK)
//
// Verilog-2001 note: no `int` loop variables and no `'1` fill literal, so
// the loop counters are declared as `integer` at module scope and the fill
// is written out. The algorithm is identical.
//===========================================================================
module uart_predictor_v #(
parameter MAXHALF = 32
) (
input wire [8:0] data_i,
input wire [3:0] nbits_i, // 5..9 data bits
input wire [1:0] parity_i, // 0 none, 1 even, 2 odd, 3 mark
input wire [2:0] stop_halves_i, // 2 = 1 bit, 3 = 1.5, 4 = 2
output reg [MAXHALF-1:0] halves_o, // the expected line, slot 0 first
output reg [5:0] nhalf_o, // how many slots are valid
output reg parity_bit_o // the expected parity bit
);
localparam [1:0] P_NONE = 2'd0, P_EVEN = 2'd1, P_ODD = 2'd2, P_MARK = 2'd3;
integer b, h, k;
reg data_xor;
// The parity of the data word, defined here and nowhere else in this
// file. A reduction XOR is 1 when the number of ones is ODD.
always @* begin
data_xor = 1'b0;
for (b = 0; b < 9; b = b + 1)
if (b < nbits_i) data_xor = data_xor ^ data_i[b];
end
always @* begin
case (parity_i)
P_EVEN : parity_bit_o = data_xor; // make the total count even
P_ODD : parity_bit_o = ~data_xor; // make the total count odd
P_MARK : parity_bit_o = 1'b1; // always 1, carries no information
default: parity_bit_o = 1'b0; // P_NONE: no parity slot at all
endcase
end
always @* begin
halves_o = {MAXHALF{1'b1}}; // everything past the frame is MARK
k = 0;
// start bit: SPACE, one whole bit = two half slots
halves_o[k] = 1'b0; k = k + 1;
halves_o[k] = 1'b0; k = k + 1;
// data bits, LEAST SIGNIFICANT FIRST
for (b = 0; b < 9; b = b + 1) begin
if (b < nbits_i) begin
halves_o[k] = data_i[b]; k = k + 1;
halves_o[k] = data_i[b]; k = k + 1;
end
end
// parity slot, present only when a parity mode is selected
if (parity_i != P_NONE) begin
halves_o[k] = parity_bit_o; k = k + 1;
halves_o[k] = parity_bit_o; k = k + 1;
end
// stop: MARK, for however many half-bits were configured
for (h = 0; h < 4; h = h + 1) begin
if (h < stop_halves_i) begin
halves_o[k] = 1'b1; k = k + 1;
end
end
nhalf_o = k[5:0];
end
endmodule--===========================================================================
-- uart_predictor — the reference model of Chapter 14.5, in VHDL-2008
--
-- NOT SYNTHESIZABLE, and not meant to be. This is a verification component:
-- it answers "what must a correct design put on the wire?" and it answers
-- it FROM THE SPECIFICATION, never from the design.
--
-- It emits the frame at HALF-BIT resolution. That is not decoration -- a
-- 1.5-bit stop is a legal configuration, and a model that works in whole
-- bits has to special-case it. In half-bits it is three slots instead of
-- two and the special case disappears.
--
-- Slot 0 is the first half of the start bit. Index order is time order.
--
-- INDEPENDENCE IS THE POINT. This file must not use the design's parity
-- package, must not instantiate any part of the design, and must not be
-- written by transcribing the RTL. It is written from the frame definition:
--
-- start (1 bit, SPACE) | data LSB-first | parity (optional) | stop (MARK)
--
-- VHDL note: the frame is assembled in a VARIABLE and assigned once. Using
-- a signal and writing individual slots would work, but a variable makes the
-- sequential construction explicit -- and construction order IS the
-- specification here, so it should be visible.
--===========================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity uart_predictor is
generic (
MAXHALF : positive := 32
);
port (
data_i : in std_logic_vector(8 downto 0);
nbits_i : in std_logic_vector(3 downto 0); -- 5..9 data bits
parity_i : in std_logic_vector(1 downto 0); -- 0 none,1 even,2 odd,3 mark
stop_halves_i : in std_logic_vector(2 downto 0); -- 2 = 1 bit, 3 = 1.5, 4 = 2
halves_o : out std_logic_vector(MAXHALF-1 downto 0);
nhalf_o : out std_logic_vector(5 downto 0);
parity_bit_o : out std_logic
);
end entity uart_predictor;
architecture model of uart_predictor is
constant P_NONE : std_logic_vector(1 downto 0) := "00";
constant P_EVEN : std_logic_vector(1 downto 0) := "01";
constant P_ODD : std_logic_vector(1 downto 0) := "10";
constant P_MARK : std_logic_vector(1 downto 0) := "11";
signal data_xor : std_logic;
signal par_bit : std_logic;
begin
-- The parity of the data word, defined here and nowhere else in this
-- file. A reduction XOR is 1 when the number of ones is ODD.
xor_proc : process (all)
variable x : std_logic;
begin
x := '0';
for b in 0 to 8 loop
if b < to_integer(unsigned(nbits_i)) then
x := x xor data_i(b);
end if;
end loop;
data_xor <= x;
end process xor_proc;
with parity_i select par_bit <=
data_xor when P_EVEN, -- make the total count even
not data_xor when P_ODD, -- make the total count odd
'1' when P_MARK, -- always 1, carries no information
'0' when others; -- P_NONE: no parity slot at all
parity_bit_o <= par_bit;
frame_proc : process (all)
variable v : std_logic_vector(MAXHALF-1 downto 0);
variable k : natural;
begin
v := (others => '1'); -- everything past the frame is MARK
k := 0;
-- start bit: SPACE, one whole bit = two half slots
v(k) := '0'; k := k + 1;
v(k) := '0'; k := k + 1;
-- data bits, LEAST SIGNIFICANT FIRST
for b in 0 to 8 loop
if b < to_integer(unsigned(nbits_i)) then
v(k) := data_i(b); k := k + 1;
v(k) := data_i(b); k := k + 1;
end if;
end loop;
-- parity slot, present only when a parity mode is selected
if parity_i /= P_NONE then
v(k) := par_bit; k := k + 1;
v(k) := par_bit; k := k + 1;
end if;
-- stop: MARK, for however many half-bits were configured
for h in 0 to 3 loop
if h < to_integer(unsigned(stop_halves_i)) then
v(k) := '1'; k := k + 1;
end if;
end loop;
halves_o <= v;
nhalf_o <= std_logic_vector(to_unsigned(k, 6));
end process frame_proc;
end architecture model;8. The One Component You Can Test Exhaustively
Everything else in a verification environment is sampled: you pick stimulus, you hope it is representative, and coverage tells you afterwards what you missed. The reference model is different. Its input space is finite and small — a data value, a width, a parity mode, a stop length — so it can be walked completely, and there is no excuse for not walking it.
5 data widths × 4 parity modes × 3 stop lengths × 512 data values = 30,720 frames, every one checked slot by slot.
//===========================================================================
// tb_uart_predictor — self-checking SystemVerilog testbench
//
// The reference model is the ONE component in a verification environment
// that can be tested EXHAUSTIVELY, and it is the one that most needs to be,
// because every other check in the environment is measured against it.
//
// A wrong predictor does not cause a test to fail. It causes a test to
// agree with a wrong design, or to disagree with a correct one -- and both
// of those cost more debugging time than the bug would have.
//
// So this suite walks the WHOLE input space: 5 data widths x 4 parity modes
// x 3 stop lengths x 512 data values = 30,720 frames, every one of them
// checked slot by slot.
//
// INDEPENDENCE, again. The predictor computes parity with a reduction XOR.
// This testbench checks it by POPULATION COUNT against the definition --
// "even parity means the total number of ones is even" -- which is a
// different computation reaching the same place. Re-deriving the expected
// value with the same expression the model uses proves only that the
// expression is stable.
//===========================================================================
`timescale 1ns/1ps
module tb_uart_predictor;
localparam MAXHALF = 32;
localparam P_NONE = 2'd0, P_EVEN = 2'd1, P_ODD = 2'd2, P_MARK = 2'd3;
logic [8:0] data;
logic [3:0] nbits;
logic [1:0] parity;
logic [2:0] stop_halves;
wire [MAXHALF-1:0] halves;
wire [5:0] nhalf;
wire parity_bit;
uart_predictor #(.MAXHALF(MAXHALF)) dut (
.data_i(data), .nbits_i(nbits), .parity_i(parity),
.stop_halves_i(stop_halves),
.halves_o(halves), .nhalf_o(nhalf), .parity_bit_o(parity_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
// whole-space error counters
int n_frames = 0;
int e_start = 0; // start bit not two SPACE slots
int e_len = 0; // nhalf disagrees with the arithmetic
int e_data = 0; // a data slot wrong, or not LSB-first
int e_pair = 0; // the two halves of a bit disagree
int e_parity = 0; // parity bit violates the DEFINITION
int e_par_slot = 0; // a parity slot present when it should not be
int e_stop = 0; // a stop slot not MARK
int e_tail = 0; // anything past the frame not MARK
int nb, pm, sh, dv, b, h, k, ones, exp_len, tot;
initial begin
#200_000_000;
$display(" FAIL watchdog: simulation did not finish");
$display("== %0d checks, %0d failures ==", checks+1, failures+1);
$display(" RESULT: SYSTEMVERILOG PREDICTOR TESTS FAILED (timeout)");
$finish;
end
initial begin
$display("== uart_predictor : self-checking SystemVerilog testbench ==");
//=== the exhaustive walk ===========================================
for (nb = 5; nb <= 9; nb = nb + 1)
for (pm = 0; pm <= 3; pm = pm + 1)
for (sh = 2; sh <= 4; sh = sh + 1)
for (dv = 0; dv < 512; dv = dv + 1) begin
data = dv[8:0];
nbits = nb[3:0];
parity = pm[1:0];
stop_halves = sh[2:0];
#1;
n_frames++;
// ---- length ---------------------------------------------------
exp_len = 2 + 2*nb + ((pm != 0) ? 2 : 0) + sh;
if (nhalf !== exp_len[5:0]) e_len++;
// ---- start bit: two SPACE slots -------------------------------
if (halves[0] !== 1'b0 || halves[1] !== 1'b0) e_start++;
// ---- data, LEAST SIGNIFICANT FIRST ----------------------------
k = 2;
for (b = 0; b < nb; b = b + 1) begin
if (halves[k] !== data[b]) e_data++;
if (halves[k+1] !== data[b]) e_data++;
if (halves[k] !== halves[k+1]) e_pair++;
k = k + 2;
end
// ---- parity, checked against the DEFINITION -------------------
// Population count, not the reduction XOR the model uses.
ones = 0;
for (b = 0; b < nb; b = b + 1) ones = ones + data[b];
if (pm != 0) begin
if (halves[k] !== halves[k+1]) e_pair++;
tot = ones + halves[k];
case (pm)
1: if (tot % 2 != 0) e_parity++; // EVEN: total even
2: if (tot % 2 != 1) e_parity++; // ODD : total odd
3: if (halves[k] !== 1'b1) e_parity++;
endcase
k = k + 2;
end else begin
// With no parity the slot after the data MUST already be stop.
if (halves[k] !== 1'b1) e_par_slot++;
end
// ---- stop: MARK for the configured number of halves -----------
for (h = 0; h < sh; h = h + 1) begin
if (halves[k] !== 1'b1) e_stop++;
k++;
end
// ---- the idle tail --------------------------------------------
for (h = k; h < MAXHALF; h = h + 1)
if (halves[h] !== 1'b1) e_tail++;
end
//=== the results ====================================================
check(n_frames == 5*4*3*512,
"the walk covered the whole space: 5 widths x 4 parities x 3 stops x 512");
check(e_len == 0, "every frame length matched 2 + 2n + parity + stop");
check(e_start == 0, "every frame began with TWO SPACE half-slots");
check(e_data == 0, "every data slot carried the right bit, LSB first");
check(e_pair == 0, "the two halves of every whole bit always agreed");
check(e_parity== 0, "every parity bit satisfied its DEFINITION by population count");
check(e_par_slot == 0,
"with parity NONE there is no parity slot -- stop follows the data");
check(e_stop == 0, "every stop slot was MARK");
check(e_tail == 0, "everything past the frame was MARK");
//=== spot checks a human can read ===================================
// These duplicate the walk, and they earn their place: when the walk
// fails, it reports 30,720 frames without saying what a frame looks
// like. These say it.
data = 9'h055; nbits = 4'd8; parity = P_NONE; stop_halves = 3'd2; #1;
check(nhalf == 6'd20, "8N1 frame is 20 half-slots: 2 start + 16 data + 2 stop");
check(halves[1:0] == 2'b00, "8N1 0x55: start is SPACE");
check(halves[3:2] == 2'b11, "8N1 0x55: first data bit is 1 (LSB of 0x55)");
check(halves[5:4] == 2'b00, "8N1 0x55: second data bit is 0");
check(halves[19:18] == 2'b11, "8N1 0x55: stop is MARK");
data = 9'h0A5; nbits = 4'd8; parity = P_EVEN; stop_halves = 3'd2; #1;
// 0xA5 = 10100101, four ones -> even already -> parity bit 0
check(parity_bit === 1'b0, "8E1 0xA5 has four ones, so even parity is 0");
check(nhalf == 6'd22, "and the frame grew by one bit to 22 half-slots");
data = 9'h0A5; nbits = 4'd8; parity = P_ODD; stop_halves = 3'd2; #1;
check(parity_bit === 1'b1, "8O1 0xA5 needs a 1 to make the count odd");
data = 9'h07F; nbits = 4'd7; parity = P_MARK; stop_halves = 3'd3; #1;
check(parity_bit === 1'b1, "MARK parity is 1 regardless of the data");
check(nhalf == 6'd21, "7-bit + mark parity + 1.5 stop is 21 half-slots");
$display("== %0d checks, %0d failures ==", checks, failures);
if (failures == 0) $display(" RESULT: ALL SYSTEMVERILOG PREDICTOR TESTS PASSED");
else $display(" RESULT: SYSTEMVERILOG PREDICTOR TESTS FAILED");
$finish;
end
endmodule//===========================================================================
// tb_uart_predictor_v — self-checking Verilog-2001 testbench
//
// The reference model is the ONE component in a verification environment
// that can be tested EXHAUSTIVELY, and it is the one that most needs to be,
// because every other check in the environment is measured against it.
//
// A wrong predictor does not cause a test to fail. It causes a test to
// agree with a wrong design, or to disagree with a correct one -- and both
// of those cost more debugging time than the bug would have.
//
// So this suite walks the WHOLE input space: 5 data widths x 4 parity modes
// x 3 stop lengths x 512 data values = 30,720 frames, every one of them
// checked slot by slot.
//
// INDEPENDENCE, again. The predictor computes parity with a reduction XOR.
// This testbench checks it by POPULATION COUNT against the definition --
// "even parity means the total number of ones is even" -- which is a
// different computation reaching the same place. Re-deriving the expected
// value with the same expression the model uses proves only that the
// expression is stable.
//===========================================================================
`timescale 1ns/1ps
module tb_uart_predictor_v;
localparam MAXHALF = 32;
localparam P_NONE = 2'd0, P_EVEN = 2'd1, P_ODD = 2'd2, P_MARK = 2'd3;
reg [8:0] data;
reg [3:0] nbits;
reg [1:0] parity;
reg [2:0] stop_halves;
wire [MAXHALF-1:0] halves;
wire [5:0] nhalf;
wire parity_bit;
uart_predictor_v #(.MAXHALF(MAXHALF)) dut (
.data_i(data), .nbits_i(nbits), .parity_i(parity),
.stop_halves_i(stop_halves),
.halves_o(halves), .nhalf_o(nhalf), .parity_bit_o(parity_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
// whole-space error counters
integer n_frames = 0;
integer e_start = 0; // start bit not two SPACE slots
integer e_len = 0; // nhalf disagrees with the arithmetic
integer e_data = 0; // a data slot wrong, or not LSB-first
integer e_pair = 0; // the two halves of a bit disagree
integer e_parity = 0; // parity bit violates the DEFINITION
integer e_par_slot = 0; // a parity slot present when it should not be
integer e_stop = 0; // a stop slot not MARK
integer e_tail = 0; // anything past the frame not MARK
integer nb, pm, sh, dv, b, h, k, ones, exp_len, tot;
initial begin
#200_000_000;
$display(" FAIL watchdog: simulation did not finish");
$display("== %0d checks, %0d failures ==", checks+1, failures+1);
$display(" RESULT: VERILOG PREDICTOR TESTS FAILED (timeout)");
$finish;
end
initial begin
$display("== uart_predictor_v : self-checking Verilog testbench ==");
//=== the exhaustive walk ===========================================
for (nb = 5; nb <= 9; nb = nb + 1)
for (pm = 0; pm <= 3; pm = pm + 1)
for (sh = 2; sh <= 4; sh = sh + 1)
for (dv = 0; dv < 512; dv = dv + 1) begin
data = dv[8:0];
nbits = nb[3:0];
parity = pm[1:0];
stop_halves = sh[2:0];
#1;
n_frames = n_frames + 1;
// ---- length ---------------------------------------------------
exp_len = 2 + 2*nb + ((pm != 0) ? 2 : 0) + sh;
if (nhalf !== exp_len[5:0]) e_len = e_len + 1;
// ---- start bit: two SPACE slots -------------------------------
if (halves[0] !== 1'b0 || halves[1] !== 1'b0) e_start = e_start + 1;
// ---- data, LEAST SIGNIFICANT FIRST ----------------------------
k = 2;
for (b = 0; b < nb; b = b + 1) begin
if (halves[k] !== data[b]) e_data = e_data + 1;
if (halves[k+1] !== data[b]) e_data = e_data + 1;
if (halves[k] !== halves[k+1]) e_pair = e_pair + 1;
k = k + 2;
end
// ---- parity, checked against the DEFINITION -------------------
// Population count, not the reduction XOR the model uses.
ones = 0;
for (b = 0; b < nb; b = b + 1) ones = ones + data[b];
if (pm != 0) begin
if (halves[k] !== halves[k+1]) e_pair = e_pair + 1;
tot = ones + halves[k];
case (pm)
1: if (tot % 2 != 0) e_parity = e_parity + 1; // EVEN: total even
2: if (tot % 2 != 1) e_parity = e_parity + 1; // ODD : total odd
3: if (halves[k] !== 1'b1) e_parity = e_parity + 1;
endcase
k = k + 2;
end else begin
// With no parity the slot after the data MUST already be stop.
if (halves[k] !== 1'b1) e_par_slot = e_par_slot + 1;
end
// ---- stop: MARK for the configured number of halves -----------
for (h = 0; h < sh; h = h + 1) begin
if (halves[k] !== 1'b1) e_stop = e_stop + 1;
k = k + 1;
end
// ---- the idle tail --------------------------------------------
for (h = k; h < MAXHALF; h = h + 1)
if (halves[h] !== 1'b1) e_tail = e_tail + 1;
end
//=== the results ====================================================
check(n_frames == 5*4*3*512,
"the walk covered the whole space: 5 widths x 4 parities x 3 stops x 512");
check(e_len == 0, "every frame length matched 2 + 2n + parity + stop");
check(e_start == 0, "every frame began with TWO SPACE half-slots");
check(e_data == 0, "every data slot carried the right bit, LSB first");
check(e_pair == 0, "the two halves of every whole bit always agreed");
check(e_parity== 0, "every parity bit satisfied its DEFINITION by population count");
check(e_par_slot == 0,
"with parity NONE there is no parity slot -- stop follows the data");
check(e_stop == 0, "every stop slot was MARK");
check(e_tail == 0, "everything past the frame was MARK");
//=== spot checks a human can read ===================================
// These duplicate the walk, and they earn their place: when the walk
// fails, it reports 30,720 frames without saying what a frame looks
// like. These say it.
data = 9'h055; nbits = 4'd8; parity = P_NONE; stop_halves = 3'd2; #1;
check(nhalf == 6'd20, "8N1 frame is 20 half-slots: 2 start + 16 data + 2 stop");
check(halves[1:0] == 2'b00, "8N1 0x55: start is SPACE");
check(halves[3:2] == 2'b11, "8N1 0x55: first data bit is 1 (LSB of 0x55)");
check(halves[5:4] == 2'b00, "8N1 0x55: second data bit is 0");
check(halves[19:18] == 2'b11, "8N1 0x55: stop is MARK");
data = 9'h0A5; nbits = 4'd8; parity = P_EVEN; stop_halves = 3'd2; #1;
// 0xA5 = 10100101, four ones -> even already -> parity bit 0
check(parity_bit === 1'b0, "8E1 0xA5 has four ones, so even parity is 0");
check(nhalf == 6'd22, "and the frame grew by one bit to 22 half-slots");
data = 9'h0A5; nbits = 4'd8; parity = P_ODD; stop_halves = 3'd2; #1;
check(parity_bit === 1'b1, "8O1 0xA5 needs a 1 to make the count odd");
data = 9'h07F; nbits = 4'd7; parity = P_MARK; stop_halves = 3'd3; #1;
check(parity_bit === 1'b1, "MARK parity is 1 regardless of the data");
check(nhalf == 6'd21, "7-bit + mark parity + 1.5 stop is 21 half-slots");
$display("== %0d checks, %0d failures ==", checks, failures);
if (failures == 0) $display(" RESULT: ALL VERILOG PREDICTOR TESTS PASSED");
else $display(" RESULT: VERILOG PREDICTOR TESTS FAILED");
$finish;
end
endmodule--===========================================================================
-- tb_uart_predictor — self-checking VHDL-2008 testbench
--
-- The reference model is the ONE component in a verification environment
-- that can be tested EXHAUSTIVELY, and it is the one that most needs to be,
-- because every other check in the environment is measured against it.
--
-- A wrong predictor does not cause a test to fail. It causes a test to
-- agree with a wrong design, or to disagree with a correct one -- and both
-- cost more debugging time than the bug would have.
--
-- So this suite walks the WHOLE input space: 5 data widths x 4 parity modes
-- x 3 stop lengths x 512 data values = 30,720 frames, every one checked
-- slot by slot.
--
-- INDEPENDENCE, again. The predictor computes parity with a reduction XOR.
-- This testbench checks it by POPULATION COUNT against the definition --
-- "even parity means the total number of ones is even" -- which is a
-- different computation reaching the same place.
--
-- Same 19 counted checks as the Verilog and SystemVerilog twins.
--===========================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity tb_uart_predictor is
end entity tb_uart_predictor;
architecture sim of tb_uart_predictor is
constant MAXHALF : positive := 32;
constant P_NONE : natural := 0;
constant P_EVEN : natural := 1;
constant P_ODD : natural := 2;
constant P_MARK : natural := 3;
signal data : std_logic_vector(8 downto 0) := (others => '0');
signal nbits : std_logic_vector(3 downto 0) := "1000";
signal parity : std_logic_vector(1 downto 0) := "00";
signal stop_halves : std_logic_vector(2 downto 0) := "010";
signal halves : std_logic_vector(MAXHALF-1 downto 0);
signal nhalf : std_logic_vector(5 downto 0);
signal parity_bit : std_logic;
begin
dut : entity work.uart_predictor
generic map (MAXHALF => MAXHALF)
port map (data_i => data, nbits_i => nbits, parity_i => parity,
stop_halves_i => stop_halves,
halves_o => halves, nhalf_o => nhalf,
parity_bit_o => parity_bit);
watchdog : process
begin
wait for 200 ms;
report "watchdog: simulation did not finish" severity failure;
end process watchdog;
stim : process
variable checks, failures : natural := 0;
-- whole-space error counters
variable n_frames : natural := 0;
variable e_start : natural := 0;
variable e_len : natural := 0;
variable e_data : natural := 0;
variable e_pair : natural := 0;
variable e_parity : natural := 0;
variable e_par_slot : natural := 0;
variable e_stop : natural := 0;
variable e_tail : natural := 0;
variable k, ones, exp_len, tot : natural;
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;
begin
report "== uart_predictor : self-checking VHDL testbench ==" severity note;
--=== the exhaustive walk ===========================================
for nb in 5 to 9 loop
for pm in 0 to 3 loop
for sh in 2 to 4 loop
for dv in 0 to 511 loop
data <= std_logic_vector(to_unsigned(dv, 9));
nbits <= std_logic_vector(to_unsigned(nb, 4));
parity <= std_logic_vector(to_unsigned(pm, 2));
stop_halves <= std_logic_vector(to_unsigned(sh, 3));
wait for 1 ns;
n_frames := n_frames + 1;
-- length
exp_len := 2 + 2*nb + sh;
if pm /= 0 then exp_len := exp_len + 2; end if;
if to_integer(unsigned(nhalf)) /= exp_len then e_len := e_len + 1; end if;
-- start bit: two SPACE slots
if halves(0) /= '0' or halves(1) /= '0' then e_start := e_start + 1; end if;
-- data, LEAST SIGNIFICANT FIRST
k := 2;
for b in 0 to nb-1 loop
if halves(k) /= data(b) then e_data := e_data + 1; end if;
if halves(k+1) /= data(b) then e_data := e_data + 1; end if;
if halves(k) /= halves(k+1) then e_pair := e_pair + 1; end if;
k := k + 2;
end loop;
-- parity, checked against the DEFINITION by population count
ones := 0;
for b in 0 to nb-1 loop
if data(b) = '1' then ones := ones + 1; end if;
end loop;
if pm /= 0 then
if halves(k) /= halves(k+1) then e_pair := e_pair + 1; end if;
tot := ones;
if halves(k) = '1' then tot := tot + 1; end if;
if pm = P_EVEN and tot mod 2 /= 0 then e_parity := e_parity + 1;
elsif pm = P_ODD and tot mod 2 /= 1 then e_parity := e_parity + 1;
elsif pm = P_MARK and halves(k) /= '1' then e_parity := e_parity + 1;
end if;
k := k + 2;
else
-- with no parity the slot after the data MUST already be stop
if halves(k) /= '1' then e_par_slot := e_par_slot + 1; end if;
end if;
-- stop: MARK for the configured number of halves
for h in 0 to sh-1 loop
if halves(k) /= '1' then e_stop := e_stop + 1; end if;
k := k + 1;
end loop;
-- the idle tail
for h in k to MAXHALF-1 loop
if halves(h) /= '1' then e_tail := e_tail + 1; end if;
end loop;
end loop;
end loop;
end loop;
end loop;
--=== the results ====================================================
check(n_frames = 5*4*3*512,
"the walk covered the whole space: 5 widths x 4 parities x 3 stops x 512");
check(e_len = 0, "every frame length matched 2 + 2n + parity + stop");
check(e_start = 0, "every frame began with TWO SPACE half-slots");
check(e_data = 0, "every data slot carried the right bit, LSB first");
check(e_pair = 0, "the two halves of every whole bit always agreed");
check(e_parity = 0,
"every parity bit satisfied its DEFINITION by population count");
check(e_par_slot = 0,
"with parity NONE there is no parity slot -- stop follows the data");
check(e_stop = 0, "every stop slot was MARK");
check(e_tail = 0, "everything past the frame was MARK");
--=== spot checks a human can read ===================================
data <= '0' & x"55";
nbits <= "1000"; parity <= "00"; stop_halves <= "010";
wait for 1 ns;
check(to_integer(unsigned(nhalf)) = 20,
"8N1 frame is 20 half-slots: 2 start + 16 data + 2 stop");
check(halves(1 downto 0) = "00", "8N1 0x55: start is SPACE");
check(halves(3 downto 2) = "11", "8N1 0x55: first data bit is 1 (LSB of 0x55)");
check(halves(5 downto 4) = "00", "8N1 0x55: second data bit is 0");
check(halves(19 downto 18) = "11", "8N1 0x55: stop is MARK");
data <= '0' & x"A5"; parity <= "01"; wait for 1 ns;
check(parity_bit = '0', "8E1 0xA5 has four ones, so even parity is 0");
check(to_integer(unsigned(nhalf)) = 22,
"and the frame grew by one bit to 22 half-slots");
parity <= "10"; wait for 1 ns;
check(parity_bit = '1', "8O1 0xA5 needs a 1 to make the count odd");
data <= '0' & x"7F"; nbits <= "0111"; parity <= "11"; stop_halves <= "011";
wait for 1 ns;
check(parity_bit = '1', "MARK parity is 1 regardless of the data");
check(to_integer(unsigned(nhalf)) = 21,
"7-bit + mark parity + 1.5 stop is 21 half-slots");
report "== " & integer'image(checks) & " checks, "
& integer'image(failures) & " failures ==" severity note;
if failures = 0 then
report " RESULT: ALL VHDL PREDICTOR TESTS PASSED" severity note;
else
report " RESULT: VHDL PREDICTOR TESTS FAILED" severity error;
end if;
wait;
end process stim;
end architecture sim;Nineteen checks, and all three languages agree — to the nanosecond: the Verilog and VHDL runs both finish at 30,724 ns.
PASS the walk covered the whole space: 5 widths x 4 parities x 3 stops x 512
PASS every frame length matched 2 + 2n + parity + stop
PASS every frame began with TWO SPACE half-slots
PASS every data slot carried the right bit, LSB first
PASS the two halves of every whole bit always agreed
PASS every parity bit satisfied its DEFINITION by population count
PASS with parity NONE there is no parity slot -- stop follows the data
PASS every stop slot was MARK
PASS everything past the frame was MARK
PASS 8N1 frame is 20 half-slots: 2 start + 16 data + 2 stop
PASS 8E1 0xA5 has four ones, so even parity is 0
PASS 8O1 0xA5 needs a 1 to make the count odd
PASS MARK parity is 1 regardless of the data
== 19 checks, 0 failures ==
Verilog-2001 : 19 checks, 0 failures
SystemVerilog : 19 checks, 0 failures
VHDL-2008 : 19 checks, 0 failures9. The Scoreboard, in UVM
The directed suites above use a queue and a comparison loop, because with one driver and one monitor that is all a scoreboard is. A UVM environment does the same thing with more structure, and the structure earns its place as soon as there is more than one source of transactions.
// ---------------------------------------------------------------------------
// uart_scoreboard — compares, and does NOT interpret (section 5)
//
// Two analysis exports: one from the predictor, one from the line monitor.
// The scoreboard's entire job is to pair them up and report differences. It
// contains no knowledge of frame format, parity rules or bit order -- if it
// did, it would be a third implementation of the specification, and a
// disagreement would no longer tell you which of the three was wrong.
// ---------------------------------------------------------------------------
class uart_scoreboard extends uvm_scoreboard;
`uvm_component_utils(uart_scoreboard)
uvm_analysis_imp_exp #(uart_frame_item, uart_scoreboard) exp_export;
uvm_analysis_imp_obs #(uart_frame_item, uart_scoreboard) obs_export;
protected uart_frame_item m_expected[$];
protected int m_compared, m_mismatch;
function void write_exp(uart_frame_item t);
m_expected.push_back(t);
endfunction
function void write_obs(uart_frame_item t);
uart_frame_item e;
if (m_expected.size() == 0) begin
`uvm_error("SB", $sformatf(
"observed a frame (data=%02h) that was never predicted", t.data))
return;
end
e = m_expected.pop_front();
m_compared++;
// Field by field, and each mismatch reported SEPARATELY. A single
// "frames differ" message makes the reader diff two hex strings by
// eye; naming the field that differs is the difference between a
// five-minute debug and an hour of one.
if (t.data !== e.data)
report_diff("data", $sformatf("%03h", e.data), $sformatf("%03h", t.data));
if (t.parity_err !== e.parity_err)
report_diff("parity_err", $sformatf("%0b", e.parity_err),
$sformatf("%0b", t.parity_err));
if (t.frame_err !== e.frame_err)
report_diff("frame_err", $sformatf("%0b", e.frame_err),
$sformatf("%0b", t.frame_err));
endfunction
protected function void report_diff(string field, string exp, string obs);
m_mismatch++;
`uvm_error("SB", $sformatf("frame %0d: %s expected %s, observed %s",
m_compared, field, exp, obs))
endfunction
function void check_phase(uvm_phase phase);
if (m_expected.size() != 0)
`uvm_error("SB", $sformatf(
"%0d predicted frames were never observed", m_expected.size()))
if (m_compared == 0)
`uvm_error("SB", "the scoreboard compared NOTHING -- see below")
endfunction
endclass10. Verification
Check the checker with a mutant. §4 is not only an argument; it is a test that should exist. Inject a known defect, confirm the environment reports it, remove the defect, confirm the report goes away. An environment that has never failed has not been shown capable of failing.
Grep for the three leaks. They are mechanically detectable:
| Leak | How to find it |
|---|---|
| shared definition | does the environment import any of the design's packages? |
| borrowed timing | does any monitor sample on a design-generated signal? |
| peeked state | does any hierarchical path into the DUT appear outside a debug display? |
Assert that the two streams are the same length. A scoreboard comparing 8 observed against 9 expected has already found a defect, and it is worth reporting as a count mismatch rather than as eight positional mismatches.
Print the first few mismatches and then stop. §5's positional comparison means one dropped frame produces a wall; a scoreboard that prints all of it buries the useful line.
11. Debugging
12. What This Means in Practice
Independence costs duplication and buys the ability to disagree. The parity rule exists twice on purpose. Anyone who removes the duplication in the name of tidiness will delete the property §4 measured.
A UART reference model is small, so spend the effort on stimulus. Two tasks cover the contract. For a larger design the balance shifts, but the principle does not: the model states the contract rather than reimplementing the design.
Write the monitor so it works against a different implementation. If it only works against this RTL, it has absorbed something from it. The BFM here decodes any UART, which is both a reuse benefit and a proof of independence.
Keep the mutant. A single deliberately-broken copy of the design, run occasionally, is the cheapest available evidence that the environment can still fail.
13. Understanding Check
14. Summary
Three roles, three boundaries: the monitor sees only a wire, the reference model sees only the stimulus, the scoreboard sees both and interprets neither.
The monitor reconstructs rather than reads — its own nominal bit period, its own parity definition, its own recovery policy — which is what lets it detect a timing or recovery defect at all.
The reference model states the contract, not the mechanism. Two short tasks cover a UART, because the contract is small; reimplementing the design would reproduce its misunderstandings at greater cost.
Independence is functional, and it was measured. A receiver with even and odd parity swapped, driven with eight clean frames: the independent checker found 8 mismatches, the checker reusing the design's own parity function found 0 — and the second is not badly written, it is merely incapable of disagreeing.
The scoreboard compares whole tuples, positionally, counts matches as well as mismatches, and prints both values on a failure.
Buffer the observations so the driver and the checker never have to agree about when a frame ends.
And the rule that follows from §4: duplication between a design and its checker is the mechanism, and removing it in the name of tidiness deletes the check while leaving the testbench running.
15. What Comes Next
The environment is complete: stimulus that can be wrong on purpose, a monitor that reconstructs, a model that predicts, and a scoreboard that compares. What it has not yet been pointed at is the set of inputs most likely to break something.
Chapter 14.6 chooses corners deliberately — frame boundaries, queue limits, configuration changes in flight, reset, and marginal baud — and runs the full parity-by-corruption cross that Chapter 14.1 §4 left at four bins of twenty-four. One of those twenty-four behaves differently from its twenty-three neighbours, and the chapter is largely about what that turns out to mean.
Browse the full path on the UART tutorials index. For the coverage model these corners close, read back to Chapter 14.1.
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
Building a UART Verification Plan
Deriving checkable requirements from framing, timing, error and status behaviour, and turning the configuration space into a coverage model that reports what testing actually reached.
- Related topic
Stimulus Architecture and the Serial BFM
A bus-functional model that drives and samples a UART line at an arbitrary and deliberately imperfect baud rate — and the receiver tolerance it measures.
- 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.
Where this fits
Part of the UART curriculum.
