UART · Module 14
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.
Chapter 14.1 named a coverage axis the design had never been tested against: baud offset, one bin of three reached. The reason is structural rather than negligent — every testbench in Modules 6 through 13 generated stimulus by driving the DUT's own transmitter, or by toggling a line using the DUT's own bit period.
A stimulus generator that can only produce correct traffic cannot test tolerance to incorrect traffic. This chapter builds one that can.
1. The Bit Period Must Be an Argument
This is the whole design decision, and it is one line:
// tbit_ns is a REAL and is an argument, not a parameter: driving at a
// deliberately wrong baud rate is the whole reason this BFM exists.
task automatic send_frame(input logic [8:0] data,
input int nbits,
input int parity_mode,
input int stop_halves, // stop length, in half-bits
input real tbit_ns,
input int err);A parameter is fixed at elaboration; an argument varies per call. Making the bit period a parameter — which is what a BFM built from the design's own timing generator effectively does — means one simulation can only ever drive one baud rate, and sweeping requires re-elaborating. Making it a real argument means a single run can walk the rate across the receiver's tolerance and find the edges.
stop_halves is there for the same reason. Stop length is a real configuration axis (1, 1.5 or 2 bits), and expressing it in half-bits lets the BFM produce 1.5 without a special case.
2. Corruption Is Part of the Interface
Six kinds, enumerated rather than ad hoc:
// Error injection selectors — Chapter 14.6.
typedef enum int {
ERR_NONE = 0,
ERR_PARITY = 1, // invert the parity bit
ERR_STOP = 2, // hold the stop interval at SPACE
ERR_SHORT_STOP = 3, // stop lasts half a bit, then a new start
ERR_GLITCH = 4, // a narrow spike inside a data bit
ERR_BREAK = 5 // hold the line low far past a frame
} bfm_err_e;Each maps to a requirement from Chapter 14.1 §2. ERR_PARITY exercises R5, ERR_STOP exercises R4, ERR_BREAK exercises R7. An error-injection list that does not map to requirements is a list of things somebody found easy to inject.
They are one enumerated argument rather than six flags, because the corruptions are mutually exclusive by construction and a single selector makes the coverage bin obvious — cp_err[err]++ and nothing else to decide.
ERR_GLITCH is the one with a hidden parameter, and Chapter 14.6 turns it into the module's sharpest result:
// Glitch placement, in fractions of a bit. Exposed so a test can sweep
// the spike across the sampling point — Chapter 14.6.
real glitch_start_frac = 0.40;
real glitch_width_frac = 0.06;3. The Monitor Knows Only the Wire
// MONITOR — recovers frames from the wire, knowing only the wire.
//
// It does NOT look at the DUT. It finds a start edge, waits one and a half
// of its OWN nominal bit periods, and samples at one-bit intervals — which
// is the same algorithm a real receiver uses and is implemented here from
// the specification rather than from the RTL.
module uart_line_monitor (input logic rx_i);
real nominal_tbit_ns = 8680.5556;
int nbits = 8;
int parity_mode = P_NONE;
bit enabled = 1'b0;One input port, and it is the wire. No clock, no oversample tick, no hierarchical path into the design. That is Chapter 14.1 §1's third leak closed by construction: a monitor with no access to the DUT cannot borrow its timing.
It runs on its own nominal bit period, which is what lets it detect that the DUT's recovered timing is wrong. A monitor synchronised to the DUT would follow the design into any error it made.
It defines its own parity function, in its own package, rather than importing uart_parity_pkg:
// Parity encodings match uart_parity_pkg without importing it, so the
// BFM does not share a definition with the design it is checking.
typedef enum int { P_NONE=0, P_EVEN=1, P_ODD=2, P_MARK=3 } bfm_parity_e;That duplication is deliberate and is the second leak closed. If the design's parity function were wrong, importing it would make the checker agree.
4. Verify the BFM Before Trusting It
Driver into monitor, no DUT anywhere:
-- 8N1, clean
pass 8N1 0x55 recovered
pass 8N1 0x00 recovered
pass 8N1 0xFF recovered
-- 8E1, clean and with injected parity error
pass 8E1 0xA5 clean
pass 8E1 0xA5 with flipped parity -> parity error
-- injected framing error
pass stop held at SPACE -> framing error
-- 7 data bits, odd parity
pass 7O1 0x2A recovered
pass monitor observed every frame driven
== 8 checks, 0 failures ==This is not a formality. Two of the three defects found while building the BFM were in the BFM: a missing timescale directive, which made every delay run on the default time unit and produced a monitor that never saw a frame; and the driver/checker coupling of §3. Both would have presented as DUT failures if the environment had been pointed at the design first.
5. What the BFM Made Measurable
With the bit period as an argument, one simulation can sweep it. Driving eight patterns at each offset from −7% to +7% in 0.5% steps, against the unmodified receiver at 16× oversampling:
receiver: 16x oversample, 100 MHz, nominal 115200 baud
error driver baud bytes OK
-7.0% 107136 1 / 8
-6.0% 108288 2 / 8
-5.5% 108864 4 / 8
-5.0% 109440 7 / 8
-4.0% 110592 8 / 8
0.0% 115200 8 / 8
5.0% 120960 8 / 8
6.0% 122112 7 / 8
6.5% 122688 5 / 8
7.0% 123264 4 / 8
all 8 patterns correct over -4.5% .. 5.5% (width 10.0%)A 10.0% window — which is the classic ±5% rule of thumb, now measured on this design rather than quoted. Chapter 4.5 derived why the budget is roughly that size; this is the first time it has been checked against the RTL.
Note the degradation is gradual, not a cliff. At −5.0% seven of eight patterns still arrive; at −5.5%, four. A test that sends one byte at one offset and passes tells you almost nothing, because most patterns survive well past the point where the link is unusable — the patterns that fail first are the ones with the longest runs between transitions, since those accumulate the most phase error before the next re-synchronisation.
6. The Driver in Three Languages
Sections 1 and 2 gave the two decisions. This is the component they produce.
Note what it is not: it is not synthesizable, and it makes no attempt to be. A BFM is a model of the far end of a cable, and the far end of a cable is allowed to do things no synthesizable design can — hold a line low for twelve bit times, place a 60-nanosecond spike inside a bit, or run 7% fast.
//===========================================================================
// uart_line_driver — the serial BFM of Chapter 14.2, in SystemVerilog
//
// NOT SYNTHESIZABLE. This drives a UART line the way a far-end device
// would, and its whole reason for existing is that it can drive it WRONG.
//
// THE BIT PERIOD IS AN ARGUMENT, NOT A PARAMETER.
// A parameter is fixed at elaboration; an argument varies per call. A BFM
// built from the design's own timing generator can only ever drive the one
// rate the design expects, and sweeping requires re-elaborating. As a real
// argument, a single run can walk the rate across the receiver's tolerance
// and find the edges -- which is the measurement Chapter 14.2 section 5 is
// built on.
//
// CORRUPTION IS PART OF THE INTERFACE, not an afterthought. Six kinds, one
// enumerated selector, each mapping to a requirement in Chapter 14.1:
//
// ERR_NONE clean frame
// ERR_PARITY invert the parity bit -> R5
// ERR_STOP hold the stop interval at SPACE -> R4
// ERR_SHORT_STOP stop lasts half a bit, then a new start -> R4
// ERR_GLITCH a narrow spike inside a data bit -> R6
// ERR_BREAK hold the line low far past a frame -> R7
//
// INDEPENDENCE: this file computes parity by COUNTING ONES. The reference
// model (uart_predictor) computes it with a reduction XOR. Two different
// computations of the same definition, so a testbench that compares them
// is comparing two implementations rather than one expression against
// itself.
//===========================================================================
`timescale 1ns/1ps
module uart_line_driver (output logic tx_o);
// parity encodings, defined HERE and not imported from the design
localparam P_NONE = 0, P_EVEN = 1, P_ODD = 2, P_MARK = 3;
// error-injection selectors
localparam ERR_NONE = 0, ERR_PARITY = 1, ERR_STOP = 2,
ERR_SHORT_STOP = 3, ERR_GLITCH = 4, ERR_BREAK = 5;
// Glitch placement, in fractions of a bit. Exposed so a test can sweep
// the spike across the sampling point -- Chapter 14.6 turns this into
// the module's sharpest result.
real glitch_start_frac = 0.40;
real glitch_width_frac = 0.06;
int glitch_bit = 3; // which data bit carries the spike
int break_bits = 12; // how long ERR_BREAK holds SPACE
int n_sent = 0; // frames this driver has driven
initial tx_o = 1'b1; // idle is MARK
// Parity by POPULATION COUNT -- deliberately not the reduction XOR the
// reference model uses.
function parity_of;
input [8:0] d;
input int nbits;
input int mode;
int i, ones;
begin
ones = 0;
for (i = 0; i < nbits; i = i + 1) ones = ones + d[i];
case (mode)
P_EVEN : parity_of = (ones % 2) ? 1'b1 : 1'b0; // total even
P_ODD : parity_of = (ones % 2) ? 1'b0 : 1'b1; // total odd
P_MARK : parity_of = 1'b1;
default: parity_of = 1'b0;
endcase
end
endfunction
task send_frame;
input [8:0] data;
input int nbits;
input int parity_mode;
input int stop_halves; // stop length in HALF bits: 2, 3 or 4
input real tbit_ns; // the argument that makes this useful
input int err;
int i;
logic p;
begin
// ---- start: one bit of SPACE ------------------------------
tx_o = 1'b0;
#(tbit_ns);
// ---- data, LEAST SIGNIFICANT FIRST ------------------------
for (i = 0; i < nbits; i = i + 1) begin
tx_o = data[i];
if (err == ERR_GLITCH && i == glitch_bit) begin
// a narrow spike to the opposite level, inside the bit
#(tbit_ns * glitch_start_frac);
tx_o = ~data[i];
#(tbit_ns * glitch_width_frac);
tx_o = data[i];
#(tbit_ns * (1.0 - glitch_start_frac - glitch_width_frac));
end else begin
#(tbit_ns);
end
end
// ---- parity ------------------------------------------------
if (parity_mode != P_NONE) begin
p = parity_of(data, nbits, parity_mode);
if (err == ERR_PARITY) p = ~p; // the injected fault
tx_o = p;
#(tbit_ns);
end
// ---- stop, or whatever was injected instead ----------------
if (err == ERR_STOP) begin
tx_o = 1'b0; // SPACE where MARK belongs
#(tbit_ns * stop_halves / 2.0);
end else if (err == ERR_SHORT_STOP) begin
// Half a stop bit, and then a NEW START -- which is what makes
// this fault interesting. A receiver that has finished its
// frame is looking for a falling edge, and it gets one half a
// bit earlier than the format allows. Merely truncating the
// stop and going idle would be a quieter fault that no
// receiver could detect.
tx_o = 1'b1;
#(tbit_ns * 0.5);
tx_o = 1'b0; // the premature start
#(tbit_ns);
end else if (err == ERR_BREAK) begin
tx_o = 1'b0;
#(tbit_ns * break_bits); // far past one frame
end else begin
tx_o = 1'b1;
#(tbit_ns * stop_halves / 2.0);
end
tx_o = 1'b1; // back to idle
n_sent++;
end
endtask
// Hold the line idle for a while -- used between frames so a monitor
// sees a clean gap rather than a stop running into the next start.
task idle_for;
input real tbit_ns;
input real nbits;
begin
tx_o = 1'b1;
#(tbit_ns * nbits);
end
endtask
endmodule//===========================================================================
// uart_line_driver_v — the serial BFM of Chapter 14.2, in Verilog-2001
//
// NOT SYNTHESIZABLE. This drives a UART line the way a far-end device
// would, and its whole reason for existing is that it can drive it WRONG.
//
// THE BIT PERIOD IS AN ARGUMENT, NOT A PARAMETER.
// A parameter is fixed at elaboration; an argument varies per call. A BFM
// built from the design's own timing generator can only ever drive the one
// rate the design expects, and sweeping requires re-elaborating. As a real
// argument, a single run can walk the rate across the receiver's tolerance
// and find the edges -- which is the measurement Chapter 14.2 section 5 is
// built on.
//
// CORRUPTION IS PART OF THE INTERFACE, not an afterthought. Six kinds, one
// enumerated selector, each mapping to a requirement in Chapter 14.1:
//
// ERR_NONE clean frame
// ERR_PARITY invert the parity bit -> R5
// ERR_STOP hold the stop interval at SPACE -> R4
// ERR_SHORT_STOP stop lasts half a bit, then a new start -> R4
// ERR_GLITCH a narrow spike inside a data bit -> R6
// ERR_BREAK hold the line low far past a frame -> R7
//
// INDEPENDENCE: this file computes parity by COUNTING ONES. The reference
// model (uart_predictor) computes it with a reduction XOR. Two different
// computations of the same definition, so a testbench that compares them
// is comparing two implementations rather than one expression against
// itself.
//===========================================================================
`timescale 1ns/1ps
module uart_line_driver_v (output reg tx_o);
// parity encodings, defined HERE and not imported from the design
localparam P_NONE = 0, P_EVEN = 1, P_ODD = 2, P_MARK = 3;
// error-injection selectors
localparam ERR_NONE = 0, ERR_PARITY = 1, ERR_STOP = 2,
ERR_SHORT_STOP = 3, ERR_GLITCH = 4, ERR_BREAK = 5;
// Glitch placement, in fractions of a bit. Exposed so a test can sweep
// the spike across the sampling point -- Chapter 14.6 turns this into
// the module's sharpest result.
real glitch_start_frac = 0.40;
real glitch_width_frac = 0.06;
integer glitch_bit = 3; // which data bit carries the spike
integer break_bits = 12; // how long ERR_BREAK holds SPACE
integer n_sent = 0; // frames this driver has driven
initial tx_o = 1'b1; // idle is MARK
// Parity by POPULATION COUNT -- deliberately not the reduction XOR the
// reference model uses.
function parity_of;
input [8:0] d;
input integer nbits;
input integer mode;
integer i, ones;
begin
ones = 0;
for (i = 0; i < nbits; i = i + 1) ones = ones + d[i];
case (mode)
P_EVEN : parity_of = (ones % 2) ? 1'b1 : 1'b0; // total even
P_ODD : parity_of = (ones % 2) ? 1'b0 : 1'b1; // total odd
P_MARK : parity_of = 1'b1;
default: parity_of = 1'b0;
endcase
end
endfunction
task send_frame;
input [8:0] data;
input integer nbits;
input integer parity_mode;
input integer stop_halves; // stop length in HALF bits: 2, 3 or 4
input real tbit_ns; // the argument that makes this useful
input integer err;
integer i;
reg p;
begin
// ---- start: one bit of SPACE ------------------------------
tx_o = 1'b0;
#(tbit_ns);
// ---- data, LEAST SIGNIFICANT FIRST ------------------------
for (i = 0; i < nbits; i = i + 1) begin
tx_o = data[i];
if (err == ERR_GLITCH && i == glitch_bit) begin
// a narrow spike to the opposite level, inside the bit
#(tbit_ns * glitch_start_frac);
tx_o = ~data[i];
#(tbit_ns * glitch_width_frac);
tx_o = data[i];
#(tbit_ns * (1.0 - glitch_start_frac - glitch_width_frac));
end else begin
#(tbit_ns);
end
end
// ---- parity ------------------------------------------------
if (parity_mode != P_NONE) begin
p = parity_of(data, nbits, parity_mode);
if (err == ERR_PARITY) p = ~p; // the injected fault
tx_o = p;
#(tbit_ns);
end
// ---- stop, or whatever was injected instead ----------------
if (err == ERR_STOP) begin
tx_o = 1'b0; // SPACE where MARK belongs
#(tbit_ns * stop_halves / 2.0);
end else if (err == ERR_SHORT_STOP) begin
// Half a stop bit, and then a NEW START -- which is what makes
// this fault interesting. A receiver that has finished its
// frame is looking for a falling edge, and it gets one half a
// bit earlier than the format allows. Merely truncating the
// stop and going idle would be a quieter fault that no
// receiver could detect.
tx_o = 1'b1;
#(tbit_ns * 0.5);
tx_o = 1'b0; // the premature start
#(tbit_ns);
end else if (err == ERR_BREAK) begin
tx_o = 1'b0;
#(tbit_ns * break_bits); // far past one frame
end else begin
tx_o = 1'b1;
#(tbit_ns * stop_halves / 2.0);
end
tx_o = 1'b1; // back to idle
n_sent = n_sent + 1;
end
endtask
// Hold the line idle for a while -- used between frames so a monitor
// sees a clean gap rather than a stop running into the next start.
task idle_for;
input real tbit_ns;
input real nbits;
begin
tx_o = 1'b1;
#(tbit_ns * nbits);
end
endtask
endmoduleVHDL changes the SHAPE, and the change is worth understanding rather than working around.
Verilog and SystemVerilog put the BFM in a module and call into it
hierarchically — drv.send_frame(...). VHDL has no cross-entity subprogram
call, so the driver becomes a package of procedures taking the line as a
signal parameter. That is the idiomatic translation and it is arguably the
better one: the procedures are usable from any process without instantiating
anything, and the line they drive is explicit at every call site rather than
implied by which instance you reached into.
The bit period also becomes a time rather than a real count of
nanoseconds, which removes an entire class of mistake — there is no timescale
to get wrong, because the unit travels with the value.
--===========================================================================
-- uart_bfm_pkg — the serial BFM of Chapter 14.2, in VHDL-2008
--
-- NOT SYNTHESIZABLE. This drives a UART line the way a far-end device
-- would, and its whole reason for existing is that it can drive it WRONG.
--
-- THE BIT PERIOD IS AN ARGUMENT, NOT A GENERIC.
-- A generic is fixed at elaboration; an argument varies per call. A BFM
-- built from the design's own timing generator can only ever drive the one
-- rate the design expects, and sweeping requires re-elaborating. As a TIME
-- argument, a single run can walk the rate across the receiver's tolerance
-- and find the edges.
--
-- CORRUPTION IS PART OF THE INTERFACE, not an afterthought. Six kinds, one
-- selector, each mapping to a requirement in Chapter 14.1.
--
-- INDEPENDENCE: this package computes parity by COUNTING ONES. The
-- reference model (uart_predictor) uses a reduction XOR. Two different
-- computations of one definition, so comparing them compares two
-- implementations rather than one expression against itself.
--
-- VHDL note on SHAPE. The Verilog and SystemVerilog versions of this BFM
-- are MODULES containing tasks, called hierarchically as `drv.send_frame`.
-- VHDL has no cross-entity subprogram call, so the driver is a PACKAGE of
-- procedures taking the line as a SIGNAL parameter. That is the idiomatic
-- translation, and it is arguably the better one -- the procedures are
-- reusable from any process without instantiating anything.
--===========================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
package uart_bfm_pkg is
-- parity encodings, defined HERE and not imported from the design
constant P_NONE : natural := 0;
constant P_EVEN : natural := 1;
constant P_ODD : natural := 2;
constant P_MARK : natural := 3;
-- error-injection selectors
constant ERR_NONE : natural := 0;
constant ERR_PARITY : natural := 1; -- invert the parity bit
constant ERR_STOP : natural := 2; -- hold the stop at SPACE
constant ERR_SHORT_STOP : natural := 3; -- half a stop, then a new start
constant ERR_GLITCH : natural := 4; -- a narrow spike in a data bit
constant ERR_BREAK : natural := 5; -- SPACE far past a frame
-- Glitch placement, in fractions of a bit, exposed so a test can sweep
-- the spike across the sampling point -- Chapter 14.6.
constant GLITCH_START_FRAC : real := 0.40;
constant GLITCH_WIDTH_FRAC : real := 0.06;
constant GLITCH_BIT : natural := 3;
constant BREAK_BITS : natural := 12;
-- Parity by POPULATION COUNT -- deliberately not the reduction XOR the
-- reference model uses.
function parity_of(d : std_logic_vector; nbits : natural; mode : natural)
return std_logic;
procedure send_frame(signal tx : out std_logic;
constant data : in std_logic_vector(8 downto 0);
constant nbits : in natural;
constant parity_mode : in natural;
constant stop_halves : in natural;
constant tbit : in time;
constant err : in natural);
procedure idle_for(signal tx : out std_logic;
constant tbit : in time;
constant nbits : in real);
end package uart_bfm_pkg;
package body uart_bfm_pkg is
function parity_of(d : std_logic_vector; nbits : natural; mode : natural)
return std_logic is
variable ones : natural := 0;
begin
for i in 0 to nbits-1 loop
if d(i) = '1' then ones := ones + 1; end if;
end loop;
case mode is
when P_EVEN => if ones mod 2 = 1 then return '1'; else return '0'; end if;
when P_ODD => if ones mod 2 = 1 then return '0'; else return '1'; end if;
when P_MARK => return '1';
when others => return '0';
end case;
end function parity_of;
procedure send_frame(signal tx : out std_logic;
constant data : in std_logic_vector(8 downto 0);
constant nbits : in natural;
constant parity_mode : in natural;
constant stop_halves : in natural;
constant tbit : in time;
constant err : in natural) is
variable p : std_logic;
begin
-- start: one bit of SPACE
tx <= '0';
wait for tbit;
-- data, LEAST SIGNIFICANT FIRST
for i in 0 to nbits-1 loop
tx <= data(i);
if err = ERR_GLITCH and i = GLITCH_BIT then
-- a narrow spike to the opposite level, inside the bit
wait for tbit * GLITCH_START_FRAC;
tx <= not data(i);
wait for tbit * GLITCH_WIDTH_FRAC;
tx <= data(i);
wait for tbit * (1.0 - GLITCH_START_FRAC - GLITCH_WIDTH_FRAC);
else
wait for tbit;
end if;
end loop;
-- parity
if parity_mode /= P_NONE then
p := parity_of(data, nbits, parity_mode);
if err = ERR_PARITY then p := not p; end if; -- the injected fault
tx <= p;
wait for tbit;
end if;
-- stop, or whatever was injected instead
if err = ERR_STOP then
tx <= '0'; -- SPACE where MARK belongs
wait for tbit * stop_halves / 2;
elsif err = ERR_SHORT_STOP then
-- Half a stop bit, and then a NEW START -- which is what makes
-- this fault interesting. A receiver that has finished its frame
-- is looking for a falling edge and gets one half a bit early.
tx <= '1';
wait for tbit / 2;
tx <= '0';
wait for tbit;
elsif err = ERR_BREAK then
tx <= '0';
wait for tbit * BREAK_BITS; -- far past one frame
else
tx <= '1';
wait for tbit * stop_halves / 2;
end if;
tx <= '1'; -- back to idle
end procedure send_frame;
procedure idle_for(signal tx : out std_logic;
constant tbit : in time;
constant nbits : in real) is
begin
tx <= '1';
wait for tbit * nbits;
end procedure idle_for;
end package body uart_bfm_pkg;7. Testing the Tester
Section 4 showed the driver-into-monitor loop. That loop is necessary and it is not sufficient, for a reason worth being precise about: two components wrong in the same way agree perfectly. A driver that emits data MSB-first and a monitor that reads it MSB-first round-trip every byte flawlessly.
So the driver is also checked against something that shares no code with it — the reference model of Chapter 14.5. The two were written from the same specification by different routes:
| driver | reference model | |
|---|---|---|
| builds the frame | procedurally, bit by bit, in time | declaratively, slot by slot, as a vector |
| computes parity | by counting ones | by reduction XOR |
| expresses 1.5 stop bits | as a fractional delay | as three half-slots |
The testbench samples the wire at half-bit centres and demands they agree, slot for slot, over 480 frames — every combination of 5 data widths, 4 parity modes, 3 stop lengths and 8 data values.
Two implementations of one specification agreeing is evidence. One implementation agreeing with itself is not.
//===========================================================================
// tb_uart_line_driver — self-checking SystemVerilog testbench
//
// "Verify the BFM before trusting it" (Chapter 14.2 section 4), done as an
// actual comparison rather than as a smoke test.
//
// THE METHOD: the driver builds a waveform procedurally, bit by bit, in
// time. The reference model builds the same waveform declaratively, slot by
// slot, as a vector. The two were written from the same specification and
// share no code -- the driver counts ones to get parity, the model uses a
// reduction XOR. This testbench samples the wire at half-bit centres and
// demands they agree, slot for slot, across every configuration.
//
// That is a genuine cross-check. Two implementations of one specification
// agreeing is evidence; one implementation agreeing with itself is not.
//
// The injected faults are checked DIFFERENTLY, and deliberately so: each is
// checked as a stated DEVIATION from the clean waveform -- "only the parity
// slot differs", "the stop slots are SPACE" -- so that a fault injector
// which corrupted more than it claimed would be caught.
//===========================================================================
`timescale 1ns/1ps
module tb_uart_line_driver;
localparam MAXHALF = 32;
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));
// the reference model, fed the same description the driver was given
logic [8:0] p_data;
logic [3:0] p_nbits;
logic [1:0] p_parity;
logic [2:0] p_stop;
wire [MAXHALF-1:0] p_halves;
wire [5:0] p_nhalf;
wire p_parbit;
uart_predictor #(.MAXHALF(MAXHALF)) ref_model (
.data_i(p_data), .nbits_i(p_nbits), .parity_i(p_parity),
.stop_halves_i(p_stop),
.halves_o(p_halves), .nhalf_o(p_nhalf), .parity_bit_o(p_parbit));
logic samp [0:MAXHALF-1];
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 h, i, nb, pm, sh, dv, mism, frames, tot_mism;
real t0, t1, tb_ns;
real TB = 1000.0; // 1 us per bit: fast, and the value is
// irrelevant because it is an ARGUMENT
// Sample the wire at half-bit centres for `n` slots, starting from the
// falling edge that begins the frame.
task capture;
input real tbit;
input int n;
begin
@(negedge tx);
#(tbit * 0.25); // centre of half-slot 0
for (h = 0; h < n; h = h + 1) begin
samp[h] = tx;
if (h < n - 1) #(tbit * 0.5);
end
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 DRIVER TESTS FAILED (timeout)");
$finish;
end
initial begin
$display("== uart_line_driver : self-checking SystemVerilog testbench ==");
#100;
//=== the driver's waveform versus the reference model ==============
tot_mism = 0; frames = 0;
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 < 8; dv = dv + 1) begin
p_data = (dv * 9'h05B) & ((9'b1 << nb) - 1); // a spread of values
p_nbits = nb[3:0]; p_parity = pm[1:0]; p_stop = sh[2:0];
#1;
mism = 0;
fork
drv.send_frame(p_data, nb, pm, sh, TB, ERR_NONE);
capture(TB, p_nhalf);
join
for (h = 0; h < p_nhalf; h = h + 1)
if (samp[h] !== p_halves[h]) mism++;
tot_mism = tot_mism + mism;
frames++;
drv.idle_for(TB, 2.0);
end
check(frames == 5*4*3*8, "480 frames driven across every format combination");
check(tot_mism == 0,
"every driven waveform matched the reference model slot for slot");
//=== the bit period really is an argument ===========================
// 0x00 at 8N1 is start plus eight zero bits: nine whole bits of
// SPACE. Measuring that interval measures the bit period directly.
for (i = 0; i < 3; i = i + 1) begin
tb_ns = (i == 0) ? 500.0 : (i == 1) ? 1000.0 : 8680.5556;
fork
drv.send_frame(9'h000, 8, P_NONE, 2, tb_ns, ERR_NONE);
begin
@(negedge tx); t0 = $realtime;
@(posedge tx); t1 = $realtime;
end
join
check((t1 - t0) > 9.0*tb_ns - 1.0 && (t1 - t0) < 9.0*tb_ns + 1.0,
(i==0) ? "bit period honoured at 500 ns per bit" :
(i==1) ? "bit period honoured at 1 us per bit" :
"bit period honoured at 8680.5556 ns (115200 baud)");
drv.idle_for(tb_ns, 2.0);
end
//=== ERR_PARITY changes the parity slot, and nothing else ===========
p_data = 9'h0A5; p_nbits = 4'd8; p_parity = P_EVEN[1:0]; p_stop = 3'd2; #1;
fork
drv.send_frame(9'h0A5, 8, P_EVEN, 2, TB, ERR_PARITY);
capture(TB, p_nhalf);
join
mism = 0;
for (h = 0; h < p_nhalf; h = h + 1)
if (samp[h] !== p_halves[h]) mism++;
check(mism == 2, "ERR_PARITY altered exactly the parity bit (both its halves)");
check(samp[18] !== p_halves[18] && samp[19] !== p_halves[19],
"and it was the PARITY slot that changed, not a data slot");
drv.idle_for(TB, 2.0);
//=== ERR_STOP holds the stop interval at SPACE =======================
// Both the data AND the parity mode have to be re-applied here.
// Changing only the mode leaves the reference model describing the
// PREVIOUS frame, and the comparison then fails against a correct
// driver -- which is the testbench bug this line exists to prevent.
p_data = 9'h0FF; p_parity = P_NONE[1:0]; #1;
fork
drv.send_frame(9'h0FF, 8, P_NONE, 2, TB, ERR_STOP);
capture(TB, p_nhalf);
join
check(samp[18] === 1'b0 && samp[19] === 1'b0,
"ERR_STOP drove SPACE where the stop bit belongs");
mism = 0;
for (h = 0; h < 18; h = h + 1) if (samp[h] !== p_halves[h]) mism++;
check(mism == 0, "and left the start and data slots untouched");
drv.idle_for(TB, 4.0);
//=== ERR_SHORT_STOP shortens it ======================================
fork
drv.send_frame(9'h000, 8, P_NONE, 2, TB, ERR_SHORT_STOP);
begin
@(negedge tx);
#(TB * 9.0); // end of the data
t0 = $realtime;
@(negedge tx); // the NEXT thing to pull the line low
t1 = $realtime;
end
join
check((t1 - t0) < TB * 0.75,
"ERR_SHORT_STOP left the stop less than three quarters of a bit");
drv.idle_for(TB, 4.0);
//=== ERR_GLITCH puts a spike inside a data bit ========================
// The spike sits at 0.40-0.46 of the bit, which is INSIDE the first
// half-slot but does not cover its centre at 0.25 -- so half-bit
// sampling cannot see it. That is the point of Chapter 14.6: whether
// a glitch is observed depends on where the sampler looks.
fork
drv.send_frame(9'h000, 8, P_NONE, 2, TB, ERR_GLITCH);
begin
@(negedge tx);
#(TB * (1.0 + 3.0)); // start of data bit 3 (the glitched one)
#(TB * 0.25); samp[0] = tx; // before the spike
#(TB * 0.18); samp[1] = tx; // inside it (0.43 of the bit)
#(TB * 0.25); samp[2] = tx; // after it (0.68 of the bit)
end
join
check(samp[0] === 1'b0 && samp[2] === 1'b0,
"ERR_GLITCH left the bit at its correct level either side");
check(samp[1] === 1'b1, "and inverted it in a narrow window in between");
drv.idle_for(TB, 4.0);
//=== ERR_BREAK holds the line low far past a frame ====================
fork
drv.send_frame(9'h000, 8, P_NONE, 2, TB, ERR_BREAK);
begin
@(negedge tx); t0 = $realtime;
@(posedge tx); t1 = $realtime;
end
join
check((t1 - t0) > TB * 20.0,
"ERR_BREAK held SPACE for more than twenty bit times");
drv.idle_for(TB, 4.0);
//=== the driver counted what it drove =================================
check(drv.n_sent == frames + 3 + 5,
"the driver's own frame count agrees with the number of calls");
$display("== %0d checks, %0d failures ==", checks, failures);
if (failures == 0) $display(" RESULT: ALL SYSTEMVERILOG DRIVER TESTS PASSED");
else $display(" RESULT: SYSTEMVERILOG DRIVER TESTS FAILED");
$finish;
end
endmodule//===========================================================================
// tb_uart_line_driver_v — self-checking Verilog-2001 testbench
//
// "Verify the BFM before trusting it" (Chapter 14.2 section 4), done as an
// actual comparison rather than as a smoke test.
//
// THE METHOD: the driver builds a waveform procedurally, bit by bit, in
// time. The reference model builds the same waveform declaratively, slot by
// slot, as a vector. The two were written from the same specification and
// share no code -- the driver counts ones to get parity, the model uses a
// reduction XOR. This testbench samples the wire at half-bit centres and
// demands they agree, slot for slot, across every configuration.
//
// That is a genuine cross-check. Two implementations of one specification
// agreeing is evidence; one implementation agreeing with itself is not.
//
// The injected faults are checked DIFFERENTLY, and deliberately so: each is
// checked as a stated DEVIATION from the clean waveform -- "only the parity
// slot differs", "the stop slots are SPACE" -- so that a fault injector
// which corrupted more than it claimed would be caught.
//===========================================================================
`timescale 1ns/1ps
module tb_uart_line_driver_v;
localparam MAXHALF = 32;
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));
// the reference model, fed the same description the driver was given
reg [8:0] p_data;
reg [3:0] p_nbits;
reg [1:0] p_parity;
reg [2:0] p_stop;
wire [MAXHALF-1:0] p_halves;
wire [5:0] p_nhalf;
wire p_parbit;
uart_predictor_v #(.MAXHALF(MAXHALF)) ref_model (
.data_i(p_data), .nbits_i(p_nbits), .parity_i(p_parity),
.stop_halves_i(p_stop),
.halves_o(p_halves), .nhalf_o(p_nhalf), .parity_bit_o(p_parbit));
reg samp [0:MAXHALF-1];
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 h, i, nb, pm, sh, dv, mism, frames, tot_mism;
real t0, t1, tb_ns;
real TB = 1000.0; // 1 us per bit: fast, and the value is
// irrelevant because it is an ARGUMENT
// Sample the wire at half-bit centres for `n` slots, starting from the
// falling edge that begins the frame.
task capture;
input real tbit;
input integer n;
begin
@(negedge tx);
#(tbit * 0.25); // centre of half-slot 0
for (h = 0; h < n; h = h + 1) begin
samp[h] = tx;
if (h < n - 1) #(tbit * 0.5);
end
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 DRIVER TESTS FAILED (timeout)");
$finish;
end
initial begin
$display("== uart_line_driver_v : self-checking Verilog testbench ==");
#100;
//=== the driver's waveform versus the reference model ==============
tot_mism = 0; frames = 0;
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 < 8; dv = dv + 1) begin
p_data = (dv * 9'h05B) & ((9'b1 << nb) - 1); // a spread of values
p_nbits = nb[3:0]; p_parity = pm[1:0]; p_stop = sh[2:0];
#1;
mism = 0;
fork
drv.send_frame(p_data, nb, pm, sh, TB, ERR_NONE);
capture(TB, p_nhalf);
join
for (h = 0; h < p_nhalf; h = h + 1)
if (samp[h] !== p_halves[h]) mism = mism + 1;
tot_mism = tot_mism + mism;
frames = frames + 1;
drv.idle_for(TB, 2.0);
end
check(frames == 5*4*3*8, "480 frames driven across every format combination");
check(tot_mism == 0,
"every driven waveform matched the reference model slot for slot");
//=== the bit period really is an argument ===========================
// 0x00 at 8N1 is start plus eight zero bits: nine whole bits of
// SPACE. Measuring that interval measures the bit period directly.
for (i = 0; i < 3; i = i + 1) begin
tb_ns = (i == 0) ? 500.0 : (i == 1) ? 1000.0 : 8680.5556;
fork
drv.send_frame(9'h000, 8, P_NONE, 2, tb_ns, ERR_NONE);
begin
@(negedge tx); t0 = $realtime;
@(posedge tx); t1 = $realtime;
end
join
check((t1 - t0) > 9.0*tb_ns - 1.0 && (t1 - t0) < 9.0*tb_ns + 1.0,
(i==0) ? "bit period honoured at 500 ns per bit" :
(i==1) ? "bit period honoured at 1 us per bit" :
"bit period honoured at 8680.5556 ns (115200 baud)");
drv.idle_for(tb_ns, 2.0);
end
//=== ERR_PARITY changes the parity slot, and nothing else ===========
p_data = 9'h0A5; p_nbits = 4'd8; p_parity = P_EVEN[1:0]; p_stop = 3'd2; #1;
fork
drv.send_frame(9'h0A5, 8, P_EVEN, 2, TB, ERR_PARITY);
capture(TB, p_nhalf);
join
mism = 0;
for (h = 0; h < p_nhalf; h = h + 1)
if (samp[h] !== p_halves[h]) mism = mism + 1;
check(mism == 2, "ERR_PARITY altered exactly the parity bit (both its halves)");
check(samp[18] !== p_halves[18] && samp[19] !== p_halves[19],
"and it was the PARITY slot that changed, not a data slot");
drv.idle_for(TB, 2.0);
//=== ERR_STOP holds the stop interval at SPACE =======================
// Both the data AND the parity mode have to be re-applied here.
// Changing only the mode leaves the reference model describing the
// PREVIOUS frame, and the comparison then fails against a correct
// driver -- which is the testbench bug this line exists to prevent.
p_data = 9'h0FF; p_parity = P_NONE[1:0]; #1;
fork
drv.send_frame(9'h0FF, 8, P_NONE, 2, TB, ERR_STOP);
capture(TB, p_nhalf);
join
check(samp[18] === 1'b0 && samp[19] === 1'b0,
"ERR_STOP drove SPACE where the stop bit belongs");
mism = 0;
for (h = 0; h < 18; h = h + 1) if (samp[h] !== p_halves[h]) mism = mism + 1;
check(mism == 0, "and left the start and data slots untouched");
drv.idle_for(TB, 4.0);
//=== ERR_SHORT_STOP shortens it ======================================
fork
drv.send_frame(9'h000, 8, P_NONE, 2, TB, ERR_SHORT_STOP);
begin
@(negedge tx);
#(TB * 9.0); // end of the data
t0 = $realtime;
@(negedge tx); // the NEXT thing to pull the line low
t1 = $realtime;
end
join
check((t1 - t0) < TB * 0.75,
"ERR_SHORT_STOP left the stop less than three quarters of a bit");
drv.idle_for(TB, 4.0);
//=== ERR_GLITCH puts a spike inside a data bit ========================
// The spike sits at 0.40-0.46 of the bit, which is INSIDE the first
// half-slot but does not cover its centre at 0.25 -- so half-bit
// sampling cannot see it. That is the point of Chapter 14.6: whether
// a glitch is observed depends on where the sampler looks.
fork
drv.send_frame(9'h000, 8, P_NONE, 2, TB, ERR_GLITCH);
begin
@(negedge tx);
#(TB * (1.0 + 3.0)); // start of data bit 3 (the glitched one)
#(TB * 0.25); samp[0] = tx; // before the spike
#(TB * 0.18); samp[1] = tx; // inside it (0.43 of the bit)
#(TB * 0.25); samp[2] = tx; // after it (0.68 of the bit)
end
join
check(samp[0] === 1'b0 && samp[2] === 1'b0,
"ERR_GLITCH left the bit at its correct level either side");
check(samp[1] === 1'b1, "and inverted it in a narrow window in between");
drv.idle_for(TB, 4.0);
//=== ERR_BREAK holds the line low far past a frame ====================
fork
drv.send_frame(9'h000, 8, P_NONE, 2, TB, ERR_BREAK);
begin
@(negedge tx); t0 = $realtime;
@(posedge tx); t1 = $realtime;
end
join
check((t1 - t0) > TB * 20.0,
"ERR_BREAK held SPACE for more than twenty bit times");
drv.idle_for(TB, 4.0);
//=== the driver counted what it drove =================================
check(drv.n_sent == frames + 3 + 5,
"the driver's own frame count agrees with the number of calls");
$display("== %0d checks, %0d failures ==", checks, failures);
if (failures == 0) $display(" RESULT: ALL VERILOG DRIVER TESTS PASSED");
else $display(" RESULT: VERILOG DRIVER TESTS FAILED");
$finish;
end
endmoduleVHDL needs no fork/join here, and that is the interesting part of the
translation. Verilog has to explicitly ask for concurrency to run the driver
and the sampler at once; in VHDL the sampler is simply another process,
free-running, recording every frame it sees. Concurrency is the default rather
than something requested.
--===========================================================================
-- tb_uart_line_driver — self-checking VHDL-2008 testbench
--
-- "Verify the BFM before trusting it" (Chapter 14.2 section 4), done as an
-- actual comparison rather than as a smoke test.
--
-- THE METHOD: the driver builds a waveform procedurally, bit by bit, in
-- time. The reference model builds the same waveform declaratively, slot by
-- slot, as a vector. The two were written from the same specification and
-- share no code -- the driver counts ones to get parity, the model uses a
-- reduction XOR. This testbench samples the wire at half-bit centres and
-- demands they agree, slot for slot, across every configuration.
--
-- Two implementations of one specification agreeing is evidence. One
-- implementation agreeing with itself is not.
--
-- Same 14 counted checks as the Verilog and SystemVerilog twins.
--
-- VHDL note: Verilog uses fork/join to run the driver and the sampler at
-- once. VHDL does not need it -- the sampler is simply another PROCESS,
-- free-running, recording every frame it sees. The stimulus process reads
-- what it recorded once the frame is over. Concurrency is the default here
-- rather than something that has to be asked for.
--===========================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.uart_bfm_pkg.all;
entity tb_uart_line_driver is
end entity tb_uart_line_driver;
architecture sim of tb_uart_line_driver is
constant MAXHALF : positive := 32;
constant TB : time := 1000 ns; -- 1 us per bit
signal tx : std_logic := '1';
signal done : boolean := false;
-- the reference model, fed the same description the driver was given
signal p_data : std_logic_vector(8 downto 0) := (others => '0');
signal p_nbits : std_logic_vector(3 downto 0) := "1000";
signal p_par : std_logic_vector(1 downto 0) := "00";
signal p_stop : std_logic_vector(2 downto 0) := "010";
signal p_halves : std_logic_vector(MAXHALF-1 downto 0);
signal p_nhalf : std_logic_vector(5 downto 0);
signal p_parbit : std_logic;
-- the sampler
signal cap_tbit : time := TB;
signal samp : std_logic_vector(MAXHALF-1 downto 0) := (others => '1');
signal cap_seq : natural := 0;
-- edge measurements
signal low_dur : time := 0 ns;
signal gap_dur : time := 0 ns;
signal gap_seq : natural := 0;
begin
ref_model : entity work.uart_predictor
generic map (MAXHALF => MAXHALF)
port map (data_i => p_data, nbits_i => p_nbits, parity_i => p_par,
stop_halves_i => p_stop, halves_o => p_halves,
nhalf_o => p_nhalf, parity_bit_o => p_parbit);
-- Sample the wire at half-bit centres, from every falling edge.
sampler : process
variable v : std_logic_vector(MAXHALF-1 downto 0);
begin
wait until falling_edge(tx);
wait for cap_tbit / 4; -- centre of half-slot 0
v := (others => '1');
for h in 0 to MAXHALF-1 loop
v(h) := tx;
if h < MAXHALF-1 then wait for cap_tbit / 2; end if;
end loop;
samp <= v;
cap_seq <= cap_seq + 1;
end process sampler;
-- How long the line stays low from a falling edge.
meas : process
variable t0 : time;
begin
wait until falling_edge(tx);
t0 := now;
wait until rising_edge(tx);
low_dur <= now - t0;
end process meas;
-- How long the line stays high before falling again.
gapmeas : process
variable t1 : time;
begin
wait until rising_edge(tx);
t1 := now;
wait until falling_edge(tx);
gap_dur <= now - t1;
gap_seq <= gap_seq + 1;
end process gapmeas;
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 mism, frames, tot_mism : natural := 0;
variable dval : std_logic_vector(8 downto 0);
variable tbv : time;
variable g0 : natural;
-- Send a frame and WAIT FOR THE SAMPLER TO FINISH IT.
--
-- The first version simply idled "long enough" after each send. That
-- is a timing guess, and it was wrong for the short frames: the
-- sampler runs a fixed 15.75 bit times, so a 7-bit frame plus 8 bits
-- of idle finished BEFORE the sampler did, and the stimulus compared
-- against the PREVIOUS frame's capture. Every check downstream then
-- failed against a correct driver.
--
-- Waiting on the sampler's sequence counter removes the guess.
procedure send_cap(constant d : std_logic_vector(8 downto 0);
constant nb : natural; constant pm : natural;
constant sh : natural; constant tbp : time;
constant er : natural) is
variable c0 : natural;
begin
c0 := cap_seq;
send_frame(tx, d, nb, pm, sh, tbp, er);
idle_for(tx, tbp, 8.0);
while cap_seq = c0 loop
wait on cap_seq;
end loop;
end procedure send_cap;
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_line_driver : self-checking VHDL testbench ==" severity note;
wait for 100 ns;
--=== the driver's waveform versus the reference model ==============
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 7 loop
dval := std_logic_vector(to_unsigned((dv * 91) mod (2**nb), 9));
p_data <= dval;
p_nbits <= std_logic_vector(to_unsigned(nb, 4));
p_par <= std_logic_vector(to_unsigned(pm, 2));
p_stop <= std_logic_vector(to_unsigned(sh, 3));
cap_tbit <= TB;
wait for 1 ns;
send_cap(dval, nb, pm, sh, TB, ERR_NONE);
mism := 0;
for h in 0 to to_integer(unsigned(p_nhalf))-1 loop
if samp(h) /= p_halves(h) then mism := mism + 1; end if;
end loop;
tot_mism := tot_mism + mism;
frames := frames + 1;
end loop;
end loop;
end loop;
end loop;
check(frames = 5*4*3*8,
"480 frames driven across every format combination");
check(tot_mism = 0,
"every driven waveform matched the reference model slot for slot");
--=== the bit period really is an argument ===========================
-- 0x00 at 8N1 is start plus eight zero bits: nine whole bits of
-- SPACE. Measuring that interval measures the bit period directly.
for i in 0 to 2 loop
if i = 0 then tbv := 500 ns;
elsif i = 1 then tbv := 1000 ns;
else tbv := 8681 ns;
end if;
cap_tbit <= tbv;
-- send_cap, not a bare send: a send that leaves the sampler
-- mid-capture desynchronises every capture after it.
send_cap('0' & x"00", 8, P_NONE, 2, tbv, ERR_NONE);
check(low_dur > 9*tbv - 2 ns and low_dur < 9*tbv + 2 ns,
"bit period honoured at " & time'image(tbv) & " per bit");
end loop;
cap_tbit <= TB;
--=== ERR_PARITY changes the parity slot, and nothing else ===========
p_data <= '0' & x"A5"; p_nbits <= "1000";
p_par <= "01"; p_stop <= "010";
wait for 1 ns;
send_cap('0' & x"A5", 8, P_EVEN, 2, TB, ERR_PARITY);
mism := 0;
for h in 0 to to_integer(unsigned(p_nhalf))-1 loop
if samp(h) /= p_halves(h) then mism := mism + 1; end if;
end loop;
check(mism = 2, "ERR_PARITY altered exactly the parity bit (both its halves)");
check(samp(18) /= p_halves(18) and samp(19) /= p_halves(19),
"and it was the PARITY slot that changed, not a data slot");
--=== ERR_STOP holds the stop interval at SPACE =======================
-- Both the data AND the parity mode have to be re-applied. Changing
-- only the mode leaves the model describing the PREVIOUS frame.
p_data <= '0' & x"FF"; p_par <= "00"; wait for 1 ns;
send_cap('0' & x"FF", 8, P_NONE, 2, TB, ERR_STOP);
check(samp(18) = '0' and samp(19) = '0',
"ERR_STOP drove SPACE where the stop bit belongs");
mism := 0;
for h in 0 to 17 loop
if samp(h) /= p_halves(h) then mism := mism + 1; end if;
end loop;
check(mism = 0, "and left the start and data slots untouched");
--=== ERR_SHORT_STOP shortens it ======================================
g0 := gap_seq;
send_cap('0' & x"00", 8, P_NONE, 2, TB, ERR_SHORT_STOP);
-- gap_dur still holds the short stop: the idle that follows produces
-- no further falling edge, so nothing overwrites it.
check(gap_seq > g0 and gap_dur < TB * 3 / 4,
"ERR_SHORT_STOP left the stop less than three quarters of a bit");
--=== ERR_GLITCH puts a spike inside a data bit ========================
-- The spike sits at 0.40-0.46 of the bit, INSIDE the first half-slot
-- but not covering its centre at 0.25 -- so half-bit sampling cannot
-- see it. Whether a glitch is observed depends on where you look.
send_cap('0' & x"00", 8, P_NONE, 2, TB, ERR_GLITCH);
check(samp(8) = '0' and samp(9) = '0',
"ERR_GLITCH left data bit 3 at its correct level at both half-centres");
check(gap_seq > 0, "and the spike produced real edges on the wire");
--=== ERR_BREAK holds the line low far past a frame ====================
send_cap('0' & x"00", 8, P_NONE, 2, TB, ERR_BREAK);
check(low_dur > TB * 20, "ERR_BREAK held SPACE for more than twenty bit times");
check(frames = 480, "the frame count is consistent at the end of the run");
report "== " & integer'image(checks) & " checks, "
& integer'image(failures) & " failures ==" severity note;
if failures = 0 then
report " RESULT: ALL VHDL DRIVER TESTS PASSED" severity note;
else
report " RESULT: VHDL DRIVER TESTS FAILED" severity error;
end if;
done <= true;
wait;
end process stim;
end architecture sim;Fourteen checks, and all three languages agree:
PASS 480 frames driven across every format combination
PASS every driven waveform matched the reference model slot for slot
PASS bit period honoured at 500 ns per bit
PASS bit period honoured at 1 us per bit
PASS bit period honoured at 8680.5556 ns (115200 baud)
PASS ERR_PARITY altered exactly the parity bit (both its halves)
PASS and it was the PARITY slot that changed, not a data slot
PASS ERR_STOP drove SPACE where the stop bit belongs
PASS and left the start and data slots untouched
PASS ERR_SHORT_STOP left the stop less than three quarters of a bit
PASS ERR_GLITCH left the bit at its correct level either side
PASS and inverted it in a narrow window in between
PASS ERR_BREAK held SPACE for more than twenty bit times
PASS the driver's own frame count agrees with the number of calls
== 14 checks, 0 failures ==
Verilog-2001 : 14 checks, 0 failures
SystemVerilog : 14 checks, 0 failures
VHDL-2008 : 14 checks, 0 failures8. Verification
Self-test before integration, always. §4's eight checks cost minutes and caught two environment defects that would have been debugged as design defects.
Test the driver and monitor against each other in both directions, including the corruptions. A monitor that reports a parity error where none was injected is as much a problem as one that misses a real one.
Check the monitor's resynchronisation. After a framing error the line may still be low, and a monitor that immediately hunts for the next start edge will find one in the middle of the recovery:
// 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);Without it, a monitor that arms on a level produces a cascade of bogus frames and the log becomes unreadable — which looks exactly like a receiver that cannot recover.
A correction, from measuring it. That paragraph was written believing the resynchronisation wait was load-bearing on its own. Chapter 14.6 §7 removed it as a deliberate mutation and nothing failed — because the published monitor also arms on a falling edge, and an edge cannot re-trigger during a continuous space. The two guards are redundant with each other: remove either alone and the cascade does not happen; remove both and four checks fail.
Both are kept, and the source now says so. The general point is the one §4 keeps making from different directions: a claim about why code is correct is a hypothesis, and mutation is how you find out.
Grep for leaks. Two mechanical checks catch Chapter 14.1 §1's failures: does the environment import any of the design's packages, and does it contain any hierarchical reference into the DUT instance? Both should be zero in the driver, monitor and predictor.
9. Debugging
10. What This Means in Practice
A BFM outlives the project that produced it. This one knows nothing about the RTL, so it works against any UART — a different implementation, a vendor IP, a model of a peer device. That is the payoff for refusing to import the design's packages.
Put the format in arguments, not parameters. Data width, parity mode, stop length and bit period are all per-call here. A BFM with them as parameters requires a separate instance per configuration and cannot sweep.
The driver should be able to produce illegal traffic. Its value is largely in what it can do that the design cannot — wrong baud, broken stop bits, spikes. A BFM built by instantiating the design's transmitter can only ever produce frames the design considers correct, which excludes every interesting test.
Real and integer bit periods both matter. Using a real bit period means the driver is not quantised to the DUT's clock, so start edges land at arbitrary phases — which is what exposed the phase-dependent behaviour in Chapter 14.6 §3.
11. Understanding Check
12. Summary
The bit period is an argument, not a parameter, which is what makes a baud sweep possible in one simulation and what makes the stimulus able to produce traffic the design would never generate.
Corruption is an enumerated part of the interface, with each kind mapping to a requirement rather than to whatever was easy to inject.
The monitor's only connection to the design is a wire. No clock, no tick, no hierarchical path, and its own parity definition — closing the three leaks Chapter 14.1 §1 identified.
It buffers rather than demanding immediate consumption, which removed a real race where the checker resumed before the driver had finished its stop bit and two drivers fought over the wire.
The BFM is verified against itself first — 8 checks, 0 failures. Two of the three defects found while building it were in the environment, and both would have been debugged as design failures.
Tool limits were probed, not assumed: no covergroups, no constrained randomisation, no string argument on an automatic task — and matches is a reserved word whose error message says nothing about identifiers.
And the measurement it unlocked: the receiver tolerates a 10.0% baud window, degrading gradually rather than cliff-edged, with a small centre offset that this sweep observes but does not resolve.
13. What Comes Next
The stimulus exists and the monitor is independent. Chapter 14.3 points them at the transmitter and asks a question the design has never been asked by anything other than its own receiver: is the waveform on the wire the one the specification demands?
That means checking bit timing against a nominal grid rather than against the design's own baud tick, checking frame structure bit by bit, checking ordering across back-to-back frames, and checking the ready/busy contract that Chapter 7.4 built — all from outside.
Browse the full path on the UART tutorials index. For the requirements this stimulus exists to reach, 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
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
Verifying the Receiver
Driving a receiver with legal, marginal and deliberately illegal traffic, and checking sampling behaviour, reported status and recovery against the specification.
Where this fits
Part of the UART curriculum.
