UART · Module 16
Monitor, Agent and Active/Passive Reuse
Recovering frames independently of the driver, the analysis port that broadcasts without coupling — built as running code with its dangerous property measured — and the flag that makes one agent serve two testbenches.
The monitor is the component this curriculum has already been most careful about. Chapter 14.2 §3 gave it one input port and made it bring its own bit period; Chapter 14.4 measured what happens when it is told the wrong one. None of that changes here.
What changes is how it publishes what it saw — and that mechanism has a property most people do not know about until it costs them a debugging week.
1. The Monitor, Unchanged
class uart_monitor extends uvm_monitor;
`uvm_component_utils(uart_monitor)
virtual uart_line_if vif;
uart_cfg m_cfg;
// The port. Note the type: it is parameterised on the TRANSACTION, not
// on who receives it -- the monitor has no idea who is listening.
uvm_analysis_port #(uart_frame_item) ap;
function new(string name, uvm_component parent);
super.new(name, parent);
ap = new("ap", this);
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 monitor")
if (!uvm_config_db#(uart_cfg)::get(this, "", "cfg", m_cfg))
`uvm_fatal("NOCFG", "no uart_cfg for the UART monitor")
endfunction
task run_phase(uvm_phase phase);
forever begin
uart_frame_item t;
recover_frame(t); // Chapter 14.4's algorithm, unchanged:
// find a start edge, wait 1.5 of ITS OWN
// bit periods, sample at one-bit intervals
ap.write(t); // and publish. This call cannot fail.
end
endtask
endclass2. The Analysis Port Cannot Be Refused
This is the property worth knowing:
uvm_analysis_port::write()is a void function. No return value, no ready signal, no backpressure, no failure mode.
Compare it with uvm_tlm_fifo::put(), which blocks, or try_put(), which can refuse. The analysis port can do neither. That absence is the entire design, and it is what buys:
- a monitor that broadcasts without knowing who is listening, or whether anyone is;
- a guarantee that connecting a second subscriber cannot change the first one's timing;
- and therefore an agent that can be reused in an environment with a completely different set of consumers.
And it costs exactly one thing, which §3 measures.
3. An Analysis Port That Runs
//===========================================================================
// uart_analysis_bus_v — the ANALYSIS PORT, in Verilog-2001
//
// NOT SYNTHESIZABLE. A verification component.
//
// THE ONE PROPERTY THAT DEFINES AN ANALYSIS PORT: it cannot be refused.
//
// `uvm_analysis_port::write()` is a void function. It has no return value,
// no ready signal, no backpressure and no failure mode. A monitor calls it
// and the call returns, whatever the subscribers are doing. Compare that
// with `uvm_tlm_fifo::put()`, which blocks, or `try_put()`, which can
// refuse.
//
// Notice what is NOT in this port list: there is no `ready` input from any
// subscriber, and there is no way to add one. That absence is the entire
// design. It is why:
//
// - a monitor can broadcast without knowing who is listening, or whether
// anyone is;
// - connecting a second subscriber cannot change the first one's timing;
// - and a subscriber that does slow work inside write() does not stall
// the monitor -- it stalls the SIMULATION, or silently misses traffic,
// depending on how it was written.
//
// The last point is the one that bites. An analysis port never tells you
// that a subscriber could not keep up, because there is no channel on
// which it could.
//
// THE BROADCAST IS SIMULTANEOUS. Every subscriber sees the same item on
// the same cycle. UVM makes no ordering guarantee between subscribers, and
// a testbench that depends on one seeing it first is relying on the order
// the connections happened to be made in.
//===========================================================================
`timescale 1ns/1ps
module uart_analysis_bus_v #(
parameter NSUB = 3, // how many subscribers are connected
parameter W = 9
) (
input wire clk,
input wire rst_n,
// ---- the producer: a monitor calling write() ------------------------
input wire write_i,
input wire [W-1:0] item_i,
// NOTE: there is deliberately no `ready` output here.
// ---- the subscribers ------------------------------------------------
output reg [NSUB-1:0] sub_valid_o,
output reg [NSUB*W-1:0] sub_item_o,
// ---- observation -----------------------------------------------------
output reg [31:0] n_written_o
);
integer s;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sub_valid_o <= {NSUB{1'b0}};
sub_item_o <= {(NSUB*W){1'b0}};
n_written_o <= 32'd0;
end else begin
sub_valid_o <= {NSUB{1'b0}}; // one cycle per write
if (write_i) begin
for (s = 0; s < NSUB; s = s + 1) begin
sub_valid_o[s] <= 1'b1;
sub_item_o[s*W +: W] <= item_i;
end
n_written_o <= n_written_o + 1;
end
end
end
endmodule//===========================================================================
// uart_analysis_bus — the ANALYSIS PORT, in SystemVerilog
//
// NOT SYNTHESIZABLE. A verification component.
//
// THE ONE PROPERTY THAT DEFINES AN ANALYSIS PORT: it cannot be refused.
//
// `uvm_analysis_port::write()` is a void function. It has no return value,
// no ready signal, no backpressure and no failure mode. A monitor calls it
// and the call returns, whatever the subscribers are doing. Compare that
// with `uvm_tlm_fifo::put()`, which blocks, or `try_put()`, which can
// refuse.
//
// Notice what is NOT in this port list: there is no `ready` input from any
// subscriber, and there is no way to add one. That absence is the entire
// design. It is why:
//
// - a monitor can broadcast without knowing who is listening, or whether
// anyone is;
// - connecting a second subscriber cannot change the first one's timing;
// - and a subscriber that does slow work inside write() does not stall
// the monitor -- it stalls the SIMULATION, or silently misses traffic,
// depending on how it was written.
//
// The last point is the one that bites. An analysis port never tells you
// that a subscriber could not keep up, because there is no channel on
// which it could.
//
// THE BROADCAST IS SIMULTANEOUS. Every subscriber sees the same item on
// the same cycle. UVM makes no ordering guarantee between subscribers, and
// a testbench that depends on one seeing it first is relying on the order
// the connections happened to be made in.
//===========================================================================
`timescale 1ns/1ps
module uart_analysis_bus #(
parameter NSUB = 3, // how many subscribers are connected
parameter W = 9
) (
input wire clk,
input wire rst_n,
// ---- the producer: a monitor calling write() ------------------------
input wire write_i,
input wire [W-1:0] item_i,
// NOTE: there is deliberately no `ready` output here.
// ---- the subscribers ------------------------------------------------
output logic [NSUB-1:0] sub_valid_o,
output logic [NSUB*W-1:0] sub_item_o,
// ---- observation -----------------------------------------------------
output logic [31:0] n_written_o
);
int s;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
sub_valid_o <= {NSUB{1'b0}};
sub_item_o <= {(NSUB*W){1'b0}};
n_written_o <= 32'd0;
end else begin
sub_valid_o <= {NSUB{1'b0}}; // one cycle per write
if (write_i) begin
for (s = 0; s < NSUB; s = s + 1) begin
sub_valid_o[s] <= 1'b1;
sub_item_o[s*W +: W] <= item_i;
end
n_written_o <= n_written_o + 1;
end
end
end
endmodule--===========================================================================
-- uart_analysis_bus — the ANALYSIS PORT, in VHDL-2008
--
-- NOT SYNTHESIZABLE. A verification component.
--
-- THE ONE PROPERTY THAT DEFINES AN ANALYSIS PORT: it cannot be refused.
--
-- uvm_analysis_port::write() is a void function. No return value, no ready
-- signal, no backpressure, no failure mode. A monitor calls it and the call
-- returns, whatever the subscribers are doing. Compare uvm_tlm_fifo::put(),
-- which blocks, or try_put(), which can refuse.
--
-- Notice what is NOT in this port list: there is no `ready` input from any
-- subscriber, and no way to add one. That absence is the entire design.
-- It is why a monitor can broadcast without knowing who is listening, why
-- connecting a second subscriber cannot change the first one's timing, and
-- why a subscriber that does slow work inside write() does not stall the
-- monitor -- it silently misses traffic instead.
--
-- An analysis port never tells you a subscriber could not keep up, because
-- there is no channel on which it could.
--===========================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity uart_analysis_bus is
generic (
NSUB : positive := 3; -- how many subscribers are connected
W : positive := 9
);
port (
clk : in std_logic;
rst_n : in std_logic;
-- the producer: a monitor calling write()
write_i : in std_logic;
item_i : in std_logic_vector(W-1 downto 0);
-- NOTE: there is deliberately no `ready` output here.
-- the subscribers
sub_valid_o : out std_logic_vector(NSUB-1 downto 0);
sub_item_o : out std_logic_vector(NSUB*W-1 downto 0);
n_written_o : out natural
);
end entity uart_analysis_bus;
architecture model of uart_analysis_bus is
signal written : natural := 0;
begin
bcast : process (clk, rst_n)
begin
if rst_n = '0' then
sub_valid_o <= (others => '0');
sub_item_o <= (others => '0');
written <= 0;
elsif rising_edge(clk) then
sub_valid_o <= (others => '0'); -- one cycle per write
if write_i = '1' then
for s in 0 to NSUB-1 loop
sub_valid_o(s) <= '1';
sub_item_o((s+1)*W-1 downto s*W) <= item_i;
end loop;
written <= written + 1;
end if;
end if;
end process bcast;
n_written_o <= written;
end architecture model;The suite that measures it
//===========================================================================
// tb_uart_analysis_bus_v — self-checking Verilog-2001 testbench
//
// An analysis port is defined by what it CANNOT do, so most of this suite
// is about absences:
//
// - every subscriber sees every item, on the same cycle;
// - connecting more subscribers changes nothing for the existing ones;
// - and there is no way for a subscriber to refuse or delay a write.
//
// The last one is demonstrated rather than asserted: a subscriber that
// cannot keep up is modelled, and the suite measures what it MISSES. That
// loss is invisible to the producer by construction, which is the single
// most important thing to understand about analysis ports.
//===========================================================================
`timescale 1ns/1ps
module tb_uart_analysis_bus_v;
localparam NSUB = 3;
localparam W = 9;
reg clk = 1'b0;
always #5 clk = ~clk;
reg rst_n = 1'b0;
reg write = 1'b0;
reg [W-1:0] item = {W{1'b0}};
wire [NSUB-1:0] sub_valid;
wire [NSUB*W-1:0] sub_item;
wire [31:0] n_written;
uart_analysis_bus_v #(.NSUB(NSUB), .W(W)) dut (
.clk(clk), .rst_n(rst_n),
.write_i(write), .item_i(item),
.sub_valid_o(sub_valid), .sub_item_o(sub_item),
.n_written_o(n_written));
//---- three subscribers, modelled with different behaviour ------------
// sub 0 : keeps up perfectly
// sub 1 : keeps up perfectly (to prove 0 and 1 agree exactly)
// sub 2 : SLOW -- busy for BUSY_CYC cycles after each item it accepts,
// and therefore misses anything that arrives while it is busy
localparam BUSY_CYC = 4;
integer seen [0:NSUB-1];
integer missed2 = 0;
integer busy2 = 0;
integer disagree = 0;
reg [W-1:0] last [0:NSUB-1];
integer s;
always @(posedge clk) if (rst_n) begin
// subscribers 0 and 1 take everything
for (s = 0; s < 2; s = s + 1)
if (sub_valid[s]) begin
seen[s] = seen[s] + 1;
last[s] = sub_item[s*W +: W];
end
// subscriber 2 is busy some of the time
if (busy2 > 0) busy2 = busy2 - 1;
if (sub_valid[2]) begin
if (busy2 == 0) begin
seen[2] = seen[2] + 1;
last[2] = sub_item[2*W +: W];
busy2 = BUSY_CYC;
end else begin
// The write still happened. Nothing told the producer.
missed2 = missed2 + 1;
end
end
// whenever two subscribers are both delivered to, the item must match
if (sub_valid[0] && sub_valid[1] &&
(sub_item[0*W +: W] !== sub_item[1*W +: W])) disagree = disagree + 1;
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, base0;
task post;
input [W-1:0] d;
begin
@(negedge clk) write = 1'b1; item = d;
@(negedge clk) write = 1'b0;
end
endtask
initial begin
#10_000_000;
$display(" FAIL watchdog: simulation did not finish");
$display("== %0d checks, %0d failures ==", checks+1, failures+1);
$display(" RESULT: VERILOG ANALYSIS-BUS TESTS FAILED (timeout)");
$finish;
end
initial begin
$display("== uart_analysis_bus_v : self-checking Verilog testbench ==");
for (i = 0; i < NSUB; i = i + 1) begin seen[i] = 0; last[i] = 0; end
rst_n = 1'b0;
repeat (4) @(negedge clk);
check(sub_valid === {NSUB{1'b0}}, "reset: no subscriber is being written to");
rst_n = 1'b1;
repeat (2) @(negedge clk);
//=== one write reaches every subscriber, on the same cycle ==========
post(9'h0A5);
repeat (2) @(negedge clk);
check(n_written == 1, "one write is counted once by the producer");
check(seen[0] == 1 && seen[1] == 1 && seen[2] == 1,
"and reaches ALL THREE subscribers");
check(last[0] == 9'h0A5 && last[1] == 9'h0A5 && last[2] == 9'h0A5,
"each of them receiving the same item");
//=== the broadcast is simultaneous ==================================
// sub_valid is a single vector: there is no ordering between
// subscribers to depend on, by construction.
check(disagree == 0,
"subscribers never receive different items on the same write");
//=== a burst, with everyone keeping up ==============================
for (i = 0; i < 20; i = i + 1) begin
post(9'h010 + i[8:0]);
repeat (BUSY_CYC) @(negedge clk); // slow enough for sub 2
end
check(seen[0] == 21 && seen[1] == 21,
"twenty more writes: the fast subscribers saw all of them");
check(seen[2] == 21, "and so did the slow one, given time between writes");
check(missed2 == 0, "nothing was missed while the pace allowed it");
//=== THE property: a slow subscriber cannot push back ===============
// Back-to-back writes, faster than subscriber 2 can accept.
base0 = seen[0];
for (i = 0; i < 60; i = i + 1) post(9'h080 + i[8:0]);
repeat (4) @(negedge clk);
check(seen[0] - base0 == 60,
"60 back-to-back writes all reached the fast subscriber");
check(n_written == 81, "and the producer counted every one of them");
check(missed2 > 0,
"the SLOW subscriber missed some -- and could not say so");
$display(" [info] slow subscriber missed %0d of 60 writes", missed2);
//=== there is no backpressure path at all ============================
// Not a measurement -- a statement about the port list. The producer
// has no input from any subscriber, so nothing a subscriber does can
// change when write_i is accepted.
check(n_written == 81,
"the producer's count is unaffected by any subscriber's state");
check(seen[0] == seen[1],
"and two subscribers with identical behaviour agree exactly");
//=== adding a subscriber changes nothing for the others =============
// Subscriber 2 has been missing items throughout. Subscribers 0 and 1
// are unaffected by that, which is the reuse property.
check(seen[0] > seen[2],
"the slow subscriber is behind the others");
check(seen[0] == seen[1],
"yet the fast ones are still exactly in step with each other");
//=== quiet means quiet ==============================================
base0 = seen[0];
repeat (30) @(negedge clk);
check(seen[0] == base0, "with no write, no subscriber is disturbed");
check(sub_valid === {NSUB{1'b0}}, "and no valid is left asserted");
$display("== %0d checks, %0d failures ==", checks, failures);
if (failures == 0) $display(" RESULT: ALL VERILOG ANALYSIS-BUS TESTS PASSED");
else $display(" RESULT: VERILOG ANALYSIS-BUS TESTS FAILED");
$finish;
end
endmodule//===========================================================================
// tb_uart_analysis_bus — self-checking SystemVerilog testbench
//
// An analysis port is defined by what it CANNOT do, so most of this suite
// is about absences:
//
// - every subscriber sees every item, on the same cycle;
// - connecting more subscribers changes nothing for the existing ones;
// - and there is no way for a subscriber to refuse or delay a write.
//
// The last one is demonstrated rather than asserted: a subscriber that
// cannot keep up is modelled, and the suite measures what it MISSES. That
// loss is invisible to the producer by construction, which is the single
// most important thing to understand about analysis ports.
//===========================================================================
`timescale 1ns/1ps
module tb_uart_analysis_bus;
localparam NSUB = 3;
localparam W = 9;
logic clk = 1'b0;
always #5 clk = ~clk;
logic rst_n = 1'b0;
logic write = 1'b0;
logic [W-1:0] item = {W{1'b0}};
wire [NSUB-1:0] sub_valid;
wire [NSUB*W-1:0] sub_item;
wire [31:0] n_written;
uart_analysis_bus #(.NSUB(NSUB), .W(W)) dut (
.clk(clk), .rst_n(rst_n),
.write_i(write), .item_i(item),
.sub_valid_o(sub_valid), .sub_item_o(sub_item),
.n_written_o(n_written));
//---- three subscribers, modelled with different behaviour ------------
// sub 0 : keeps up perfectly
// sub 1 : keeps up perfectly (to prove 0 and 1 agree exactly)
// sub 2 : SLOW -- busy for BUSY_CYC cycles after each item it accepts,
// and therefore misses anything that arrives while it is busy
localparam BUSY_CYC = 4;
int seen [0:NSUB-1];
int missed2 = 0;
int busy2 = 0;
int disagree = 0;
logic [W-1:0] last [0:NSUB-1];
int s;
always @(posedge clk) if (rst_n) begin
// subscribers 0 and 1 take everything
for (s = 0; s < 2; s = s + 1)
if (sub_valid[s]) begin
seen[s] = seen[s] + 1;
last[s] = sub_item[s*W +: W];
end
// subscriber 2 is busy some of the time
if (busy2 > 0) busy2 = busy2 - 1;
if (sub_valid[2]) begin
if (busy2 == 0) begin
seen[2] = seen[2] + 1;
last[2] = sub_item[2*W +: W];
busy2 = BUSY_CYC;
end else begin
// The write still happened. Nothing told the producer.
missed2++;
end
end
// whenever two subscribers are both delivered to, the item must match
if (sub_valid[0] && sub_valid[1] &&
(sub_item[0*W +: W] !== sub_item[1*W +: W])) disagree++;
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, base0;
task post;
input [W-1:0] d;
begin
@(negedge clk) write = 1'b1; item = d;
@(negedge clk) write = 1'b0;
end
endtask
initial begin
#10_000_000;
$display(" FAIL watchdog: simulation did not finish");
$display("== %0d checks, %0d failures ==", checks+1, failures+1);
$display(" RESULT: SYSTEMVERILOG ANALYSIS-BUS TESTS FAILED (timeout)");
$finish;
end
initial begin
$display("== uart_analysis_bus : self-checking Verilog testbench ==");
for (i = 0; i < NSUB; i = i + 1) begin seen[i] = 0; last[i] = 0; end
rst_n = 1'b0;
repeat (4) @(negedge clk);
check(sub_valid === {NSUB{1'b0}}, "reset: no subscriber is being written to");
rst_n = 1'b1;
repeat (2) @(negedge clk);
//=== one write reaches every subscriber, on the same cycle ==========
post(9'h0A5);
repeat (2) @(negedge clk);
check(n_written == 1, "one write is counted once by the producer");
check(seen[0] == 1 && seen[1] == 1 && seen[2] == 1,
"and reaches ALL THREE subscribers");
check(last[0] == 9'h0A5 && last[1] == 9'h0A5 && last[2] == 9'h0A5,
"each of them receiving the same item");
//=== the broadcast is simultaneous ==================================
// sub_valid is a single vector: there is no ordering between
// subscribers to depend on, by construction.
check(disagree == 0,
"subscribers never receive different items on the same write");
//=== a burst, with everyone keeping up ==============================
for (i = 0; i < 20; i = i + 1) begin
post(9'h010 + i[8:0]);
repeat (BUSY_CYC) @(negedge clk); // slow enough for sub 2
end
check(seen[0] == 21 && seen[1] == 21,
"twenty more writes: the fast subscribers saw all of them");
check(seen[2] == 21, "and so did the slow one, given time between writes");
check(missed2 == 0, "nothing was missed while the pace allowed it");
//=== THE property: a slow subscriber cannot push back ===============
// Back-to-back writes, faster than subscriber 2 can accept.
base0 = seen[0];
for (i = 0; i < 60; i = i + 1) post(9'h080 + i[8:0]);
repeat (4) @(negedge clk);
check(seen[0] - base0 == 60,
"60 back-to-back writes all reached the fast subscriber");
check(n_written == 81, "and the producer counted every one of them");
check(missed2 > 0,
"the SLOW subscriber missed some -- and could not say so");
$display(" [info] slow subscriber missed %0d of 60 writes", missed2);
//=== there is no backpressure path at all ============================
// Not a measurement -- a statement about the port list. The producer
// has no input from any subscriber, so nothing a subscriber does can
// change when write_i is accepted.
check(n_written == 81,
"the producer's count is unaffected by any subscriber's state");
check(seen[0] == seen[1],
"and two subscribers with identical behaviour agree exactly");
//=== adding a subscriber changes nothing for the others =============
// Subscriber 2 has been missing items throughout. Subscribers 0 and 1
// are unaffected by that, which is the reuse property.
check(seen[0] > seen[2],
"the slow subscriber is behind the others");
check(seen[0] == seen[1],
"yet the fast ones are still exactly in step with each other");
//=== quiet means quiet ==============================================
base0 = seen[0];
repeat (30) @(negedge clk);
check(seen[0] == base0, "with no write, no subscriber is disturbed");
check(sub_valid === {NSUB{1'b0}}, "and no valid is left asserted");
$display("== %0d checks, %0d failures ==", checks, failures);
if (failures == 0) $display(" RESULT: ALL SYSTEMVERILOG ANALYSIS-BUS TESTS PASSED");
else $display(" RESULT: SYSTEMVERILOG ANALYSIS-BUS TESTS FAILED");
$finish;
end
endmodule--===========================================================================
-- tb_uart_analysis_bus — self-checking VHDL-2008 testbench
--
-- An analysis port is defined by what it CANNOT do, so most of this suite
-- is about absences: every subscriber sees every item on the same cycle,
-- connecting more subscribers changes nothing for the existing ones, and
-- there is no way for a subscriber to refuse or delay a write.
--
-- The last is demonstrated rather than asserted: a subscriber that cannot
-- keep up is modelled, and the suite measures what it MISSES. That loss is
-- invisible to the producer by construction, which is the single most
-- important thing to understand about analysis ports.
--
-- Same 17 counted checks as the Verilog and SystemVerilog twins.
--===========================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity tb_uart_analysis_bus is
end entity tb_uart_analysis_bus;
architecture sim of tb_uart_analysis_bus is
constant NSUB : positive := 3;
constant W : positive := 9;
constant TCLK : time := 10 ns;
constant BUSY_CYC : natural := 4;
signal clk : std_logic := '0';
signal rst_n : std_logic := '0';
signal done : boolean := false;
signal write_s : std_logic := '0';
signal item_s : std_logic_vector(W-1 downto 0) := (others => '0');
signal sub_valid : std_logic_vector(NSUB-1 downto 0);
signal sub_item : std_logic_vector(NSUB*W-1 downto 0);
signal n_written : natural;
type nat_arr is array (0 to NSUB-1) of natural;
type vec_arr is array (0 to NSUB-1) of std_logic_vector(W-1 downto 0);
signal seen : nat_arr := (others => 0);
signal last : vec_arr := (others => (others => '0'));
signal missed2 : natural := 0;
signal disagree : natural := 0;
begin
clk <= not clk after TCLK/2 when not done else '0';
dut : entity work.uart_analysis_bus
generic map (NSUB => NSUB, W => W)
port map (clk => clk, rst_n => rst_n,
write_i => write_s, item_i => item_s,
sub_valid_o => sub_valid, sub_item_o => sub_item,
n_written_o => n_written);
-- sub 0 and 1 keep up perfectly; sub 2 is busy for BUSY_CYC cycles after
-- each item it accepts, and therefore misses anything arriving meanwhile.
subs : process (clk)
variable busy2 : natural := 0;
begin
if rising_edge(clk) and rst_n = '1' then
for s in 0 to 1 loop
if sub_valid(s) = '1' then
seen(s) <= seen(s) + 1;
last(s) <= sub_item((s+1)*W-1 downto s*W);
end if;
end loop;
if busy2 > 0 then busy2 := busy2 - 1; end if;
if sub_valid(2) = '1' then
if busy2 = 0 then
seen(2) <= seen(2) + 1;
last(2) <= sub_item(3*W-1 downto 2*W);
busy2 := BUSY_CYC;
else
-- The write still happened. Nothing told the producer.
missed2 <= missed2 + 1;
end if;
end if;
if sub_valid(0) = '1' and sub_valid(1) = '1'
and sub_item(W-1 downto 0) /= sub_item(2*W-1 downto W) then
disagree <= disagree + 1;
end if;
end if;
end process subs;
watchdog : process
begin
wait for 10 ms;
report "watchdog: simulation did not finish" severity failure;
end process watchdog;
stim : process
variable checks, failures : natural := 0;
variable base0 : natural;
procedure check(cond : boolean; name : string) is
begin
checks := checks + 1;
if cond then report " PASS " & name severity note;
else failures := failures + 1; report " FAIL " & name severity error;
end if;
end procedure check;
procedure post(d : std_logic_vector(W-1 downto 0)) is
begin
wait until falling_edge(clk); write_s <= '1'; item_s <= d;
wait until falling_edge(clk); write_s <= '0';
end procedure post;
begin
report "== uart_analysis_bus : self-checking VHDL testbench ==" severity note;
rst_n <= '0';
for i in 1 to 4 loop wait until falling_edge(clk); end loop;
check(sub_valid = (sub_valid'range => '0'),
"reset: no subscriber is being written to");
rst_n <= '1';
for i in 1 to 2 loop wait until falling_edge(clk); end loop;
--=== one write reaches every subscriber, on the same cycle ==========
post('0' & x"A5");
for i in 1 to 2 loop wait until falling_edge(clk); end loop;
check(n_written = 1, "one write is counted once by the producer");
check(seen(0) = 1 and seen(1) = 1 and seen(2) = 1,
"and reaches ALL THREE subscribers");
check(last(0) = ('0' & x"A5") and last(1) = ('0' & x"A5")
and last(2) = ('0' & x"A5"),
"each of them receiving the same item");
check(disagree = 0,
"subscribers never receive different items on the same write");
--=== a burst, with everyone keeping up ==============================
for i in 0 to 19 loop
post(std_logic_vector(to_unsigned(16#10# + i, W)));
for k in 1 to BUSY_CYC loop wait until falling_edge(clk); end loop;
end loop;
check(seen(0) = 21 and seen(1) = 21,
"twenty more writes: the fast subscribers saw all of them");
check(seen(2) = 21, "and so did the slow one, given time between writes");
check(missed2 = 0, "nothing was missed while the pace allowed it");
--=== THE property: a slow subscriber cannot push back ===============
base0 := seen(0);
for i in 0 to 59 loop
post(std_logic_vector(to_unsigned(16#80# + i, W)));
end loop;
for i in 1 to 4 loop wait until falling_edge(clk); end loop;
check(seen(0) - base0 = 60,
"60 back-to-back writes all reached the fast subscriber");
check(n_written = 81, "and the producer counted every one of them");
check(missed2 > 0,
"the SLOW subscriber missed some -- and could not say so");
report " [info] slow subscriber missed " & integer'image(missed2)
& " of 60 writes" severity note;
--=== there is no backpressure path at all ============================
check(n_written = 81,
"the producer's count is unaffected by any subscriber's state");
check(seen(0) = seen(1),
"and two subscribers with identical behaviour agree exactly");
--=== adding a subscriber changes nothing for the others =============
check(seen(0) > seen(2), "the slow subscriber is behind the others");
check(seen(0) = seen(1),
"yet the fast ones are still exactly in step with each other");
--=== quiet means quiet ==============================================
base0 := seen(0);
for i in 1 to 30 loop wait until falling_edge(clk); end loop;
check(seen(0) = base0, "with no write, no subscriber is disturbed");
check(sub_valid = (sub_valid'range => '0'),
"and no valid is left asserted");
report "== " & integer'image(checks) & " checks, "
& integer'image(failures) & " failures ==" severity note;
if failures = 0 then
report " RESULT: ALL VHDL ANALYSIS-BUS TESTS PASSED" severity note;
else
report " RESULT: VHDL ANALYSIS-BUS TESTS FAILED" severity error;
end if;
done <= true;
wait;
end process stim;
end architecture sim;What it costs
The testbench models three subscribers. Two keep up perfectly. The third is busy for four cycles after each item it accepts — a subscriber doing real work inside write(), which is exactly what a coverage collector sampling a covergroup or a scoreboard doing a queue search is doing.
Then sixty writes arrive back to back:
PASS 60 back-to-back writes all reached the fast subscriber
PASS and the producer counted every one of them
PASS the SLOW subscriber missed some -- and could not say so
[info] slow subscriber missed 30 of 60 writes
PASS the producer's count is unaffected by any subscriber's state
PASS and two subscribers with identical behaviour agree exactly
== 17 checks, 0 failures ==
Verilog-2001 : 17 checks, 0 failures missed 30 of 60
SystemVerilog : 17 checks, 0 failures missed 30 of 60
VHDL-2008 : 17 checks, 0 failures missed 30 of 60Thirty of sixty transactions lost, and nothing anywhere reported it.
4. The Agent, and the Flag That Earns It
class uart_agent extends uvm_agent;
`uvm_component_utils(uart_agent)
uart_cfg m_cfg;
uart_sequencer m_sqr;
uart_driver m_drv;
uart_monitor m_mon;
// The agent republishes the monitor's port so the environment connects to
// the AGENT, not into it. An env that reached in to m_agent.m_mon.ap would
// break the moment the agent's internals changed.
uvm_analysis_port #(uart_frame_item) ap;
function new(string name, uvm_component parent);
super.new(name, parent);
ap = new("ap", this);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
if (!uvm_config_db#(uart_cfg)::get(this, "", "cfg", m_cfg))
`uvm_fatal("NOCFG", "no uart_cfg for the UART agent")
// The monitor is ALWAYS built.
m_mon = uart_monitor::type_id::create("m_mon", this);
// The sequencer and driver are built only when the agent is active.
if (m_cfg.is_active) begin
m_sqr = uart_sequencer::type_id::create("m_sqr", this);
m_drv = uart_driver ::type_id::create("m_drv", this);
end
endfunction
function void connect_phase(uvm_phase phase);
m_mon.ap.connect(ap); // republish
if (m_cfg.is_active)
m_drv.seq_item_port.connect(m_sqr.seq_item_export);
endfunction
endclassFour lines of if, and the same agent serves two completely different testbenches.
| active | passive | |
|---|---|---|
| sequencer, driver | built | not built at all |
| monitor | built | built |
| who drives the link | this agent | real software on a real processor |
| used in | block-level UART verification | an SoC testbench where the UART is incidental |
5. Mutation
Three defects installed in the analysis port:
| # | Defect installed | Result |
|---|---|---|
| M5 | only subscriber 0 receives the write | killed, 7 |
| M6 | one subscriber receives a stale item | killed, 2 |
| M7 | the producer's count is not incremented | killed, 3 |
M6 is the interesting one. A stale item reaching one subscriber and not the others is the shape of failure that a single-subscriber testbench cannot detect at all — with one consumer there is nothing to disagree with. It is caught here by a check that compares two subscribers against each other on the same write:
if (sub_valid[0] && sub_valid[1] &&
(sub_item[0*W +: W] !== sub_item[1*W +: W])) disagree = disagree + 1;Two observers of the same event are worth more than one, and the argument is the same one Chapter 12.2 §4 made about two consumers of a synchronised line: disagreement between them is observable, where a single wrong value is not.
6. Verification
Give the monitor one input and make it bring its own timing. Everything in §4 depends on it, three modules after the decision was made.
Republish the monitor's port from the agent. An environment that connects to m_agent.m_mon.ap is welded to the agent's internals.
Do no slow work inside write(). Queue the item and process it elsewhere; §3 measured 30 of 60 transactions lost to a subscriber doing four cycles of work.
Assert on subscriber counts. The analysis port has no channel to report a dropped transaction, so the only defence is a subscriber that knows how many it should have received.
Connect two subscribers and compare them. A stale or corrupted broadcast is invisible with one consumer and obvious with two.
And build the sequencer and driver only when active. A passive agent that still constructs a driver will connect a virtual interface it must not drive, and the symptom is two drivers fighting over the wire.
7. Debugging
8. Understanding Check
9. Summary
The monitor does not change — one input, its own bit period, its own parity definition. What changes is that it publishes through a port instead of being read through a hierarchical reference.
write() is a void function: no status, no ready, no backpressure, no failure mode. That absence is what lets a monitor broadcast without knowing who is listening.
And it costs exactly one thing, measured: a subscriber doing four cycles of work inside write() missed 30 of 60 transactions, with nothing anywhere reporting it.
So: queue in write(), never block, and assert on counts — the only defence is a subscriber that knows how many it should have seen.
Two subscribers compared against each other catch a corrupted broadcast that one subscriber cannot see at all.
is_active is four lines of if and it is the whole payback for the methodology: the same agent verifies the UART at block level and watches it in an SoC testbench where firmware is driving.
17 checks in each of three languages, 0 failures; three mutants, three killed.
10. What Comes Next
Chapter 16.5 connects the subscribers — the reference model, the scoreboard and the coverage collector — and builds the mechanism that decides when the test is allowed to stop, which has a failure mode that looks exactly like success.
Browse the full path on the UART tutorials index. For the monitor's algorithm, read back to Chapter 14.4.
Continue learning
Related tutorials
- Related topic
UART UVM Environment Architecture
How agent, scoreboard, reference model, coverage and configuration fit together for a serial asynchronous interface, what each UVM class replaces from the directed environment, and what the methodology does not change.
- Related topic
A Reusable UVM Ethernet Agent
Module 20's four components share no key, so the agent is where one lives; and every modulus in the coverage model is the beat width, so the cross moves 128x between 10 and 100 Gb/s.
- Related topic
UVM Concepts
You have already built a monitor, a reference model, a scoreboard and a coverage collector. UVM is a vocabulary for those roles — it supplies no protocol knowledge and no bins.
- Related topic
Parameterisation and Reusable RTL Organisation
What belongs to a parameter, what belongs to a register, and how to organise the RTL so the IP survives its second project — with three real defects the first project could never have exposed.
Where this fits
Part of the UART curriculum.
