UART · Module 16
The Driver and the Bit-Timing Problem
Driving a transaction onto a wire with no clock to synchronise to, and the sequencer that decides whose transaction goes next — built as running code in three languages, with both halves of its contract measured.
A driver for a clocked bus is straightforward: wait for a clocking-block edge, put the values on the wires, wait again. A UART driver has no such edge. The far end of a serial link is a device with its own oscillator, and the driver is that oscillator.
That difference shapes everything in this chapter, and it makes the sequencer's contract matter more than it does on a bus.
1. The Driver Has No Clock
class uart_driver extends uvm_driver #(uart_frame_item);
`uvm_component_utils(uart_driver)
virtual uart_line_if vif;
uart_cfg m_cfg;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(virtual uart_line_if)::get(this, "", "vif", vif))
`uvm_fatal("NOVIF", "no virtual interface for the UART driver")
if (!uvm_config_db#(uart_cfg)::get(this, "", "cfg", m_cfg))
`uvm_fatal("NOCFG", "no uart_cfg for the UART driver")
endfunction
task run_phase(uvm_phase phase);
vif.tx <= 1'b1; // idle MARK before anything
forever begin
seq_item_port.get_next_item(req); // BLOCKS until an item is ready
drive_frame(req);
seq_item_port.item_done(); // and only now may the next one come
end
endtask
// The body is Chapter 14.2's send_frame task, reached through a virtual
// interface instead of a hierarchical name. Note what it does NOT contain:
// there is no @(posedge clk) anywhere in it. The delays are absolute
// times, because the far end of a serial link has its own oscillator and
// the driver is standing in for it.
protected task drive_frame(uart_frame_item t);
vif.tx <= 1'b0; #(t.tbit_ns * 1ns); // start
for (int i = 0; i < t.nbits; i++) begin
vif.tx <= t.data[i]; #(t.tbit_ns * 1ns); // data, LSB first
end
if (t.parity_mode != 0) begin
vif.tx <= parity_of(t); #(t.tbit_ns * 1ns);
end
vif.tx <= 1'b1; #(t.tbit_ns * t.stop_halves / 2.0 * 1ns);
endtask
endclass2. The Sequencer's Two Contracts
Strip away the classes and a sequencer is an arbiter with a blocking handshake. It has exactly two contracts, and they fail in different ways.
The handshake. An item placed in front of the driver stays there, unchanged, until the driver says item_done. A sequencer that moves on early loses transactions, and the loss is silent — the driver simply never saw them.
The arbitration. Every requesting sequence eventually gets served. A fixed-priority arbiter satisfies every other property a sequencer has and starves the last sequence forever.
3. A Sequencer That Runs
UVM's sequencer cannot execute on this toolchain (16.1 §5). The mechanism can, and seeing it at fifty lines is worth more than reading the class.
//===========================================================================
// uart_seq_arb_v — the SEQUENCER, in Verilog-2001
//
// NOT SYNTHESIZABLE. This is a verification component, and it is the part
// of UVM that people find hardest to picture — so it is built here small
// enough to run.
//
// WHAT A UVM SEQUENCER ACTUALLY DOES. Strip away the classes and it is an
// arbiter with a blocking handshake:
//
// - several SEQUENCES each want to send an item;
// - exactly one is granted at a time;
// - the granted item is held in front of the DRIVER until the driver
// says it has finished with it;
// - and then the next sequence gets its turn.
//
// In UVM those four lines are `start_item`, arbitration, `get_next_item`
// and `item_done`. Here they are req/gnt, a round-robin pointer, a held
// valid, and a done input. The mechanism is identical; only the notation
// is poorer.
//
// WHY IT IS BUILT AT ALL. Icarus Verilog 13.0 cannot run UVM -- not the
// library, which is absent, and not a hand-rolled substitute either: it
// refuses queues of class handles, mailboxes, $cast and parameterised
// classes, and it dispatches virtual methods to the BASE class. Each of
// those was probed individually. So the sequencer is written the only way
// it can be made to execute on this toolchain, which turns out to be a
// useful way to see it.
//
// ROUND-ROBIN IS A POLICY, NOT A LAW. UVM's default arbitration is
// SEQ_ARB_FIFO; round-robin is chosen here because starvation is then a
// property a testbench can actually assert on.
//===========================================================================
`timescale 1ns/1ps
module uart_seq_arb_v #(
parameter NSEQ = 4, // how many sequences share the driver
parameter W = 9 // transaction payload width
) (
input wire clk,
input wire rst_n,
// ---- the sequence side: NSEQ producers -----------------------------
input wire [NSEQ-1:0] req_i, // this sequence has an item ready
input wire [NSEQ*W-1:0] item_i, // flattened: sequence k at [k*W +: W]
output reg [NSEQ-1:0] gnt_o, // ONE cycle, when an item is taken
// ---- the driver side: one consumer ---------------------------------
output reg [W-1:0] drv_item_o,
output reg drv_valid_o, // held until drv_done_i
input wire drv_done_i, // the driver's item_done()
// ---- observation ----------------------------------------------------
output reg [31:0] n_granted_o,
output reg [3:0] last_seq_o // which sequence was served last
);
reg [3:0] rr_ptr; // round-robin pointer
reg [3:0] pick;
reg found;
integer i, k;
// Choose the next requesting sequence, starting one past the last served.
// Written as a loop rather than a priority encoder so the rotation is
// visible: a fixed-priority arbiter starves sequence NSEQ-1 forever, and
// that is the defect the testbench is built to catch.
always @* begin
found = 1'b0;
pick = 4'd0;
for (i = 0; i < NSEQ; i = i + 1) begin
k = (rr_ptr + i) % NSEQ;
if (!found && req_i[k]) begin
found = 1'b1;
pick = k[3:0];
end
end
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
gnt_o <= {NSEQ{1'b0}};
drv_item_o <= {W{1'b0}};
drv_valid_o <= 1'b0;
n_granted_o <= 32'd0;
rr_ptr <= 4'd0;
last_seq_o <= 4'd0;
end else begin
gnt_o <= {NSEQ{1'b0}}; // grant is ONE cycle by construction
if (!drv_valid_o) begin
// idle: take an item if anyone is asking
if (found) begin
drv_item_o <= item_i[pick*W +: W];
drv_valid_o <= 1'b1;
gnt_o[pick] <= 1'b1;
n_granted_o <= n_granted_o + 1;
last_seq_o <= pick;
rr_ptr <= (pick + 1) % NSEQ;
end
end else if (drv_done_i) begin
// the driver has finished: release, and allow the next pick
drv_valid_o <= 1'b0;
end
// While drv_valid_o is high and drv_done_i is low, NOTHING moves.
// That is the blocking half of get_next_item, and it is the whole
// reason a sequencer exists rather than a fan-in mux.
end
end
endmodule//===========================================================================
// uart_seq_arb — the SEQUENCER, in SystemVerilog
//
// NOT SYNTHESIZABLE. This is a verification component, and it is the part
// of UVM that people find hardest to picture — so it is built here small
// enough to run.
//
// WHAT A UVM SEQUENCER ACTUALLY DOES. Strip away the classes and it is an
// arbiter with a blocking handshake:
//
// - several SEQUENCES each want to send an item;
// - exactly one is granted at a time;
// - the granted item is held in front of the DRIVER until the driver
// says it has finished with it;
// - and then the next sequence gets its turn.
//
// In UVM those four lines are `start_item`, arbitration, `get_next_item`
// and `item_done`. Here they are req/gnt, a round-robin pointer, a held
// valid, and a done input. The mechanism is identical; only the notation
// is poorer.
//
// WHY IT IS BUILT AT ALL. Icarus Verilog 13.0 cannot run UVM -- not the
// library, which is absent, and not a hand-rolled substitute either: it
// refuses queues of class handles, mailboxes, $cast and parameterised
// classes, and it dispatches virtual methods to the BASE class. Each of
// those was probed individually. So the sequencer is written the only way
// it can be made to execute on this toolchain, which turns out to be a
// useful way to see it.
//
// ROUND-ROBIN IS A POLICY, NOT A LAW. UVM's default arbitration is
// SEQ_ARB_FIFO; round-robin is chosen here because starvation is then a
// property a testbench can actually assert on.
//===========================================================================
`timescale 1ns/1ps
module uart_seq_arb #(
parameter NSEQ = 4, // how many sequences share the driver
parameter W = 9 // transaction payload width
) (
input wire clk,
input wire rst_n,
// ---- the sequence side: NSEQ producers -----------------------------
input wire [NSEQ-1:0] req_i, // this sequence has an item ready
input wire [NSEQ*W-1:0] item_i, // flattened: sequence k at [k*W +: W]
output logic [NSEQ-1:0] gnt_o, // ONE cycle, when an item is taken
// ---- the driver side: one consumer ---------------------------------
output logic [W-1:0] drv_item_o,
output logic drv_valid_o, // held until drv_done_i
input wire drv_done_i, // the driver's item_done()
// ---- observation ----------------------------------------------------
output logic [31:0] n_granted_o,
output logic [3:0] last_seq_o // which sequence was served last
);
logic [3:0] rr_ptr; // round-robin pointer
logic [3:0] pick;
logic found;
int i, k;
// Choose the next requesting sequence, starting one past the last served.
// Written as a loop rather than a priority encoder so the rotation is
// visible: a fixed-priority arbiter starves sequence NSEQ-1 forever, and
// that is the defect the testbench is built to catch.
always @* begin
found = 1'b0;
pick = 4'd0;
for (i = 0; i < NSEQ; i = i + 1) begin
k = (rr_ptr + i) % NSEQ;
if (!found && req_i[k]) begin
found = 1'b1;
pick = k[3:0];
end
end
end
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
gnt_o <= {NSEQ{1'b0}};
drv_item_o <= {W{1'b0}};
drv_valid_o <= 1'b0;
n_granted_o <= 32'd0;
rr_ptr <= 4'd0;
last_seq_o <= 4'd0;
end else begin
gnt_o <= {NSEQ{1'b0}}; // grant is ONE cycle by construction
if (!drv_valid_o) begin
// idle: take an item if anyone is asking
if (found) begin
drv_item_o <= item_i[pick*W +: W];
drv_valid_o <= 1'b1;
gnt_o[pick] <= 1'b1;
n_granted_o <= n_granted_o + 1;
last_seq_o <= pick;
rr_ptr <= (pick + 1) % NSEQ;
end
end else if (drv_done_i) begin
// the driver has finished: release, and allow the next pick
drv_valid_o <= 1'b0;
end
// While drv_valid_o is high and drv_done_i is low, NOTHING moves.
// That is the blocking half of get_next_item, and it is the whole
// reason a sequencer exists rather than a fan-in mux.
end
end
endmoduleVHDL has no UVM either — its equivalents are OSVVM and UVVM, neither installed here. The arbitration is the same rotation, expressed with a mod and a loop.
--===========================================================================
-- uart_seq_arb — the SEQUENCER, in VHDL-2008
--
-- NOT SYNTHESIZABLE. A verification component.
--
-- WHAT A UVM SEQUENCER ACTUALLY DOES. Strip away the classes and it is an
-- arbiter with a blocking handshake: several sequences want to send items,
-- exactly one is granted at a time, the granted item is held in front of
-- the driver until the driver says it has finished, and then the next
-- sequence gets its turn.
--
-- In UVM those are start_item, arbitration, get_next_item and item_done.
-- Here they are req/gnt, a round-robin pointer, a held valid and a done
-- input. The mechanism is identical; only the notation is poorer.
--
-- WHY IT IS BUILT AT ALL. Neither of this curriculum's simulators can run
-- UVM. There is no uvm_pkg on the machine, and Icarus Verilog 13.0 cannot
-- run a hand-rolled substitute either: it refuses queues of class handles,
-- mailboxes, $cast and parameterised classes, and it dispatches virtual
-- methods to the BASE class. Each was probed individually. VHDL has no UVM
-- at all -- its equivalents are OSVVM and UVVM, neither installed here.
--
-- So the mechanism is written the only way it can be made to execute,
-- which turns out to be a useful way to see it.
--===========================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity uart_seq_arb is
generic (
NSEQ : positive := 4; -- how many sequences share the driver
W : positive := 9 -- transaction payload width
);
port (
clk : in std_logic;
rst_n : in std_logic;
-- the sequence side: NSEQ producers
req_i : in std_logic_vector(NSEQ-1 downto 0);
item_i : in std_logic_vector(NSEQ*W-1 downto 0);
gnt_o : out std_logic_vector(NSEQ-1 downto 0);
-- the driver side: one consumer
drv_item_o : out std_logic_vector(W-1 downto 0);
drv_valid_o : out std_logic;
drv_done_i : in std_logic;
-- observation
n_granted_o : out natural;
last_seq_o : out natural
);
end entity uart_seq_arb;
architecture model of uart_seq_arb is
signal rr_ptr : natural := 0;
signal pick : natural := 0;
signal found : boolean := false;
signal valid_s : std_logic := '0';
signal granted_s : natural := 0;
signal last_s : natural := 0;
begin
-- Choose the next requesting sequence, starting one past the last served.
-- Written as a rotation rather than a priority encoder because a
-- fixed-priority arbiter starves the last sequence forever, and that is
-- the defect the testbench is built to catch.
arb : process (all)
variable f : boolean;
variable p : natural;
variable k : natural;
begin
f := false; p := 0;
for i in 0 to NSEQ-1 loop
k := (rr_ptr + i) mod NSEQ;
if not f and req_i(k) = '1' then
f := true; p := k;
end if;
end loop;
found <= f;
pick <= p;
end process arb;
seq : process (clk, rst_n)
begin
if rst_n = '0' then
gnt_o <= (others => '0');
drv_item_o <= (others => '0');
valid_s <= '0';
granted_s <= 0;
rr_ptr <= 0;
last_s <= 0;
elsif rising_edge(clk) then
gnt_o <= (others => '0'); -- grant is ONE cycle by construction
if valid_s = '0' then
if found then
drv_item_o <= item_i((pick+1)*W-1 downto pick*W);
valid_s <= '1';
gnt_o(pick) <= '1';
granted_s <= granted_s + 1;
last_s <= pick;
rr_ptr <= (pick + 1) mod NSEQ;
end if;
elsif drv_done_i = '1' then
valid_s <= '0';
end if;
-- While valid_s is high and drv_done_i is low, NOTHING moves.
-- That is the blocking half of get_next_item, and it is the whole
-- reason a sequencer exists rather than a fan-in mux.
end if;
end process seq;
drv_valid_o <= valid_s;
n_granted_o <= granted_s;
last_seq_o <= last_s;
end architecture model;4. Measuring Both Contracts
//===========================================================================
// tb_uart_seq_arb_v — self-checking Verilog-2001 testbench
//
// The sequencer has two contracts and they fail in different ways:
//
// THE HANDSHAKE. An item placed in front of the driver stays there,
// unchanged, until the driver says item_done. A
// sequencer that moves on early loses transactions and
// the loss is silent -- the driver simply never saw them.
//
// THE ARBITRATION. Every requesting sequence eventually gets served. A
// fixed-priority arbiter satisfies every other check in
// this file and starves the last sequence forever.
//
// INDEPENDENCE: the testbench records what each sequence OFFERED and what
// the driver RECEIVED, and compares the two lists. It does not read the
// arbiter's pointer to predict who should win -- that would be re-deriving
// the answer from the state that produced it.
//===========================================================================
`timescale 1ns/1ps
module tb_uart_seq_arb_v;
localparam NSEQ = 4;
localparam W = 9;
reg clk = 1'b0;
always #5 clk = ~clk;
reg rst_n = 1'b0;
reg [NSEQ-1:0] req = {NSEQ{1'b0}};
reg [NSEQ*W-1:0] item = {(NSEQ*W){1'b0}};
wire [NSEQ-1:0] gnt;
wire [W-1:0] drv_item;
wire drv_valid;
reg drv_done = 1'b0;
wire [31:0] n_granted;
wire [3:0] last_seq;
uart_seq_arb_v #(.NSEQ(NSEQ), .W(W)) dut (
.clk(clk), .rst_n(rst_n),
.req_i(req), .item_i(item), .gnt_o(gnt),
.drv_item_o(drv_item), .drv_valid_o(drv_valid), .drv_done_i(drv_done),
.n_granted_o(n_granted), .last_seq_o(last_seq));
//---- what each sequence offered, and what the driver received --------
reg [W-1:0] offered [0:1023];
reg [W-1:0] received[0:1023];
integer n_offer = 0, n_recv = 0;
integer served [0:NSEQ-1]; // fairness tally
integer mism = 0, held_bad = 0, wide_gnt = 0, multi_gnt = 0;
reg [W-1:0] held_item;
reg held_valid;
reg [NSEQ-1:0] gnt_prev;
integer g;
always @(posedge clk) if (rst_n) begin
// exactly one grant at a time, and never wider than one cycle
if (gnt != 0) begin
if ((gnt & (gnt - 1)) != 0) multi_gnt = multi_gnt + 1;
if (gnt_prev != 0) wide_gnt = wide_gnt + 1;
for (g = 0; g < NSEQ; g = g + 1)
if (gnt[g]) begin
served[g] = served[g] + 1;
offered[n_offer] = item[g*W +: W];
n_offer = n_offer + 1;
end
end
gnt_prev = gnt;
// the item in front of the driver must not change while it is held
if (held_valid && drv_valid && (drv_item !== held_item))
held_bad = held_bad + 1;
held_item = drv_item;
held_valid = drv_valid;
// the driver consumes on done
if (drv_valid && drv_done) begin
received[n_recv] = drv_item;
n_recv = n_recv + 1;
end
end
integer checks = 0, failures = 0;
task check;
input cond;
input [8*80-1:0] name;
begin
checks = checks + 1;
if (cond) $display(" PASS %0s", name);
else begin failures = failures + 1; $display(" FAIL %0s", name); end
end
endtask
integer i, j, base_recv, min_s, max_s;
// A driver that takes `lat` cycles to process each item.
task drive_for;
input integer ncycles;
input integer lat;
integer c, hold;
begin
c = 0; hold = 0;
while (c < ncycles) begin
@(negedge clk);
drv_done = 1'b0;
if (drv_valid) begin
hold = hold + 1;
if (hold >= lat) begin drv_done = 1'b1; hold = 0; end
end
c = c + 1;
end
@(negedge clk) drv_done = 1'b0;
end
endtask
initial begin
#20_000_000;
$display(" FAIL watchdog: simulation did not finish");
$display("== %0d checks, %0d failures ==", checks+1, failures+1);
$display(" RESULT: VERILOG SEQ-ARB TESTS FAILED (timeout)");
$finish;
end
initial begin
$display("== uart_seq_arb_v : self-checking Verilog testbench ==");
for (i = 0; i < NSEQ; i = i + 1) served[i] = 0;
held_valid = 1'b0; gnt_prev = {NSEQ{1'b0}};
rst_n = 1'b0;
repeat (4) @(negedge clk);
check(gnt === {NSEQ{1'b0}} && drv_valid === 1'b0,
"reset: no grant and nothing in front of the driver");
rst_n = 1'b1;
repeat (2) @(negedge clk);
//=== one sequence, one item =========================================
for (i = 0; i < NSEQ; i = i + 1) item[i*W +: W] = 9'h100 + i[8:0];
@(negedge clk) req[0] = 1'b1;
repeat (3) @(negedge clk);
check(drv_valid === 1'b1, "a lone request reaches the driver");
check(drv_item == 9'h100, "carrying that sequence's item");
check(n_granted == 1, "and is granted exactly once");
//=== THE blocking property ==========================================
// Nothing moves while the driver has not said item_done. This is the
// half that makes it a sequencer rather than a mux.
@(negedge clk) req = 4'b1111; // everyone else asks too
repeat (20) @(negedge clk);
check(n_granted == 1,
"20 cycles with every sequence asking: still ONE grant");
check(drv_item == 9'h100, "and the held item never changed");
check(held_bad == 0, "the item in front of the driver is stable");
//=== release, and the next one is served ============================
@(negedge clk) drv_done = 1'b1;
@(negedge clk) drv_done = 1'b0;
repeat (3) @(negedge clk);
check(n_granted == 2, "item_done releases the sequencer");
check(last_seq != 4'd0, "and the next grant goes to a DIFFERENT sequence");
//=== fairness under full contention =================================
req = 4'b1111;
for (i = 0; i < NSEQ; i = i + 1) served[i] = 0;
drive_for(2000, 3);
min_s = served[0]; max_s = served[0];
for (i = 1; i < NSEQ; i = i + 1) begin
if (served[i] < min_s) min_s = served[i];
if (served[i] > max_s) max_s = served[i];
end
$display(" [info] served: %0d %0d %0d %0d",
served[0], served[1], served[2], served[3]);
check(min_s > 0, "under full contention EVERY sequence is served");
check(max_s - min_s <= 1,
"and they are served equally -- round robin, not priority");
//=== the driver received exactly what was granted ===================
@(negedge clk) req = 4'b0000;
repeat (20) @(negedge clk);
drive_for(40, 1);
check(n_recv > 400, "the driver consumed a few hundred items");
check(n_recv == n_offer || n_recv == n_offer - 1,
"items received equals items granted (allowing one in flight)");
mism = 0;
for (i = 0; i < n_recv; i = i + 1)
if (received[i] !== offered[i]) mism = mism + 1;
check(mism == 0, "and every one carried the granted sequence's payload");
//=== grant hygiene ==================================================
check(multi_gnt == 0, "never more than one sequence granted at a time");
check(wide_gnt == 0, "no grant pulse was ever wider than one cycle");
//=== a slow driver does not lose items ==============================
base_recv = n_recv;
@(negedge clk) req = 4'b0101;
drive_for(600, 12); // twelve cycles per item
check(n_recv - base_recv > 20, "a slow driver still makes progress");
mism = 0;
for (i = base_recv; i < n_recv; i = i + 1)
if (received[i] !== offered[i]) mism = mism + 1;
check(mism == 0, "and loses nothing while it is slow");
//=== withdrawing a request ==========================================
@(negedge clk) req = 4'b0000;
repeat (4) @(negedge clk);
drive_for(20, 1);
base_recv = n_granted;
repeat (20) @(negedge clk);
check(n_granted == base_recv,
"with no sequence asking, nothing is granted");
check(drv_valid === 1'b0, "and the driver is left idle");
$display("== %0d checks, %0d failures ==", checks, failures);
if (failures == 0) $display(" RESULT: ALL VERILOG SEQ-ARB TESTS PASSED");
else $display(" RESULT: VERILOG SEQ-ARB TESTS FAILED");
$finish;
end
endmodule//===========================================================================
// tb_uart_seq_arb — self-checking SystemVerilog testbench
//
// The sequencer has two contracts and they fail in different ways:
//
// THE HANDSHAKE. An item placed in front of the driver stays there,
// unchanged, until the driver says item_done. A
// sequencer that moves on early loses transactions and
// the loss is silent -- the driver simply never saw them.
//
// THE ARBITRATION. Every requesting sequence eventually gets served. A
// fixed-priority arbiter satisfies every other check in
// this file and starves the last sequence forever.
//
// INDEPENDENCE: the testbench records what each sequence OFFERED and what
// the driver RECEIVED, and compares the two lists. It does not read the
// arbiter's pointer to predict who should win -- that would be re-deriving
// the answer from the state that produced it.
//===========================================================================
`timescale 1ns/1ps
module tb_uart_seq_arb;
localparam NSEQ = 4;
localparam W = 9;
logic clk = 1'b0;
always #5 clk = ~clk;
logic rst_n = 1'b0;
logic [NSEQ-1:0] req = {NSEQ{1'b0}};
logic [NSEQ*W-1:0] item = {(NSEQ*W){1'b0}};
wire [NSEQ-1:0] gnt;
wire [W-1:0] drv_item;
wire drv_valid;
logic drv_done = 1'b0;
wire [31:0] n_granted;
wire [3:0] last_seq;
uart_seq_arb #(.NSEQ(NSEQ), .W(W)) dut (
.clk(clk), .rst_n(rst_n),
.req_i(req), .item_i(item), .gnt_o(gnt),
.drv_item_o(drv_item), .drv_valid_o(drv_valid), .drv_done_i(drv_done),
.n_granted_o(n_granted), .last_seq_o(last_seq));
//---- what each sequence offered, and what the driver received --------
logic [W-1:0] offered [0:1023];
logic [W-1:0] received[0:1023];
int n_offer = 0, n_recv = 0;
int served [0:NSEQ-1]; // fairness tally
int mism = 0, held_bad = 0, wide_gnt = 0, multi_gnt = 0;
logic [W-1:0] held_item;
logic held_valid;
logic [NSEQ-1:0] gnt_prev;
int g;
always @(posedge clk) if (rst_n) begin
// exactly one grant at a time, and never wider than one cycle
if (gnt != 0) begin
if ((gnt & (gnt - 1)) != 0) multi_gnt++;
if (gnt_prev != 0) wide_gnt = wide_gnt + 1;
for (g = 0; g < NSEQ; g = g + 1)
if (gnt[g]) begin
served[g] = served[g] + 1;
offered[n_offer] = item[g*W +: W];
n_offer++;
end
end
gnt_prev = gnt;
// the item in front of the driver must not change while it is held
if (held_valid && drv_valid && (drv_item !== held_item))
held_bad++;
held_item = drv_item;
held_valid = drv_valid;
// the driver consumes on done
if (drv_valid && drv_done) begin
received[n_recv] = drv_item;
n_recv++;
end
end
int checks = 0, failures = 0;
task automatic check(input logic cond, input string name);
checks++;
if (cond) $display(" PASS %0s", name);
else begin failures++; $display(" FAIL %0s", name); end
endtask
int i, j, base_recv, min_s, max_s;
// A driver that takes `lat` cycles to process each item.
task drive_for;
input int ncycles;
input int lat;
int c, hold;
begin
c = 0; hold = 0;
while (c < ncycles) begin
@(negedge clk);
drv_done = 1'b0;
if (drv_valid) begin
hold++;
if (hold >= lat) begin drv_done = 1'b1; hold = 0; end
end
c++;
end
@(negedge clk) drv_done = 1'b0;
end
endtask
initial begin
#20_000_000;
$display(" FAIL watchdog: simulation did not finish");
$display("== %0d checks, %0d failures ==", checks+1, failures+1);
$display(" RESULT: SYSTEMVERILOG SEQ-ARB TESTS FAILED (timeout)");
$finish;
end
initial begin
$display("== uart_seq_arb : self-checking Verilog testbench ==");
for (i = 0; i < NSEQ; i = i + 1) served[i] = 0;
held_valid = 1'b0; gnt_prev = {NSEQ{1'b0}};
rst_n = 1'b0;
repeat (4) @(negedge clk);
check(gnt === {NSEQ{1'b0}} && drv_valid === 1'b0,
"reset: no grant and nothing in front of the driver");
rst_n = 1'b1;
repeat (2) @(negedge clk);
//=== one sequence, one item =========================================
for (i = 0; i < NSEQ; i = i + 1) item[i*W +: W] = 9'h100 + i[8:0];
@(negedge clk) req[0] = 1'b1;
repeat (3) @(negedge clk);
check(drv_valid === 1'b1, "a lone request reaches the driver");
check(drv_item == 9'h100, "carrying that sequence's item");
check(n_granted == 1, "and is granted exactly once");
//=== THE blocking property ==========================================
// Nothing moves while the driver has not said item_done. This is the
// half that makes it a sequencer rather than a mux.
@(negedge clk) req = 4'b1111; // everyone else asks too
repeat (20) @(negedge clk);
check(n_granted == 1,
"20 cycles with every sequence asking: still ONE grant");
check(drv_item == 9'h100, "and the held item never changed");
check(held_bad == 0, "the item in front of the driver is stable");
//=== release, and the next one is served ============================
@(negedge clk) drv_done = 1'b1;
@(negedge clk) drv_done = 1'b0;
repeat (3) @(negedge clk);
check(n_granted == 2, "item_done releases the sequencer");
check(last_seq != 4'd0, "and the next grant goes to a DIFFERENT sequence");
//=== fairness under full contention =================================
req = 4'b1111;
for (i = 0; i < NSEQ; i = i + 1) served[i] = 0;
drive_for(2000, 3);
min_s = served[0]; max_s = served[0];
for (i = 1; i < NSEQ; i = i + 1) begin
if (served[i] < min_s) min_s = served[i];
if (served[i] > max_s) max_s = served[i];
end
$display(" [info] served: %0d %0d %0d %0d",
served[0], served[1], served[2], served[3]);
check(min_s > 0, "under full contention EVERY sequence is served");
check(max_s - min_s <= 1,
"and they are served equally -- round robin, not priority");
//=== the driver received exactly what was granted ===================
@(negedge clk) req = 4'b0000;
repeat (20) @(negedge clk);
drive_for(40, 1);
check(n_recv > 400, "the driver consumed a few hundred items");
check(n_recv == n_offer || n_recv == n_offer - 1,
"items received equals items granted (allowing one in flight)");
mism = 0;
for (i = 0; i < n_recv; i = i + 1)
if (received[i] !== offered[i]) mism++;
check(mism == 0, "and every one carried the granted sequence's payload");
//=== grant hygiene ==================================================
check(multi_gnt == 0, "never more than one sequence granted at a time");
check(wide_gnt == 0, "no grant pulse was ever wider than one cycle");
//=== a slow driver does not lose items ==============================
base_recv = n_recv;
@(negedge clk) req = 4'b0101;
drive_for(600, 12); // twelve cycles per item
check(n_recv - base_recv > 20, "a slow driver still makes progress");
mism = 0;
for (i = base_recv; i < n_recv; i = i + 1)
if (received[i] !== offered[i]) mism++;
check(mism == 0, "and loses nothing while it is slow");
//=== withdrawing a request ==========================================
@(negedge clk) req = 4'b0000;
repeat (4) @(negedge clk);
drive_for(20, 1);
base_recv = n_granted;
repeat (20) @(negedge clk);
check(n_granted == base_recv,
"with no sequence asking, nothing is granted");
check(drv_valid === 1'b0, "and the driver is left idle");
$display("== %0d checks, %0d failures ==", checks, failures);
if (failures == 0) $display(" RESULT: ALL SYSTEMVERILOG SEQ-ARB TESTS PASSED");
else $display(" RESULT: SYSTEMVERILOG SEQ-ARB TESTS FAILED");
$finish;
end
endmodule--===========================================================================
-- tb_uart_seq_arb — self-checking VHDL-2008 testbench
--
-- The sequencer has two contracts and they fail in different ways:
--
-- THE HANDSHAKE. An item placed in front of the driver stays there,
-- unchanged, until the driver says item_done. A
-- sequencer that moves on early loses transactions, and
-- the loss is silent -- the driver simply never saw them.
--
-- THE ARBITRATION. Every requesting sequence eventually gets served. A
-- fixed-priority arbiter satisfies every other check in
-- this file and starves the last sequence forever.
--
-- INDEPENDENCE: the testbench records what each sequence OFFERED and what
-- the driver RECEIVED, and compares the two lists. It does not read the
-- arbiter's pointer to predict who should win.
--
-- Same 20 counted checks as the Verilog and SystemVerilog twins.
--===========================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity tb_uart_seq_arb is
end entity tb_uart_seq_arb;
architecture sim of tb_uart_seq_arb is
constant NSEQ : positive := 4;
constant W : positive := 9;
constant TCLK : time := 10 ns;
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal done : boolean := false;
signal req : std_logic_vector(NSEQ-1 downto 0) := (others => '0');
signal item : std_logic_vector(NSEQ*W-1 downto 0) := (others => '0');
signal gnt : std_logic_vector(NSEQ-1 downto 0);
signal drv_item : std_logic_vector(W-1 downto 0);
signal drv_valid : std_logic;
signal drv_done : std_logic := '0';
signal n_granted : natural;
signal last_seq : natural;
type log_arr is array (0 to 1023) of std_logic_vector(W-1 downto 0);
type tally is array (0 to NSEQ-1) of natural;
signal offered, received : log_arr := (others => (others => '0'));
signal n_offer, n_recv : natural := 0;
signal served : tally := (others => 0);
signal held_bad, wide_gnt, multi_gnt : natural := 0;
begin
clk <= not clk after TCLK/2 when not done else '0';
dut : entity work.uart_seq_arb
generic map (NSEQ => NSEQ, W => W)
port map (clk => clk, rst_n => rst_n,
req_i => req, item_i => item, gnt_o => gnt,
drv_item_o => drv_item, drv_valid_o => drv_valid,
drv_done_i => drv_done,
n_granted_o => n_granted, last_seq_o => last_seq);
obs : process (clk)
variable held_item : std_logic_vector(W-1 downto 0) := (others => '0');
variable held_valid : std_logic := '0';
variable gnt_prev : std_logic_vector(NSEQ-1 downto 0) := (others => '0');
variable ones : natural;
begin
if rising_edge(clk) and rst_n = '1' then
ones := 0;
for g in 0 to NSEQ-1 loop
if gnt(g) = '1' then ones := ones + 1; end if;
end loop;
if ones > 1 then multi_gnt <= multi_gnt + 1; end if;
if ones > 0 and gnt_prev /= (gnt_prev'range => '0') then
wide_gnt <= wide_gnt + 1;
end if;
for g in 0 to NSEQ-1 loop
if gnt(g) = '1' then
served(g) <= served(g) + 1;
offered(n_offer) <= item((g+1)*W-1 downto g*W);
n_offer <= n_offer + 1;
end if;
end loop;
gnt_prev := gnt;
-- the item in front of the driver must not change while held
if held_valid = '1' and drv_valid = '1' and drv_item /= held_item then
held_bad <= held_bad + 1;
end if;
held_item := drv_item;
held_valid := drv_valid;
if drv_valid = '1' and drv_done = '1' then
received(n_recv) <= drv_item;
n_recv <= n_recv + 1;
end if;
end if;
end process obs;
watchdog : process
begin
wait for 40 ms;
report "watchdog: simulation did not finish" severity failure;
end process watchdog;
stim : process
variable checks, failures : natural := 0;
variable base_recv, min_s, max_s, mism : natural;
-- `served` is driven by the observer process, so the stimulus cannot
-- zero it: a second driver on an unresolved type is illegal in VHDL.
-- A baseline is better practice in every language anyway -- it asks
-- "how many were served DURING the fairness test", which is the
-- actual question.
variable base_served : tally;
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;
-- A driver that takes `lat` cycles to process each item.
procedure drive_for(ncycles : natural; lat : natural) is
variable hold : natural := 0;
begin
for c in 1 to ncycles loop
wait until falling_edge(clk);
drv_done <= '0';
if drv_valid = '1' then
hold := hold + 1;
if hold >= lat then drv_done <= '1'; hold := 0; end if;
end if;
end loop;
wait until falling_edge(clk); drv_done <= '0';
end procedure drive_for;
begin
report "== uart_seq_arb : self-checking VHDL testbench ==" severity note;
rst_n <= '0';
for i in 1 to 4 loop wait until falling_edge(clk); end loop;
check(gnt = (gnt'range => '0') and drv_valid = '0',
"reset: no grant and nothing in front of the driver");
rst_n <= '1';
for i in 1 to 2 loop wait until falling_edge(clk); end loop;
--=== one sequence, one item =========================================
for i in 0 to NSEQ-1 loop
item((i+1)*W-1 downto i*W) <= std_logic_vector(to_unsigned(16#100# + i, W));
end loop;
wait until falling_edge(clk); req(0) <= '1';
for i in 1 to 3 loop wait until falling_edge(clk); end loop;
check(drv_valid = '1', "a lone request reaches the driver");
check(drv_item = std_logic_vector(to_unsigned(16#100#, W)),
"carrying that sequence's item");
check(n_granted = 1, "and is granted exactly once");
--=== THE blocking property ==========================================
wait until falling_edge(clk); req <= "1111";
for i in 1 to 20 loop wait until falling_edge(clk); end loop;
check(n_granted = 1,
"20 cycles with every sequence asking: still ONE grant");
check(drv_item = std_logic_vector(to_unsigned(16#100#, W)),
"and the held item never changed");
check(held_bad = 0, "the item in front of the driver is stable");
--=== release, and the next one is served ============================
wait until falling_edge(clk); drv_done <= '1';
wait until falling_edge(clk); drv_done <= '0';
for i in 1 to 3 loop wait until falling_edge(clk); end loop;
check(n_granted = 2, "item_done releases the sequencer");
check(last_seq /= 0, "and the next grant goes to a DIFFERENT sequence");
--=== fairness under full contention =================================
req <= "1111";
wait until falling_edge(clk);
base_served := served;
drive_for(2000, 3);
min_s := served(0) - base_served(0);
max_s := min_s;
for i in 1 to NSEQ-1 loop
if served(i) - base_served(i) < min_s then
min_s := served(i) - base_served(i);
end if;
if served(i) - base_served(i) > max_s then
max_s := served(i) - base_served(i);
end if;
end loop;
report " [info] served: "
& integer'image(served(0) - base_served(0)) & " "
& integer'image(served(1) - base_served(1)) & " "
& integer'image(served(2) - base_served(2)) & " "
& integer'image(served(3) - base_served(3)) severity note;
check(min_s > 0, "under full contention EVERY sequence is served");
check(max_s - min_s <= 1,
"and they are served equally -- round robin, not priority");
--=== the driver received exactly what was granted ===================
wait until falling_edge(clk); req <= "0000";
for i in 1 to 20 loop wait until falling_edge(clk); end loop;
drive_for(40, 1);
check(n_recv > 400, "the driver consumed a few hundred items");
check(n_recv = n_offer or n_recv = n_offer - 1,
"items received equals items granted (allowing one in flight)");
mism := 0;
for i in 0 to n_recv-1 loop
if received(i) /= offered(i) then mism := mism + 1; end if;
end loop;
check(mism = 0, "and every one carried the granted sequence's payload");
--=== grant hygiene ==================================================
check(multi_gnt = 0, "never more than one sequence granted at a time");
check(wide_gnt = 0, "no grant pulse was ever wider than one cycle");
--=== a slow driver does not lose items ==============================
base_recv := n_recv;
wait until falling_edge(clk); req <= "0101";
drive_for(600, 12);
check(n_recv - base_recv > 20, "a slow driver still makes progress");
mism := 0;
for i in base_recv to n_recv-1 loop
if received(i) /= offered(i) then mism := mism + 1; end if;
end loop;
check(mism = 0, "and loses nothing while it is slow");
--=== withdrawing a request ==========================================
wait until falling_edge(clk); req <= "0000";
for i in 1 to 4 loop wait until falling_edge(clk); end loop;
drive_for(20, 1);
base_recv := n_granted;
for i in 1 to 20 loop wait until falling_edge(clk); end loop;
check(n_granted = base_recv,
"with no sequence asking, nothing is granted");
check(drv_valid = '0', "and the driver is left idle");
report "== " & integer'image(checks) & " checks, "
& integer'image(failures) & " failures ==" severity note;
if failures = 0 then
report " RESULT: ALL VHDL SEQ-ARB TESTS PASSED" severity note;
else
report " RESULT: VHDL SEQ-ARB TESTS FAILED" severity error;
end if;
done <= true;
wait;
end process stim;
end architecture sim;Twenty checks, identical across all three languages — and the fairness tally comes out the same in each:
PASS a lone request reaches the driver
PASS 20 cycles with every sequence asking: still ONE grant
PASS and the held item never changed
PASS the item in front of the driver is stable
PASS item_done releases the sequencer
PASS and the next grant goes to a DIFFERENT sequence
[info] served: 125 124 125 125
PASS under full contention EVERY sequence is served
PASS and they are served equally -- round robin, not priority
PASS items received equals items granted (allowing one in flight)
PASS and every one carried the granted sequence's payload
PASS never more than one sequence granted at a time
PASS a slow driver still makes progress
PASS and loses nothing while it is slow
== 20 checks, 0 failures ==
Verilog-2001 : 20 / 0 served 125 124 125 125
SystemVerilog : 20 / 0 served 125 124 125 125
VHDL-2008 : 20 / 0 served 125 124 125 1255. Mutation
Four defects installed in the sequencer, each verified to have changed the source before scoring:
| # | Defect installed | Result |
|---|---|---|
| M1 | fixed priority — the round-robin pointer never moves | killed, 3 |
| M2 | the item released without waiting for item_done | killed, 8 |
| M3 | the grant held instead of pulsed for one cycle | killed, 6 |
| M4 | the pointer set to pick rather than pick + 1 | killed, 3 |
M1 and M4 are worth comparing. Both break fairness; neither breaks anything else. M1 starves sequences 1 through 3 completely; M4 serves the same sequence twice in a row before moving on, which is a much milder unfairness — and both are caught by the same check, max_s - min_s <= 1, because that check asserts equality rather than merely "everyone got something".
A weaker check — min_s > 0, that nobody starved — catches M1 and lets M4 through. The suite has both, and the stronger one is the one that earns its place.
6. Verification
Test that nothing happens while the driver is busy. It is the only check that distinguishes a sequencer from a multiplexer, and it looks like a test of nothing.
Assert equality of service, not merely absence of starvation. min > 0 catches a fixed-priority arbiter and misses a pointer that advances by the wrong amount.
Compare what was offered against what was received, item by item. The sequencer's payload path is a place where a byte can be silently substituted, and only a list comparison sees it.
Check the grant is one cycle wide. A held grant means a sequence believes it has been served repeatedly, and it will send items nobody consumes.
Vary the driver's latency. A sequencer that works with a one-cycle driver and loses items with a twelve-cycle one has a handshake bug that a fast driver hides.
And use absolute time in the driver, never a clocking block. A driver synchronised to the DUT's clock cannot be wrong on purpose, and cannot test tolerance.
7. Debugging
8. Understanding Check
9. Summary
A UART driver has no clock to synchronise to — it is standing in for the far end's oscillator, so its delays are absolute times and a clocking block would make tolerance testing impossible.
A sequencer is an arbiter with a blocking handshake, and it has exactly two contracts: it holds, and it is fair.
The holding contract is the one that distinguishes it from a multiplexer, and the check that tests it looks like a test of nothing.
Fairness must be asserted as equality, not as the absence of starvation — a pointer that advances by the wrong amount passes the weaker form.
Twenty checks in each of three languages, 0 failures, with the same 125/124/125/125 tally in all three.
Four mutants, four killed, the largest blast radius belonging to the released-early handshake at eight failing checks.
10. What Comes Next
Chapter 16.4 does the same for the monitor: the analysis port that lets it broadcast without knowing who is listening, built as running code — and a measurement of the property that makes it dangerous.
Browse the full path on the UART tutorials index. For the transaction this driver consumes, read back to Chapter 16.2.
Continue learning
Related tutorials
- Related topic
Baud Enable and TX Bit Timing
What advances the transmitter from one bit interval to the next, stated precisely — the tick's semantics, the measured cost of launching on a free-running enable, and frame durations derived and then confirmed in simulation.
- Related topic
UART as a Memory-Mapped Peripheral
The register block between a bus and the UART core, and the read and write side effects that make UART registers unlike memory — with a measured demonstration of getting one of them wrong.
- Related topic
Control, Configuration, Data and Status Registers
A complete, simulated UART register map — and the reasoning behind each placement, including a parameter that turned out to belong in a register and the elaboration check that became a runtime clamp.
- Related topic
Interrupts: Sources, Enables and Clear Semantics
Which conditions deserve to be interrupt sources, why a receive-idle timeout is not optional, and the clear-semantics decision a driver lives with for the life of the chip — with a real defect found and fixed.
Where this fits
Part of the UART curriculum.
