SPI · Module 16
Active and Passive Agents
Three agents on one bus. A structurally passive agent reports every transaction and is high impedance on every cycle measured; a flag-gated one is indistinguishable from it until a single gate is missing, at which point it corrupts the bus while still declared passive.
Chapters 16.3 to 16.6 built four components. Each is useful alone and none of them is reusable alone, because reusing one means knowing how to connect it: which pins, which configuration, which clock, in which direction.
An agent is the object that knows that, once. And the interesting question about one is not what it does.
An agent that chooses not to drive and an agent that cannot drive are indistinguishable — until one condition is missing.
1. What An Agent Packages
the DRIVE path pins the agent writes (active only)
the OBSERVE path pins the agent reads (both roles)
the configuration shared by driver and monitor, from the SPEC
the sequencer side a request/accept handshake (active only)
the analysis side reconstructed transactions (both roles)The drive path and the observe path are separate ports, deliberately. On a single-agent bus they carry the same wires and the separation looks like noise; on a shared bus they do not, and an agent that conflates them monitors its own intentions instead of the bus — which is Chapter 16.5's peeking problem arriving through the port list rather than through the configuration.
2. Two Ways To Build A Passive Agent
AS A FLAG AS A PARAMETER
------------------------------------- ----------------------------------
the driver EXISTS there is NO DRIVER
its outputs are live nets the pin outputs are constants
one condition per pin stands between decided at ELABORATION
them and the bus no runtime condition can change it
miss one pin and the agent drives the failure mode is unrepresentable
a bus it was declared unable to
touchThe flag version works. It is one condition, read at the right moment, and it is what almost everybody writes. Its weakness is not that the condition is hard — it is that there is one per pin, and the number of pins grows.
The parameter version removes the question. When ACTIVE is zero the generate branch that would have built a driver is not taken: no instance, no state, no nets. There is nothing left to condition.
3. Passive Does Not Mean Partial
32 transactions in 16 configurations, one active agent and two declared passive
transactions the ACTIVE agent's monitor reported ....... 32
transactions the PASSIVE agent's monitor reported ...... 32
reports agreeing on every field ....................... 32
reports disagreeing ................................... 0
cycles the passive agent's drive path was NOT high-Z ... 0 of 2512
the passive agent's busy/done, with a request held on
its inputs all run: 0 / 0Field for field identical to the active agent's own monitor, across every configuration. An observer that misses transactions is a broken observer, not a modest one — and the comparison is made cycle by cycle, including the equality of the two valid signals, because comparing them only when both fire would hide a passive agent that emits late or not at all.
And the drive path: high impedance on all 2512 cycles sampled, with a complete transaction request held on the agent's inputs for the entire run. There is no driver inside it to hear the request, so the request produces nothing at all. That is measured rather than asserted, which matters because "the code does not drive" is a claim about reading and this is a claim about behaviour.
4. The Missing Gate
The flag-gated agent is held passive for the entire simulation and is never asked for a transaction.
gates master's words correct master's words wrong
all present 3 0
one missing 0 3With every gate present it is indistinguishable from the structurally passive one. Nothing wrong, nothing to review, nothing to raise. Which is exactly why the flag version survives review for years.
With exactly one gate left out, it corrupted every word the master sent. And no transaction was needed for the damage:
an idle driver still holds its output at a DEFINITE LEVEL
a definite level contending with another agent's definite level
is a wire carrying XThe driver has been sitting at cs_n = 1, sclk = cpol, mosi = 0 since reset. Ungate MOSI and that 0 contends with the master's data on every bit that should be a 1.
5. The Principle
WHEN A GUARANTEE MATTERS, SPEND STRUCTURE ON IT RATHER THAN CONTROL FLOW.This is the third time the module has reached that conclusion from a different direction, and the repetition is the argument:
16.3 `modport mon` grants inputs only, so a monitor that assigns to a pin
does not COMPILE. Not "should not" -- cannot.
16.5 the monitor's configuration comes from the specification. A field
describing the implementation is what turns a checker into an echo,
and no amount of care in the body prevents it.
16.7 the passive agent has no driver. Not a disabled one -- none.Each case replaces a promise that somebody has to keep with a property that cannot be broken. And VHDL supplies part of this one for free: the monitor's pins are ports of mode in, so the analyser refuses any assignment to them before a simulation exists.
6. Building It — Three HDLs
// spi_agent.sv
//
// Chapter 16.7 -- the agent, and the difference between an agent that CHOOSES not to drive and
// one that CANNOT.
//
// AN AGENT IS THE PACKAGE, and the packaging is the point.
//
// Chapters 16.3 to 16.6 built four components: an interface that holds the sampling discipline,
// a driver that owns every timing number, a monitor that reads only pins and configuration, and
// a reference model with a scoreboard behind it. Each is useful alone and none of them is
// reusable alone, because reusing one means knowing how to connect it -- which pins, which
// configuration, which clock, in which direction. An agent is the object that knows that, once,
// so that connecting a second instance to a second bus is one line rather than a paragraph.
//
// ACTIVE AND PASSIVE, and why the distinction is structural here rather than a mode bit.
//
// An ACTIVE agent drives and observes. A PASSIVE agent only observes -- it is what you connect
// to the far end of a bus whose traffic somebody else generates: a second master in a
// multi-master system, a slave being characterised, a link between two blocks that both belong
// to the design.
//
// The obvious way to build one is a flag. `if (!passive) drive_the_pins();` -- one condition,
// read at the right moment, and it works. It also means the driver EXISTS, its outputs are live
// nets, and the only thing between them and the bus is a condition somebody has to have written
// correctly on every pin. Miss one pin, or gate three of four, and the agent drives a bus it was
// declared unable to touch. `spi_agent_flag` below is that agent, with a switch to leave exactly
// one pin ungated, and the testbench measures what it does to traffic it is only supposed to be
// watching.
//
// THIS AGENT DOES IT WITH A PARAMETER AND A GENERATE BLOCK.
//
// When ACTIVE is zero THERE IS NO DRIVER. Not a disabled driver, not a driver whose outputs are
// masked -- no instance, no state, no nets. The pin outputs are constant high impedance, decided
// at elaboration, and no runtime condition can change that because there is nothing left to
// condition. The failure mode of the flag version is not merely unlikely here; it is
// unrepresentable.
//
// That is the general principle and it is worth stating plainly, because it is not specific to
// SPI or to agents: WHEN A GUARANTEE MATTERS, SPEND STRUCTURE ON IT RATHER THAN CONTROL FLOW.
// The same argument produced Chapter 16.3's `modport mon`, which makes a monitor's accidental
// drive a compile error, and the same argument is why the VHDL version of this file gets the
// guarantee partly for free from its port modes.
//
// THE DRIVE PATH AND THE OBSERVE PATH ARE SEPARATE PORTS, deliberately.
//
// `sclk_o / cs_n_o / mosi_o` are what this agent drives. `sclk_i / cs_n_i / mosi_i / miso_i` are
// what it observes. On a single-agent bus they are the same wires and the separation looks like
// noise; on a shared bus they are not, and an agent that conflates them is an agent that
// monitors its own intentions instead of the bus -- which is Chapter 16.5's peeking problem
// arriving through the port list instead of through the configuration.
`timescale 1ns/1ps
module spi_agent #(
parameter int ACTIVE = 1,
parameter int LEAD = 4,
parameter int HALF = 3,
parameter int LAG = 2,
parameter int GAP = 3,
parameter int DW = 32,
parameter int LEN_W = 6,
parameter int CNT_W = 10
) (
input wire clk,
input wire rst_n,
// --- the sequencer side, meaningful only when ACTIVE ---------------------
input wire start,
input wire [DW-1:0] tx_data,
input wire [LEN_W-1:0] nbits,
input wire [2:0] fault,
output wire busy,
output wire done,
output wire [DW-1:0] rx_data,
// --- the configuration, shared by the driver and the monitor -------------
input wire cpol,
input wire cpha,
input wire lsb_first,
// --- the DRIVE path ------------------------------------------------------
output wire sclk_o,
output wire cs_n_o,
output wire mosi_o,
// --- the OBSERVE path ----------------------------------------------------
input wire sclk_i,
input wire cs_n_i,
input wire mosi_i,
input wire miso_i,
// --- the analysis side, present in both roles ----------------------------
output wire t_valid,
output wire [DW-1:0] t_mosi,
output wire [DW-1:0] t_miso,
output wire [LEN_W:0] t_nbits,
output wire [CNT_W-1:0] t_edges,
output wire t_partial
);
generate
if (ACTIVE != 0) begin : g_active
spi_driver #(.LEAD(LEAD), .HALF(HALF), .LAG(LAG), .GAP(GAP),
.DW(DW), .LEN_W(LEN_W), .CNT_W(16)) u_drv (
.clk(clk), .rst_n(rst_n),
.start(start), .tx_data(tx_data), .nbits(nbits),
.cpol(cpol), .cpha(cpha), .lsb_first(lsb_first), .fault(fault),
.busy(busy), .done(done), .rx_data(rx_data),
.sclk(sclk_o), .cs_n(cs_n_o), .mosi(mosi_o), .miso(miso_i)
);
end else begin : g_passive
// THERE IS NO DRIVER HERE. This is the whole difference between this agent and the
// flag-gated one below: not a driver that is switched off, but an elaboration in
// which no driver was ever built. The three pin outputs are constants.
assign sclk_o = 1'bz;
assign cs_n_o = 1'bz;
assign mosi_o = 1'bz;
// The sequencer-side outputs are constants too, so that a testbench which
// mistakenly waits for this agent to finish a transaction HANGS rather than
// proceeding on a fabricated handshake. A false `done` is worse than a hang: it
// turns a wiring mistake into a test that passes.
assign busy = 1'b0;
assign done = 1'b0;
assign rx_data = {DW{1'b0}};
end
endgenerate
// The monitor is present in BOTH roles, connected to the OBSERVE path, and its
// configuration comes from the same place the driver's does -- which is the specification,
// not the driver. Chapter 16.5 measured what happens when it comes from the driver instead.
spi_txn_monitor #(.DW(DW), .LEN_W(LEN_W), .CNT_W(CNT_W)) u_mon (
.clk(clk), .rst_n(rst_n),
.sclk(sclk_i), .cs_n(cs_n_i), .mosi(mosi_i), .miso(miso_i),
.cpol(cpol), .cpha(cpha), .lsb_first(lsb_first), .len(nbits),
.t_valid(t_valid), .t_mosi(t_mosi), .t_miso(t_miso),
.t_nbits(t_nbits), .t_edges(t_edges), .t_partial(t_partial)
);
endmodule
// =========================================================================================
// THE FLAG-GATED AGENT -- passivity as a runtime condition, which is how it is usually built.
// =========================================================================================
//
// The driver exists. Its outputs are live nets. Between them and the bus is one condition per
// pin, and `gate_bug` leaves exactly one of those conditions out -- which is not a contrived
// fault, it is the most ordinary mistake in this shape of code: a pin added later, a copied
// line, a gate written for three signals when the bus has four.
//
// The consequence is not a wrong report. It is a corrupted BUS, which means every other agent's
// observations become wrong, and the failure appears at the far end of the system as a design
// bug in something that is working correctly.
module spi_agent_flag #(
parameter int LEAD = 4,
parameter int HALF = 3,
parameter int LAG = 2,
parameter int GAP = 3,
parameter int DW = 32,
parameter int LEN_W = 6,
parameter int CNT_W = 10
) (
input wire clk,
input wire rst_n,
input wire passive, // "do not drive" -- as a runtime promise
input wire gate_bug, // one pin's gate, left out
input wire start,
input wire [DW-1:0] tx_data,
input wire [LEN_W-1:0] nbits,
input wire [2:0] fault,
input wire cpol,
input wire cpha,
input wire lsb_first,
output wire sclk_o,
output wire cs_n_o,
output wire mosi_o,
input wire sclk_i,
input wire cs_n_i,
input wire mosi_i,
input wire miso_i
);
wire d_sclk, d_cs_n, d_mosi;
wire d_busy, d_done;
wire [DW-1:0] d_rx;
spi_driver #(.LEAD(LEAD), .HALF(HALF), .LAG(LAG), .GAP(GAP),
.DW(DW), .LEN_W(LEN_W), .CNT_W(16)) u_drv (
.clk(clk), .rst_n(rst_n),
.start(start), .tx_data(tx_data), .nbits(nbits),
.cpol(cpol), .cpha(cpha), .lsb_first(lsb_first), .fault(fault),
.busy(d_busy), .done(d_done), .rx_data(d_rx),
.sclk(d_sclk), .cs_n(d_cs_n), .mosi(d_mosi), .miso(miso_i)
);
assign sclk_o = passive ? 1'bz : d_sclk;
assign cs_n_o = passive ? 1'bz : d_cs_n;
// THE ONE MISSING GATE. With `gate_bug` set, MOSI is driven whatever `passive` says -- and
// note that no transaction is needed for the damage: a driver sitting idle still holds its
// output at a definite level, and a definite level contending with another agent's definite
// level is a bus carrying X.
assign mosi_o = (passive && !gate_bug) ? 1'bz : d_mosi;
endmodule// spi_agent.v
//
// Chapter 16.7 -- the agent, and the difference between an agent that CHOOSES not to drive and
// one that CANNOT.
//
// AN AGENT IS THE PACKAGE, and the packaging is the point.
//
// Chapters 16.3 to 16.6 built four components: an interface that holds the sampling discipline,
// a driver that owns every timing number, a monitor that reads only pins and configuration, and
// a reference model with a scoreboard behind it. Each is useful alone and none of them is
// reusable alone, because reusing one means knowing how to connect it -- which pins, which
// configuration, which clock, in which direction. An agent is the object that knows that, once,
// so that connecting a second instance to a second bus is one line rather than a paragraph.
//
// ACTIVE AND PASSIVE, and why the distinction is structural here rather than a mode bit.
//
// An ACTIVE agent drives and observes. A PASSIVE agent only observes -- it is what you connect
// to the far end of a bus whose traffic somebody else generates: a second master in a
// multi-master system, a slave being characterised, a link between two blocks that both belong
// to the design.
//
// The obvious way to build one is a flag. `if (!passive) drive_the_pins();` -- one condition,
// read at the right moment, and it works. It also means the driver EXISTS, its outputs are live
// nets, and the only thing between them and the bus is a condition somebody has to have written
// correctly on every pin. Miss one pin, or gate three of four, and the agent drives a bus it was
// declared unable to touch. `spi_agent_flag` below is that agent, with a switch to leave exactly
// one pin ungated, and the testbench measures what it does to traffic it is only supposed to be
// watching.
//
// THIS AGENT DOES IT WITH A PARAMETER AND A GENERATE BLOCK.
//
// When ACTIVE is zero THERE IS NO DRIVER. Not a disabled driver, not a driver whose outputs are
// masked -- no instance, no state, no nets. The pin outputs are constant high impedance, decided
// at elaboration, and no runtime condition can change that because there is nothing left to
// condition. The failure mode of the flag version is not merely unlikely here; it is
// unrepresentable.
//
// That is the general principle and it is worth stating plainly, because it is not specific to
// SPI or to agents: WHEN A GUARANTEE MATTERS, SPEND STRUCTURE ON IT RATHER THAN CONTROL FLOW.
// The same argument produced Chapter 16.3's `modport mon`, which makes a monitor's accidental
// drive a compile error, and the same argument is why the VHDL version of this file gets the
// guarantee partly for free from its port modes.
//
// THE DRIVE PATH AND THE OBSERVE PATH ARE SEPARATE PORTS, deliberately.
//
// `sclk_o / cs_n_o / mosi_o` are what this agent drives. `sclk_i / cs_n_i / mosi_i / miso_i` are
// what it observes. On a single-agent bus they are the same wires and the separation looks like
// noise; on a shared bus they are not, and an agent that conflates them is an agent that
// monitors its own intentions instead of the bus -- which is Chapter 16.5's peeking problem
// arriving through the port list instead of through the configuration.
`timescale 1ns/1ps
module spi_agent #(
parameter ACTIVE = 1,
parameter LEAD = 4,
parameter HALF = 3,
parameter LAG = 2,
parameter GAP = 3,
parameter DW = 32,
parameter LEN_W = 6,
parameter CNT_W = 10
) (
input wire clk,
input wire rst_n,
// --- the sequencer side, meaningful only when ACTIVE ---------------------
input wire start,
input wire [DW-1:0] tx_data,
input wire [LEN_W-1:0] nbits,
input wire [2:0] fault,
output wire busy,
output wire done,
output wire [DW-1:0] rx_data,
// --- the configuration, shared by the driver and the monitor -------------
input wire cpol,
input wire cpha,
input wire lsb_first,
// --- the DRIVE path ------------------------------------------------------
output wire sclk_o,
output wire cs_n_o,
output wire mosi_o,
// --- the OBSERVE path ----------------------------------------------------
input wire sclk_i,
input wire cs_n_i,
input wire mosi_i,
input wire miso_i,
// --- the analysis side, present in both roles ----------------------------
output wire t_valid,
output wire [DW-1:0] t_mosi,
output wire [DW-1:0] t_miso,
output wire [LEN_W:0] t_nbits,
output wire [CNT_W-1:0] t_edges,
output wire t_partial
);
generate
if (ACTIVE != 0) begin : g_active
spi_driver #(.LEAD(LEAD), .HALF(HALF), .LAG(LAG), .GAP(GAP),
.DW(DW), .LEN_W(LEN_W), .CNT_W(16)) u_drv (
.clk(clk), .rst_n(rst_n),
.start(start), .tx_data(tx_data), .nbits(nbits),
.cpol(cpol), .cpha(cpha), .lsb_first(lsb_first), .fault(fault),
.busy(busy), .done(done), .rx_data(rx_data),
.sclk(sclk_o), .cs_n(cs_n_o), .mosi(mosi_o), .miso(miso_i)
);
end else begin : g_passive
// THERE IS NO DRIVER HERE. This is the whole difference between this agent and the
// flag-gated one below: not a driver that is switched off, but an elaboration in
// which no driver was ever built. The three pin outputs are constants.
assign sclk_o = 1'bz;
assign cs_n_o = 1'bz;
assign mosi_o = 1'bz;
// The sequencer-side outputs are constants too, so that a testbench which
// mistakenly waits for this agent to finish a transaction HANGS rather than
// proceeding on a fabricated handshake. A false `done` is worse than a hang: it
// turns a wiring mistake into a test that passes.
assign busy = 1'b0;
assign done = 1'b0;
assign rx_data = {DW{1'b0}};
end
endgenerate
// The monitor is present in BOTH roles, connected to the OBSERVE path, and its
// configuration comes from the same place the driver's does -- which is the specification,
// not the driver. Chapter 16.5 measured what happens when it comes from the driver instead.
spi_txn_monitor #(.DW(DW), .LEN_W(LEN_W), .CNT_W(CNT_W)) u_mon (
.clk(clk), .rst_n(rst_n),
.sclk(sclk_i), .cs_n(cs_n_i), .mosi(mosi_i), .miso(miso_i),
.cpol(cpol), .cpha(cpha), .lsb_first(lsb_first), .len(nbits),
.t_valid(t_valid), .t_mosi(t_mosi), .t_miso(t_miso),
.t_nbits(t_nbits), .t_edges(t_edges), .t_partial(t_partial)
);
endmodule
// =========================================================================================
// THE FLAG-GATED AGENT -- passivity as a runtime condition, which is how it is usually built.
// =========================================================================================
//
// The driver exists. Its outputs are live nets. Between them and the bus is one condition per
// pin, and `gate_bug` leaves exactly one of those conditions out -- which is not a contrived
// fault, it is the most ordinary mistake in this shape of code: a pin added later, a copied
// line, a gate written for three signals when the bus has four.
//
// The consequence is not a wrong report. It is a corrupted BUS, which means every other agent's
// observations become wrong, and the failure appears at the far end of the system as a design
// bug in something that is working correctly.
module spi_agent_flag #(
parameter LEAD = 4,
parameter HALF = 3,
parameter LAG = 2,
parameter GAP = 3,
parameter DW = 32,
parameter LEN_W = 6,
parameter CNT_W = 10
) (
input wire clk,
input wire rst_n,
input wire passive, // "do not drive" -- as a runtime promise
input wire gate_bug, // one pin's gate, left out
input wire start,
input wire [DW-1:0] tx_data,
input wire [LEN_W-1:0] nbits,
input wire [2:0] fault,
input wire cpol,
input wire cpha,
input wire lsb_first,
output wire sclk_o,
output wire cs_n_o,
output wire mosi_o,
input wire sclk_i,
input wire cs_n_i,
input wire mosi_i,
input wire miso_i
);
wire d_sclk, d_cs_n, d_mosi;
wire d_busy, d_done;
wire [DW-1:0] d_rx;
spi_driver #(.LEAD(LEAD), .HALF(HALF), .LAG(LAG), .GAP(GAP),
.DW(DW), .LEN_W(LEN_W), .CNT_W(16)) u_drv (
.clk(clk), .rst_n(rst_n),
.start(start), .tx_data(tx_data), .nbits(nbits),
.cpol(cpol), .cpha(cpha), .lsb_first(lsb_first), .fault(fault),
.busy(d_busy), .done(d_done), .rx_data(d_rx),
.sclk(d_sclk), .cs_n(d_cs_n), .mosi(d_mosi), .miso(miso_i)
);
assign sclk_o = passive ? 1'bz : d_sclk;
assign cs_n_o = passive ? 1'bz : d_cs_n;
// THE ONE MISSING GATE. With `gate_bug` set, MOSI is driven whatever `passive` says -- and
// note that no transaction is needed for the damage: a driver sitting idle still holds its
// output at a definite level, and a definite level contending with another agent's definite
// level is a bus carrying X.
assign mosi_o = (passive && !gate_bug) ? 1'bz : d_mosi;
endmodule-- spi_agent.vhd
--
-- Chapter 16.7 -- the agent, and the difference between one that CHOOSES not to drive and one
-- that CANNOT.
--
-- AN AGENT IS THE PACKAGE, and the packaging is the point.
--
-- Chapters 16.3 to 16.6 built four components: a bundle that holds the sampling discipline, a
-- driver that owns every timing number, a monitor that reads only pins and configuration, and a
-- reference model with a scoreboard behind it. Each is useful alone and none is reusable alone,
-- because reusing one means knowing how to connect it -- which pins, which configuration, which
-- clock, in which direction. An agent is the object that knows that once, so that connecting a
-- second instance to a second bus is one line rather than a paragraph.
--
-- ACTIVE AND PASSIVE, and why the distinction is structural here rather than a mode bit.
--
-- An ACTIVE agent drives and observes. A PASSIVE agent only observes -- what you connect to the
-- far end of a bus whose traffic somebody else generates: a second master in a multi-master
-- system, a slave being characterised, a link between two blocks that both belong to the design.
--
-- The obvious way to build one is a flag. One condition, read at the right moment, and it works.
-- It also means the driver EXISTS, its outputs are live signals, and the only thing between them
-- and the bus is a condition somebody has to have written correctly on every pin. Miss one pin,
-- or gate three of four, and the agent drives a bus it was declared unable to touch.
-- `spi_agent_flag` below is that agent, with a switch that leaves exactly one pin ungated, and
-- the testbench measures what it does to traffic it is only supposed to be watching.
--
-- THIS AGENT DOES IT WITH A GENERIC AND A CONDITIONAL GENERATE.
--
-- When ACTIVE is zero THERE IS NO DRIVER. Not a disabled driver, not a driver whose outputs are
-- masked -- no instance, no state, no signals. The pin outputs are constant high impedance,
-- decided at ELABORATION, and no runtime condition can change that because there is nothing left
-- to condition. The failure mode of the flag version is not merely unlikely here; it is
-- unrepresentable.
--
-- That is the general principle, and it is not specific to SPI or to agents: WHEN A GUARANTEE
-- MATTERS, SPEND STRUCTURE ON IT RATHER THAN CONTROL FLOW. The same argument produced Chapter
-- 16.3's `modport mon` in SystemVerilog -- and in VHDL the corresponding guarantee is already
-- free, because the monitor's pins are ports of mode `in` and the analyser refuses any assignment
-- to them.
--
-- THE DRIVE PATH AND THE OBSERVE PATH ARE SEPARATE PORTS, deliberately. On a single-agent bus
-- they carry the same wires and the separation looks like noise; on a shared bus they do not, and
-- an agent that conflates them monitors its own intentions instead of the bus -- Chapter 16.5's
-- peeking problem arriving through the port list instead of through the configuration.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.spi_driver_pkg.all;
use work.spi_txn_pkg.all;
entity spi_agent is
generic (
ACTIVE : natural := 1;
LEAD : natural := 4;
HALF : natural := 3;
LAG : natural := 2;
GAP : natural := 3
);
port (
clk : in std_logic;
rst_n : in std_logic;
-- the sequencer side, meaningful only when ACTIVE
start : in std_logic;
req : in spi_req_t;
busy : out std_logic;
done : out std_logic;
rx_data : out std_logic_vector(DW - 1 downto 0);
-- the DRIVE path
sclk_o : out std_logic;
cs_n_o : out std_logic;
mosi_o : out std_logic;
-- the OBSERVE path
sclk_i : in std_logic;
cs_n_i : in std_logic;
mosi_i : in std_logic;
miso_i : in std_logic;
-- the analysis side, present in both roles
obs : out spi_obs_t
);
end entity spi_agent;
architecture rtl of spi_agent is
begin
g_active : if ACTIVE /= 0 generate
u_drv : entity work.spi_driver
generic map (LEAD => LEAD, HALF => HALF, LAG => LAG, GAP => GAP)
port map (clk => clk, rst_n => rst_n, start => start, req => req,
busy => busy, done => done, rx_data => rx_data,
sclk => sclk_o, cs_n => cs_n_o, mosi => mosi_o, miso => miso_i);
end generate g_active;
-- THERE IS NO DRIVER HERE. This is the whole difference between this agent and the
-- flag-gated one below: not a driver that is switched off, but an elaboration in which no
-- driver was ever built. The three pin outputs are constants.
--
-- The sequencer-side outputs are constants too, so that a testbench which mistakenly waits
-- for this agent to finish a transaction HANGS rather than proceeding on a fabricated
-- handshake. A false `done` is worse than a hang: it turns a wiring mistake into a test that
-- passes.
g_passive : if ACTIVE = 0 generate
sclk_o <= 'Z';
cs_n_o <= 'Z';
mosi_o <= 'Z';
busy <= '0';
done <= '0';
rx_data <= (others => '0');
end generate g_passive;
-- The monitor is present in BOTH roles, connected to the OBSERVE path, and its configuration
-- comes from the same place the driver's does -- the specification, not the driver. Chapter
-- 16.5 measured what happens when it comes from the driver instead.
u_mon : entity work.spi_txn_monitor
port map (clk => clk, rst_n => rst_n,
sclk => sclk_i, cs_n => cs_n_i, mosi => mosi_i, miso => miso_i,
cpol => req.cpol, cpha => req.cpha, lsb_first => req.lsb_first,
len => req.nbits, obs => obs);
end architecture rtl;
-- =========================================================================================
-- THE FLAG-GATED AGENT -- passivity as a runtime condition, which is how it is usually built.
-- =========================================================================================
--
-- The driver exists. Its outputs are live signals. Between them and the bus is one condition per
-- pin, and `gate_bug` leaves exactly one of those conditions out -- which is not a contrived
-- fault, it is the most ordinary mistake in this shape of code: a pin added later, a copied line,
-- a gate written for three signals when the bus has four.
--
-- The consequence is not a wrong report. It is a corrupted BUS, so every other agent's
-- observations become wrong, and the failure appears at the far end of the system as a design bug
-- in something that is working correctly.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.spi_driver_pkg.all;
entity spi_agent_flag is
generic (
LEAD : natural := 4;
HALF : natural := 3;
LAG : natural := 2;
GAP : natural := 3
);
port (
clk : in std_logic;
rst_n : in std_logic;
passive : in std_logic; -- "do not drive" -- as a runtime promise
gate_bug : in std_logic; -- one pin's gate, left out
start : in std_logic;
req : in spi_req_t;
sclk_o : out std_logic;
cs_n_o : out std_logic;
mosi_o : out std_logic;
sclk_i : in std_logic;
cs_n_i : in std_logic;
mosi_i : in std_logic;
miso_i : in std_logic
);
end entity spi_agent_flag;
architecture rtl of spi_agent_flag is
signal d_sclk, d_cs_n, d_mosi : std_logic;
signal d_busy, d_done : std_logic;
signal d_rx : std_logic_vector(DW - 1 downto 0);
begin
u_drv : entity work.spi_driver
generic map (LEAD => LEAD, HALF => HALF, LAG => LAG, GAP => GAP)
port map (clk => clk, rst_n => rst_n, start => start, req => req,
busy => d_busy, done => d_done, rx_data => d_rx,
sclk => d_sclk, cs_n => d_cs_n, mosi => d_mosi, miso => miso_i);
sclk_o <= 'Z' when passive = '1' else d_sclk;
cs_n_o <= 'Z' when passive = '1' else d_cs_n;
-- THE ONE MISSING GATE. With `gate_bug` set, MOSI is driven whatever `passive` says -- and no
-- transaction is needed for the damage: a driver sitting idle still holds its output at a
-- definite level, and a definite level contending with another agent's definite level is a bus
-- carrying 'X'.
mosi_o <= 'Z' when (passive = '1' and gate_bug = '0') else d_mosi;
end architecture rtl;The Bench
// spi_agent_tb.sv
//
// THREE AGENTS ON ONE BUS, AND FOUR MEASUREMENTS THAT TOGETHER SAY WHY PASSIVITY HAS TO BE
// STRUCTURAL.
//
// u_master ACTIVE. Generates all the traffic.
// u_observer PASSIVE by PARAMETER. No driver was elaborated inside it at all.
// u_flag A flag-gated agent: its driver exists and its outputs are masked by a runtime
// condition, one condition per pin. It is held `passive` throughout this bench.
// It is the version almost everybody writes.
//
// THE MEASUREMENTS.
//
// 1. THE PASSIVE AGENT SEES EVERYTHING. Its monitor reconstructs every transaction the master
// generates, field for field identical to the master's own monitor, across every
// configuration. Passive does not mean partial: an observer that misses transactions is a
// broken observer, not a modest one.
//
// 2. THE PASSIVE AGENT CANNOT DRIVE, MEASURED TWO WAYS. Its three pin outputs are high
// impedance on every single cycle of the run -- sampled and counted, not asserted -- and
// its sequencer-side handshake never asserts even when a full transaction request is held
// on its inputs throughout. Requesting a transaction from a passive agent produces nothing
// at all, which is the correct behaviour and the reason a test that waits for it should
// hang rather than proceed.
//
// 3. THE FLAG-GATED AGENT IS INDISTINGUISHABLE FROM IT -- UNTIL ONE GATE IS MISSING. With
// every gate present, the bus is clean and all three agents agree. With exactly one gate
// left out, the same agent -- still holding `passive` high, still never asked for a
// transaction -- CORRUPTS THE BUS. An idle driver still holds its output at a definite
// level, and a definite level contending with another agent's definite level is a wire
// carrying X. Measured as the master's own monitor no longer reconstructing the words the
// master sent.
//
// 4. AND THE CORRUPTION IS BLAMED ON THE WRONG COMPONENT. The failing check is a data
// mismatch on the master's transactions. Nothing in that report mentions the observer. A
// bus corrupted by a component declared unable to touch it produces a bug report against
// whatever was being tested, and this is why the guarantee is worth spending structure on
// rather than a condition: the flag version's failure is not merely a wrong answer, it is
// a wrong answer that points somewhere else.
`timescale 1ns/1ps
module spi_agent_tb;
localparam int LEAD = 4;
localparam int HALF = 3;
localparam int LAG = 2;
localparam int GAP = 3;
localparam int DW = 32;
localparam int LEN_W = 6;
localparam int CNT_W = 10;
reg clk = 1'b0;
always #5 clk = ~clk;
reg rst_n = 1'b1;
reg start = 1'b0;
reg [DW-1:0] tx_data = {DW{1'b0}};
reg [LEN_W-1:0] nbits = 6'd8;
reg cpol = 1'b0, cpha = 1'b0, lsb_first = 1'b0;
reg [2:0] fault = 3'd0;
reg flag_gate_bug = 1'b0;
// A request held on the PASSIVE agent's sequencer inputs for the whole run, so that
// measurement 2 is about what the agent cannot do rather than about what it was not asked to
// do.
reg pass_start = 1'b0;
// ------------------------------------------------------------------
// The bus. Three agents drive into these wires; resolution does the rest, which is the
// point: a contended wire is X, and X is what a missing gate produces.
// ------------------------------------------------------------------
wire sclk, cs_n, mosi;
wire miso = ~mosi;
wire m_sclk_o, m_cs_n_o, m_mosi_o;
wire p_sclk_o, p_cs_n_o, p_mosi_o;
wire f_sclk_o, f_cs_n_o, f_mosi_o;
assign sclk = m_sclk_o;
assign cs_n = m_cs_n_o;
assign mosi = m_mosi_o;
assign sclk = p_sclk_o;
assign cs_n = p_cs_n_o;
assign mosi = p_mosi_o;
assign sclk = f_sclk_o;
assign cs_n = f_cs_n_o;
assign mosi = f_mosi_o;
// ------------------------------------------------------------------
wire m_valid, m_partial;
wire [DW-1:0] m_mosi, m_miso;
wire [LEN_W:0] m_nbits;
wire [CNT_W-1:0] m_edges;
wire m_busy, m_done;
wire [DW-1:0] m_rx;
spi_agent #(.ACTIVE(1), .LEAD(LEAD), .HALF(HALF), .LAG(LAG), .GAP(GAP),
.DW(DW), .LEN_W(LEN_W), .CNT_W(CNT_W)) u_master (
.clk(clk), .rst_n(rst_n),
.start(start), .tx_data(tx_data), .nbits(nbits), .fault(fault),
.busy(m_busy), .done(m_done), .rx_data(m_rx),
.cpol(cpol), .cpha(cpha), .lsb_first(lsb_first),
.sclk_o(m_sclk_o), .cs_n_o(m_cs_n_o), .mosi_o(m_mosi_o),
.sclk_i(sclk), .cs_n_i(cs_n), .mosi_i(mosi), .miso_i(miso),
.t_valid(m_valid), .t_mosi(m_mosi), .t_miso(m_miso),
.t_nbits(m_nbits), .t_edges(m_edges), .t_partial(m_partial)
);
wire p_valid, p_partial;
wire [DW-1:0] p_mosi, p_miso;
wire [LEN_W:0] p_nbits;
wire [CNT_W-1:0] p_edges;
wire p_busy, p_done;
wire [DW-1:0] p_rx;
spi_agent #(.ACTIVE(0), .LEAD(LEAD), .HALF(HALF), .LAG(LAG), .GAP(GAP),
.DW(DW), .LEN_W(LEN_W), .CNT_W(CNT_W)) u_observer (
.clk(clk), .rst_n(rst_n),
// A FULL REQUEST, held on the inputs of an agent that has no driver to hear it.
.start(pass_start), .tx_data(tx_data), .nbits(nbits), .fault(fault),
.busy(p_busy), .done(p_done), .rx_data(p_rx),
.cpol(cpol), .cpha(cpha), .lsb_first(lsb_first),
.sclk_o(p_sclk_o), .cs_n_o(p_cs_n_o), .mosi_o(p_mosi_o),
.sclk_i(sclk), .cs_n_i(cs_n), .mosi_i(mosi), .miso_i(miso),
.t_valid(p_valid), .t_mosi(p_mosi), .t_miso(p_miso),
.t_nbits(p_nbits), .t_edges(p_edges), .t_partial(p_partial)
);
spi_agent_flag #(.LEAD(LEAD), .HALF(HALF), .LAG(LAG), .GAP(GAP),
.DW(DW), .LEN_W(LEN_W), .CNT_W(CNT_W)) u_flag (
.clk(clk), .rst_n(rst_n),
.passive(1'b1), // held passive for the entire simulation
.gate_bug(flag_gate_bug),
.start(1'b0), .tx_data({DW{1'b0}}), .nbits(nbits), .fault(3'd0),
.cpol(cpol), .cpha(cpha), .lsb_first(lsb_first),
.sclk_o(f_sclk_o), .cs_n_o(f_cs_n_o), .mosi_o(f_mosi_o),
.sclk_i(sclk), .cs_n_i(cs_n), .mosi_i(mosi), .miso_i(miso)
);
// ------------------------------------------------------------------
// Scoring.
// ------------------------------------------------------------------
reg [DW-1:0] exp_data = {DW{1'b0}};
reg [LEN_W-1:0] exp_len = 6'd8;
wire [DW-1:0] exp_mask = ({{(DW-1){1'b0}}, 1'b1} << exp_len) - {{(DW-1){1'b0}}, 1'b1};
integer n_m = 0, n_p = 0, agree = 0, disagree = 0;
integer m_right = 0, m_wrong = 0;
// The passive agent's drive path, sampled every cycle rather than asserted once.
integer p_cycles = 0, p_not_z = 0;
integer score_en = 0;
always @(posedge clk) if (rst_n) begin
if (score_en) begin
p_cycles = p_cycles + 1;
if (p_sclk_o !== 1'bz || p_cs_n_o !== 1'bz || p_mosi_o !== 1'bz)
p_not_z = p_not_z + 1;
end
if (m_valid) begin
n_m = n_m + 1;
if ((m_mosi & exp_mask) === (exp_data & exp_mask)) m_right = m_right + 1;
else m_wrong = m_wrong + 1;
end
if (p_valid) n_p = n_p + 1;
// The two monitors must emit on the same cycle and agree on every field. Comparing them
// only when both fire would hide a passive agent that emits late or not at all, so the
// cycle-by-cycle equality of the valid signals is part of the check.
if (m_valid || p_valid) begin
if (m_valid === p_valid && m_mosi === p_mosi && m_miso === p_miso
&& m_nbits === p_nbits && m_edges === p_edges && m_partial === p_partial)
agree = agree + 1;
else
disagree = disagree + 1;
end
end
integer errors = 0;
initial begin
#400_000;
$display("FAIL: the simulation did not finish within its time limit");
$finish;
end
task automatic set_cfg(input [LEN_W-1:0] n, input integer pol, input integer pha,
input integer lsb, input [DW-1:0] d);
begin
@(negedge clk);
nbits = n; cpol = pol[0]; cpha = pha[0]; lsb_first = lsb[0];
tx_data = d; exp_data = d; exp_len = n;
repeat (6) @(negedge clk);
end
endtask
task automatic run_burst(input integer ntxn);
integer k;
begin
k = 0;
@(negedge clk);
start = 1'b1;
while (k < ntxn) begin
@(negedge clk);
if (m_done) begin
k = k + 1;
if (k == ntxn) start = 1'b0;
end
end
repeat (GAP + LAG + 8) @(negedge clk);
end
endtask
integer iw, ipol, ipha, ilsb;
reg [LEN_W-1:0] w;
integer cfgs;
integer b_right, b_wrong, clean_right, clean_wrong, bug_right, bug_wrong;
initial begin
rst_n = 1'b1;
repeat (2) @(negedge clk);
rst_n = 1'b0;
repeat (4) @(negedge clk);
rst_n = 1'b1;
repeat (4) @(negedge clk);
// A transaction request sits on the passive agent's inputs from here to the end.
pass_start = 1'b1;
score_en = 1;
// ============================================================
// 1 + 2. THE PASSIVE AGENT SEES EVERYTHING AND DRIVES NOTHING.
// ============================================================
cfgs = 0;
for (iw = 0; iw < 2; iw = iw + 1) begin
w = (iw == 0) ? 6'd8 : 6'd13;
for (ipol = 0; ipol < 2; ipol = ipol + 1)
for (ipha = 0; ipha < 2; ipha = ipha + 1)
for (ilsb = 0; ilsb < 2; ilsb = ilsb + 1) begin
set_cfg(w, ipol, ipha, ilsb, (iw == 0) ? 32'h0000_1A5C : 32'h0000_0C3A);
run_burst(2);
cfgs = cfgs + 1;
end
end
$display(" %0d transactions in %0d configurations, one active agent and two declared passive",
n_m, cfgs);
$display(" transactions the ACTIVE agent's monitor reported ....... %0d", n_m);
$display(" transactions the PASSIVE agent's monitor reported ...... %0d", n_p);
$display(" reports agreeing on every field ....................... %0d", agree);
$display(" reports disagreeing ................................... %0d", disagree);
$display(" cycles the passive agent's drive path was NOT high-Z ... %0d of %0d",
p_not_z, p_cycles);
$display(" the passive agent's busy/done, with a request held on its inputs all run: %b / %b",
p_busy, p_done);
if (n_m == 0) begin
$display(" FAIL: no transactions were observed at all, so nothing below means anything");
errors = errors + 1;
end
if (n_p != n_m || disagree != 0 || agree != n_m) begin
$display(" FAIL: the passive agent reported %0d transactions against the active agent's %0d, with %0d disagreements; passive does not mean partial",
n_p, n_m, disagree);
errors = errors + 1;
end
if (m_wrong != 0) begin
$display(" FAIL: the bus was already corrupted in the clean phase -- %0d of %0d words wrong",
m_wrong, n_m);
errors = errors + 1;
end
$display(" 1. the passive agent reported all %0d transactions, field for field identical to the active agent's own monitor, across all %0d configurations. Passive does not mean partial: an observer that misses transactions is broken, not modest",
n_p, cfgs);
if (p_not_z != 0) begin
$display(" FAIL: the passive agent's drive path left high impedance on %0d of %0d cycles",
p_not_z, p_cycles);
errors = errors + 1;
end
if (p_cycles == 0) begin
$display(" FAIL: the high-impedance check ran for zero cycles, so it measured nothing");
errors = errors + 1;
end
if (p_busy !== 1'b0 || p_done !== 1'b0) begin
$display(" FAIL: the passive agent asserted a sequencer handshake; a passive agent that reports `done` turns a wiring mistake into a passing test");
errors = errors + 1;
end
$display(" 2. and it drove nothing, measured rather than asserted: its three pin outputs were high impedance on ALL %0d cycles sampled, and its sequencer handshake never asserted although a complete transaction request sat on its inputs for the entire run. There is no driver inside it to hear the request -- the generate block that would have built one was not taken -- so the request produces nothing at all, which is why a test that waits for a passive agent should HANG rather than proceed on a fabricated handshake",
p_cycles);
// ============================================================
// 3 + 4. THE FLAG-GATED AGENT, WITH AND WITHOUT ONE MISSING GATE.
// ============================================================
b_right = m_right; b_wrong = m_wrong;
flag_gate_bug = 1'b0;
set_cfg(6'd8, 0, 0, 0, 32'h0000_1A5C);
run_burst(3);
clean_right = m_right - b_right;
clean_wrong = m_wrong - b_wrong;
b_right = m_right; b_wrong = m_wrong;
flag_gate_bug = 1'b1; // exactly one pin's gate left out
set_cfg(6'd8, 0, 0, 0, 32'h0000_1A5C);
run_burst(3);
bug_right = m_right - b_right;
bug_wrong = m_wrong - b_wrong;
$display(" the flag-gated agent, held passive throughout and never asked for a transaction:");
$display(" gates master's words correct master's words wrong");
$display(" all present %22d %20d", clean_right, clean_wrong);
$display(" one missing %22d %20d", bug_right, bug_wrong);
if (clean_wrong != 0 || clean_right == 0) begin
$display(" FAIL: with every gate present the bus should be clean -- %0d correct, %0d wrong",
clean_right, clean_wrong);
errors = errors + 1;
end
if (bug_wrong == 0) begin
$display(" FAIL: with one gate missing the bus was not corrupted, so the flag-gated agent's failure mode was not reached and measurements 3 and 4 prove nothing");
errors = errors + 1;
end
$display(" 3. with every gate present the flag-gated agent is INDISTINGUISHABLE from the structurally passive one: %0d clean words, nothing wrong, nothing to review. With exactly one gate left out -- still `passive`, still never asked for a transaction -- it corrupted %0d of %0d of the master's words. No transaction was needed for the damage: an idle driver still holds its output at a definite level, and a definite level contending with another agent's definite level is a wire carrying X",
clean_right, bug_wrong, bug_right + bug_wrong);
$display(" 4. and read the failing check: `the master's transactions do not match what the master sent`. Nothing in it mentions the observer. A bus corrupted by a component DECLARED UNABLE TO TOUCH IT produces a bug report against whatever was being tested, and that misdirection is the real cost -- not the wrong answer, but the wrong answer pointing somewhere else");
if (errors == 0)
$display("PASS: an agent is the object that knows how its driver, monitor and configuration connect, so that a second instance on a second bus costs one line instead of a paragraph -- and the interesting question about one is not what it does but what it CANNOT do. The passive agent here reported all %0d transactions in %0d configurations, field for field identical to the active agent's own monitor, because passive does not mean partial: an observer that misses transactions is broken, not modest. It drove nothing, and that was measured rather than asserted -- its three pin outputs were high impedance on all %0d cycles sampled, and its sequencer handshake never asserted although a complete transaction request sat on its inputs for the whole run, because the generate block that would have built a driver was not taken and there is nothing inside it to hear the request. Against it stood the version almost everybody writes: a driver that exists, with its outputs masked by one runtime condition per pin. With every condition present the two are INDISTINGUISHABLE -- identical clean results, nothing to review, which is exactly why the flag version survives review for years. With exactly one gate left out, the flag-gated agent -- still declared passive, still never asked for a transaction -- corrupted %0d of the master's words, because an idle driver still holds a definite level and a definite level contending with another is a wire carrying X. And the failing check read `the master's transactions do not match what the master sent`, naming nothing that had anything to do with the cause. That is the whole argument, and it generalises past SPI and past agents: WHEN A GUARANTEE MATTERS, SPEND STRUCTURE ON IT RATHER THAN CONTROL FLOW -- the same reasoning that made Chapter 16.3 put the monitor behind a modport, where an accidental drive is a compile error rather than a mystery",
n_p, cfgs, p_cycles, bug_wrong);
else
$display("FAIL: %0d error(s)", errors);
$finish;
end
endmodule// spi_agent_tb.v
//
// THREE AGENTS ON ONE BUS, AND FOUR MEASUREMENTS THAT TOGETHER SAY WHY PASSIVITY HAS TO BE
// STRUCTURAL.
//
// u_master ACTIVE. Generates all the traffic.
// u_observer PASSIVE by PARAMETER. No driver was elaborated inside it at all.
// u_flag A flag-gated agent: its driver exists and its outputs are masked by a runtime
// condition, one condition per pin. It is held `passive` throughout this bench.
// It is the version almost everybody writes.
//
// THE MEASUREMENTS.
//
// 1. THE PASSIVE AGENT SEES EVERYTHING. Its monitor reconstructs every transaction the master
// generates, field for field identical to the master's own monitor, across every
// configuration. Passive does not mean partial: an observer that misses transactions is a
// broken observer, not a modest one.
//
// 2. THE PASSIVE AGENT CANNOT DRIVE, MEASURED TWO WAYS. Its three pin outputs are high
// impedance on every single cycle of the run -- sampled and counted, not asserted -- and
// its sequencer-side handshake never asserts even when a full transaction request is held
// on its inputs throughout. Requesting a transaction from a passive agent produces nothing
// at all, which is the correct behaviour and the reason a test that waits for it should
// hang rather than proceed.
//
// 3. THE FLAG-GATED AGENT IS INDISTINGUISHABLE FROM IT -- UNTIL ONE GATE IS MISSING. With
// every gate present, the bus is clean and all three agents agree. With exactly one gate
// left out, the same agent -- still holding `passive` high, still never asked for a
// transaction -- CORRUPTS THE BUS. An idle driver still holds its output at a definite
// level, and a definite level contending with another agent's definite level is a wire
// carrying X. Measured as the master's own monitor no longer reconstructing the words the
// master sent.
//
// 4. AND THE CORRUPTION IS BLAMED ON THE WRONG COMPONENT. The failing check is a data
// mismatch on the master's transactions. Nothing in that report mentions the observer. A
// bus corrupted by a component declared unable to touch it produces a bug report against
// whatever was being tested, and this is why the guarantee is worth spending structure on
// rather than a condition: the flag version's failure is not merely a wrong answer, it is
// a wrong answer that points somewhere else.
`timescale 1ns/1ps
module spi_agent_tb;
localparam LEAD = 4;
localparam HALF = 3;
localparam LAG = 2;
localparam GAP = 3;
localparam DW = 32;
localparam LEN_W = 6;
localparam CNT_W = 10;
reg clk;
always #5 clk = ~clk;
reg rst_n;
reg start;
reg [DW-1:0] tx_data;
reg [LEN_W-1:0] nbits;
reg cpol, cpha, lsb_first;
reg [2:0] fault;
reg flag_gate_bug;
// A request held on the PASSIVE agent's sequencer inputs for the whole run, so that
// measurement 2 is about what the agent cannot do rather than about what it was not asked to
// do.
reg pass_start;
// ------------------------------------------------------------------
// The bus. Three agents drive into these wires; resolution does the rest, which is the
// point: a contended wire is X, and X is what a missing gate produces.
// ------------------------------------------------------------------
wire sclk, cs_n, mosi;
wire miso = ~mosi;
wire m_sclk_o, m_cs_n_o, m_mosi_o;
wire p_sclk_o, p_cs_n_o, p_mosi_o;
wire f_sclk_o, f_cs_n_o, f_mosi_o;
assign sclk = m_sclk_o;
assign cs_n = m_cs_n_o;
assign mosi = m_mosi_o;
assign sclk = p_sclk_o;
assign cs_n = p_cs_n_o;
assign mosi = p_mosi_o;
assign sclk = f_sclk_o;
assign cs_n = f_cs_n_o;
assign mosi = f_mosi_o;
// ------------------------------------------------------------------
wire m_valid, m_partial;
wire [DW-1:0] m_mosi, m_miso;
wire [LEN_W:0] m_nbits;
wire [CNT_W-1:0] m_edges;
wire m_busy, m_done;
wire [DW-1:0] m_rx;
spi_agent #(.ACTIVE(1), .LEAD(LEAD), .HALF(HALF), .LAG(LAG), .GAP(GAP),
.DW(DW), .LEN_W(LEN_W), .CNT_W(CNT_W)) u_master (
.clk(clk), .rst_n(rst_n),
.start(start), .tx_data(tx_data), .nbits(nbits), .fault(fault),
.busy(m_busy), .done(m_done), .rx_data(m_rx),
.cpol(cpol), .cpha(cpha), .lsb_first(lsb_first),
.sclk_o(m_sclk_o), .cs_n_o(m_cs_n_o), .mosi_o(m_mosi_o),
.sclk_i(sclk), .cs_n_i(cs_n), .mosi_i(mosi), .miso_i(miso),
.t_valid(m_valid), .t_mosi(m_mosi), .t_miso(m_miso),
.t_nbits(m_nbits), .t_edges(m_edges), .t_partial(m_partial)
);
wire p_valid, p_partial;
wire [DW-1:0] p_mosi, p_miso;
wire [LEN_W:0] p_nbits;
wire [CNT_W-1:0] p_edges;
wire p_busy, p_done;
wire [DW-1:0] p_rx;
spi_agent #(.ACTIVE(0), .LEAD(LEAD), .HALF(HALF), .LAG(LAG), .GAP(GAP),
.DW(DW), .LEN_W(LEN_W), .CNT_W(CNT_W)) u_observer (
.clk(clk), .rst_n(rst_n),
// A FULL REQUEST, held on the inputs of an agent that has no driver to hear it.
.start(pass_start), .tx_data(tx_data), .nbits(nbits), .fault(fault),
.busy(p_busy), .done(p_done), .rx_data(p_rx),
.cpol(cpol), .cpha(cpha), .lsb_first(lsb_first),
.sclk_o(p_sclk_o), .cs_n_o(p_cs_n_o), .mosi_o(p_mosi_o),
.sclk_i(sclk), .cs_n_i(cs_n), .mosi_i(mosi), .miso_i(miso),
.t_valid(p_valid), .t_mosi(p_mosi), .t_miso(p_miso),
.t_nbits(p_nbits), .t_edges(p_edges), .t_partial(p_partial)
);
spi_agent_flag #(.LEAD(LEAD), .HALF(HALF), .LAG(LAG), .GAP(GAP),
.DW(DW), .LEN_W(LEN_W), .CNT_W(CNT_W)) u_flag (
.clk(clk), .rst_n(rst_n),
.passive(1'b1), // held passive for the entire simulation
.gate_bug(flag_gate_bug),
.start(1'b0), .tx_data({DW{1'b0}}), .nbits(nbits), .fault(3'd0),
.cpol(cpol), .cpha(cpha), .lsb_first(lsb_first),
.sclk_o(f_sclk_o), .cs_n_o(f_cs_n_o), .mosi_o(f_mosi_o),
.sclk_i(sclk), .cs_n_i(cs_n), .mosi_i(mosi), .miso_i(miso)
);
// ------------------------------------------------------------------
// Scoring.
// ------------------------------------------------------------------
reg [DW-1:0] exp_data;
reg [LEN_W-1:0] exp_len;
wire [DW-1:0] exp_mask = ({{(DW-1){1'b0}}, 1'b1} << exp_len) - {{(DW-1){1'b0}}, 1'b1};
integer n_m, n_p, agree, disagree;
integer m_right, m_wrong;
// The passive agent's drive path, sampled every cycle rather than asserted once.
integer p_cycles, p_not_z;
integer score_en;
always @(posedge clk) if (rst_n) begin
if (score_en) begin
p_cycles = p_cycles + 1;
if (p_sclk_o !== 1'bz || p_cs_n_o !== 1'bz || p_mosi_o !== 1'bz)
p_not_z = p_not_z + 1;
end
if (m_valid) begin
n_m = n_m + 1;
if ((m_mosi & exp_mask) === (exp_data & exp_mask)) m_right = m_right + 1;
else m_wrong = m_wrong + 1;
end
if (p_valid) n_p = n_p + 1;
// The two monitors must emit on the same cycle and agree on every field. Comparing them
// only when both fire would hide a passive agent that emits late or not at all, so the
// cycle-by-cycle equality of the valid signals is part of the check.
if (m_valid || p_valid) begin
if (m_valid === p_valid && m_mosi === p_mosi && m_miso === p_miso
&& m_nbits === p_nbits && m_edges === p_edges && m_partial === p_partial)
agree = agree + 1;
else
disagree = disagree + 1;
end
end
integer errors;
initial begin
#400_000;
$display("FAIL: the simulation did not finish within its time limit");
$finish;
end
task set_cfg;
input [LEN_W-1:0] n;
input integer pol;
input integer pha;
input integer lsb;
input [DW-1:0] d;
begin
@(negedge clk);
nbits = n; cpol = pol[0]; cpha = pha[0]; lsb_first = lsb[0];
tx_data = d; exp_data = d; exp_len = n;
repeat (6) @(negedge clk);
end
endtask
task run_burst;
input integer ntxn;
integer k;
begin
k = 0;
@(negedge clk);
start = 1'b1;
while (k < ntxn) begin
@(negedge clk);
if (m_done) begin
k = k + 1;
if (k == ntxn) start = 1'b0;
end
end
repeat (GAP + LAG + 8) @(negedge clk);
end
endtask
integer iw, ipol, ipha, ilsb;
reg [LEN_W-1:0] w;
integer cfgs;
integer b_right, b_wrong, clean_right, clean_wrong, bug_right, bug_wrong;
initial begin
rst_n = 1'b1;
repeat (2) @(negedge clk);
rst_n = 1'b0;
repeat (4) @(negedge clk);
rst_n = 1'b1;
repeat (4) @(negedge clk);
// A transaction request sits on the passive agent's inputs from here to the end.
pass_start = 1'b1;
score_en = 1;
// ============================================================
// 1 + 2. THE PASSIVE AGENT SEES EVERYTHING AND DRIVES NOTHING.
// ============================================================
cfgs = 0;
for (iw = 0; iw < 2; iw = iw + 1) begin
w = (iw == 0) ? 6'd8 : 6'd13;
for (ipol = 0; ipol < 2; ipol = ipol + 1)
for (ipha = 0; ipha < 2; ipha = ipha + 1)
for (ilsb = 0; ilsb < 2; ilsb = ilsb + 1) begin
set_cfg(w, ipol, ipha, ilsb, (iw == 0) ? 32'h0000_1A5C : 32'h0000_0C3A);
run_burst(2);
cfgs = cfgs + 1;
end
end
$display(" %0d transactions in %0d configurations, one active agent and two declared passive",
n_m, cfgs);
$display(" transactions the ACTIVE agent's monitor reported ....... %0d", n_m);
$display(" transactions the PASSIVE agent's monitor reported ...... %0d", n_p);
$display(" reports agreeing on every field ....................... %0d", agree);
$display(" reports disagreeing ................................... %0d", disagree);
$display(" cycles the passive agent's drive path was NOT high-Z ... %0d of %0d",
p_not_z, p_cycles);
$display(" the passive agent's busy/done, with a request held on its inputs all run: %b / %b",
p_busy, p_done);
if (n_m == 0) begin
$display(" FAIL: no transactions were observed at all, so nothing below means anything");
errors = errors + 1;
end
if (n_p != n_m || disagree != 0 || agree != n_m) begin
$display(" FAIL: the passive agent reported %0d transactions against the active agent's %0d, with %0d disagreements; passive does not mean partial",
n_p, n_m, disagree);
errors = errors + 1;
end
if (m_wrong != 0) begin
$display(" FAIL: the bus was already corrupted in the clean phase -- %0d of %0d words wrong",
m_wrong, n_m);
errors = errors + 1;
end
$display(" 1. the passive agent reported all %0d transactions, field for field identical to the active agent's own monitor, across all %0d configurations. Passive does not mean partial: an observer that misses transactions is broken, not modest",
n_p, cfgs);
if (p_not_z != 0) begin
$display(" FAIL: the passive agent's drive path left high impedance on %0d of %0d cycles",
p_not_z, p_cycles);
errors = errors + 1;
end
if (p_cycles == 0) begin
$display(" FAIL: the high-impedance check ran for zero cycles, so it measured nothing");
errors = errors + 1;
end
if (p_busy !== 1'b0 || p_done !== 1'b0) begin
$display(" FAIL: the passive agent asserted a sequencer handshake; a passive agent that reports `done` turns a wiring mistake into a passing test");
errors = errors + 1;
end
$display(" 2. and it drove nothing, measured rather than asserted: its three pin outputs were high impedance on ALL %0d cycles sampled, and its sequencer handshake never asserted although a complete transaction request sat on its inputs for the entire run. There is no driver inside it to hear the request -- the generate block that would have built one was not taken -- so the request produces nothing at all, which is why a test that waits for a passive agent should HANG rather than proceed on a fabricated handshake",
p_cycles);
// ============================================================
// 3 + 4. THE FLAG-GATED AGENT, WITH AND WITHOUT ONE MISSING GATE.
// ============================================================
b_right = m_right; b_wrong = m_wrong;
flag_gate_bug = 1'b0;
set_cfg(6'd8, 0, 0, 0, 32'h0000_1A5C);
run_burst(3);
clean_right = m_right - b_right;
clean_wrong = m_wrong - b_wrong;
b_right = m_right; b_wrong = m_wrong;
flag_gate_bug = 1'b1; // exactly one pin's gate left out
set_cfg(6'd8, 0, 0, 0, 32'h0000_1A5C);
run_burst(3);
bug_right = m_right - b_right;
bug_wrong = m_wrong - b_wrong;
$display(" the flag-gated agent, held passive throughout and never asked for a transaction:");
$display(" gates master's words correct master's words wrong");
$display(" all present %22d %20d", clean_right, clean_wrong);
$display(" one missing %22d %20d", bug_right, bug_wrong);
if (clean_wrong != 0 || clean_right == 0) begin
$display(" FAIL: with every gate present the bus should be clean -- %0d correct, %0d wrong",
clean_right, clean_wrong);
errors = errors + 1;
end
if (bug_wrong == 0) begin
$display(" FAIL: with one gate missing the bus was not corrupted, so the flag-gated agent's failure mode was not reached and measurements 3 and 4 prove nothing");
errors = errors + 1;
end
$display(" 3. with every gate present the flag-gated agent is INDISTINGUISHABLE from the structurally passive one: %0d clean words, nothing wrong, nothing to review. With exactly one gate left out -- still `passive`, still never asked for a transaction -- it corrupted %0d of %0d of the master's words. No transaction was needed for the damage: an idle driver still holds its output at a definite level, and a definite level contending with another agent's definite level is a wire carrying X",
clean_right, bug_wrong, bug_right + bug_wrong);
$display(" 4. and read the failing check: `the master's transactions do not match what the master sent`. Nothing in it mentions the observer. A bus corrupted by a component DECLARED UNABLE TO TOUCH IT produces a bug report against whatever was being tested, and that misdirection is the real cost -- not the wrong answer, but the wrong answer pointing somewhere else");
if (errors == 0)
$display("PASS: an agent is the object that knows how its driver, monitor and configuration connect, so that a second instance on a second bus costs one line instead of a paragraph -- and the interesting question about one is not what it does but what it CANNOT do. The passive agent here reported all %0d transactions in %0d configurations, field for field identical to the active agent's own monitor, because passive does not mean partial: an observer that misses transactions is broken, not modest. It drove nothing, and that was measured rather than asserted -- its three pin outputs were high impedance on all %0d cycles sampled, and its sequencer handshake never asserted although a complete transaction request sat on its inputs for the whole run, because the generate block that would have built a driver was not taken and there is nothing inside it to hear the request. Against it stood the version almost everybody writes: a driver that exists, with its outputs masked by one runtime condition per pin. With every condition present the two are INDISTINGUISHABLE -- identical clean results, nothing to review, which is exactly why the flag version survives review for years. With exactly one gate left out, the flag-gated agent -- still declared passive, still never asked for a transaction -- corrupted %0d of the master's words, because an idle driver still holds a definite level and a definite level contending with another is a wire carrying X. And the failing check read `the master's transactions do not match what the master sent`, naming nothing that had anything to do with the cause. That is the whole argument, and it generalises past SPI and past agents: WHEN A GUARANTEE MATTERS, SPEND STRUCTURE ON IT RATHER THAN CONTROL FLOW -- the same reasoning that made Chapter 16.3 put the monitor behind a modport, where an accidental drive is a compile error rather than a mystery",
n_p, cfgs, p_cycles, bug_wrong);
else
$display("FAIL: %0d error(s)", errors);
$finish;
end
initial begin
cpol = 1'b0;
cpha = 1'b0;
lsb_first = 1'b0;
n_m = 0;
n_p = 0;
agree = 0;
disagree = 0;
m_right = 0;
m_wrong = 0;
p_cycles = 0;
p_not_z = 0;
clk = 1'b0;
rst_n = 1'b1;
start = 1'b0;
tx_data = {DW{1'b0}};
nbits = 6'd8;
fault = 3'd0;
flag_gate_bug = 1'b0;
pass_start = 1'b0;
exp_data = {DW{1'b0}};
exp_len = 6'd8;
score_en = 0;
errors = 0;
end
endmodule-- spi_agent_tb.vhd
--
-- THREE AGENTS ON ONE BUS, AND FOUR MEASUREMENTS THAT TOGETHER SAY WHY PASSIVITY HAS TO BE
-- STRUCTURAL.
--
-- u_master ACTIVE. Generates all the traffic.
-- u_observer PASSIVE by GENERIC. No driver was elaborated inside it at all.
-- u_flag A flag-gated agent: its driver exists and its outputs are masked by a runtime
-- condition, one condition per pin. Held `passive` throughout this bench. It is the
-- version almost everybody writes.
--
-- THE MEASUREMENTS.
--
-- 1. THE PASSIVE AGENT SEES EVERYTHING. Its monitor reconstructs every transaction the master
-- generates, field for field identical to the master's own monitor, across every
-- configuration. Passive does not mean partial: an observer that misses transactions is a
-- broken observer, not a modest one.
--
-- 2. THE PASSIVE AGENT CANNOT DRIVE, MEASURED TWO WAYS. Its three pin outputs are 'Z' on every
-- cycle of the run -- sampled and counted, not asserted -- and its sequencer handshake never
-- asserts even with a full transaction request held on its inputs throughout. Requesting a
-- transaction from a passive agent produces nothing at all, which is why a test that waits
-- for one should hang rather than proceed on a fabricated handshake.
--
-- 3. THE FLAG-GATED AGENT IS INDISTINGUISHABLE FROM IT -- UNTIL ONE GATE IS MISSING. With every
-- gate present the bus is clean. With exactly one gate left out, the same agent -- still
-- `passive`, still never asked for a transaction -- CORRUPTS THE BUS, because an idle driver
-- still holds its output at a definite level and a definite level contending with another
-- agent's definite level is a wire carrying 'X'.
--
-- 4. AND THE CORRUPTION IS BLAMED ON THE WRONG COMPONENT. The failing check is a data mismatch
-- on the master's transactions, and nothing in that report mentions the observer.
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
use work.spi_driver_pkg.all;
use work.spi_txn_pkg.all;
entity spi_agent_tb is
end entity spi_agent_tb;
architecture tb of spi_agent_tb is
constant LEAD_C : natural := 4;
constant HALF_C : natural := 3;
constant LAG_C : natural := 2;
constant GAP_C : natural := 3;
constant HALF_T : time := 5 ns;
signal clk : std_logic := '0';
signal rst_n : std_logic := '1';
signal done_sim : boolean := false;
signal start : std_logic := '0';
signal pass_start : std_logic := '0';
signal req : spi_req_t := (data => (others => '0'),
nbits => to_unsigned(8, LEN_W),
cpol => '0',
cpha => '0',
lsb_first => '0',
fault => F_NONE);
signal flag_gate_bug : std_logic := '0';
-- The bus. Three agents drive into these signals; resolution does the rest, which is the
-- point: a contended wire is 'X', and 'X' is what a missing gate produces.
signal sclk, cs_n, mosi : std_logic;
signal miso : std_logic;
signal m_sclk_o, m_cs_n_o, m_mosi_o : std_logic;
signal p_sclk_o, p_cs_n_o, p_mosi_o : std_logic;
signal f_sclk_o, f_cs_n_o, f_mosi_o : std_logic;
signal m_busy, m_done : std_logic;
signal p_busy, p_done : std_logic;
signal m_rx, p_rx : std_logic_vector(DW - 1 downto 0);
signal obs_m, obs_p : spi_obs_t;
signal exp_data : std_logic_vector(DW - 1 downto 0) := (others => '0');
signal exp_len : natural := 8;
signal score_en : boolean := false;
type tally_t is protected
procedure cycle (not_z : boolean);
procedure master_txn (ok : boolean);
procedure passive_txn;
procedure pair (same : boolean);
impure function n_m return integer;
impure function n_p return integer;
impure function agree return integer;
impure function disagree return integer;
impure function m_right return integer;
impure function m_wrong return integer;
impure function cycles return integer;
impure function not_z_n return integer;
end protected tally_t;
type tally_t is protected body
variable v_nm, v_np, v_ag, v_dis, v_r, v_w, v_cy, v_nz : integer := 0;
procedure cycle (not_z : boolean) is
begin
v_cy := v_cy + 1;
if not_z then v_nz := v_nz + 1; end if;
end procedure;
procedure master_txn (ok : boolean) is
begin
v_nm := v_nm + 1;
if ok then v_r := v_r + 1; else v_w := v_w + 1; end if;
end procedure;
procedure passive_txn is
begin
v_np := v_np + 1;
end procedure;
procedure pair (same : boolean) is
begin
if same then v_ag := v_ag + 1; else v_dis := v_dis + 1; end if;
end procedure;
impure function n_m return integer is begin return v_nm; end function;
impure function n_p return integer is begin return v_np; end function;
impure function agree return integer is begin return v_ag; end function;
impure function disagree return integer is begin return v_dis; end function;
impure function m_right return integer is begin return v_r; end function;
impure function m_wrong return integer is begin return v_w; end function;
impure function cycles return integer is begin return v_cy; end function;
impure function not_z_n return integer is begin return v_nz; end function;
end protected body tally_t;
shared variable tally : tally_t;
signal errors : integer := 0;
begin
miso <= not mosi;
sclk <= m_sclk_o;
cs_n <= m_cs_n_o;
mosi <= m_mosi_o;
sclk <= p_sclk_o;
cs_n <= p_cs_n_o;
mosi <= p_mosi_o;
sclk <= f_sclk_o;
cs_n <= f_cs_n_o;
mosi <= f_mosi_o;
clk_gen : process is
begin
while not done_sim loop
wait for HALF_T;
clk <= not clk;
end loop;
wait;
end process clk_gen;
u_master : entity work.spi_agent
generic map (ACTIVE => 1, LEAD => LEAD_C, HALF => HALF_C, LAG => LAG_C, GAP => GAP_C)
port map (clk => clk, rst_n => rst_n, start => start, req => req,
busy => m_busy, done => m_done, rx_data => m_rx,
sclk_o => m_sclk_o, cs_n_o => m_cs_n_o, mosi_o => m_mosi_o,
sclk_i => sclk, cs_n_i => cs_n, mosi_i => mosi, miso_i => miso,
obs => obs_m);
-- A FULL REQUEST, held on the inputs of an agent that has no driver to hear it.
u_observer : entity work.spi_agent
generic map (ACTIVE => 0, LEAD => LEAD_C, HALF => HALF_C, LAG => LAG_C, GAP => GAP_C)
port map (clk => clk, rst_n => rst_n, start => pass_start, req => req,
busy => p_busy, done => p_done, rx_data => p_rx,
sclk_o => p_sclk_o, cs_n_o => p_cs_n_o, mosi_o => p_mosi_o,
sclk_i => sclk, cs_n_i => cs_n, mosi_i => mosi, miso_i => miso,
obs => obs_p);
u_flag : entity work.spi_agent_flag
generic map (LEAD => LEAD_C, HALF => HALF_C, LAG => LAG_C, GAP => GAP_C)
port map (clk => clk, rst_n => rst_n,
passive => '1', -- held passive for the entire simulation
gate_bug => flag_gate_bug,
start => '0',
req => (data => (others => '0'), nbits => req.nbits,
cpol => req.cpol, cpha => req.cpha,
lsb_first => req.lsb_first, fault => F_NONE),
sclk_o => f_sclk_o, cs_n_o => f_cs_n_o, mosi_o => f_mosi_o,
sclk_i => sclk, cs_n_i => cs_n, mosi_i => mosi, miso_i => miso);
score_proc : process (clk) is
variable mask : std_logic_vector(DW - 1 downto 0);
begin
if rising_edge(clk) and rst_n = '1' then
if score_en then
tally.cycle(p_sclk_o /= 'Z' or p_cs_n_o /= 'Z' or p_mosi_o /= 'Z');
end if;
mask := std_logic_vector(shift_left(to_unsigned(1, DW), exp_len) - 1);
if obs_m.valid = '1' then
tally.master_txn((obs_m.mosi and mask) = (exp_data and mask));
end if;
if obs_p.valid = '1' then
tally.passive_txn;
end if;
-- The two monitors must emit on the same cycle and agree on every field. Comparing
-- them only when both fire would hide a passive agent that emits late or not at all,
-- so the cycle-by-cycle equality of the valid signals is part of the check.
if obs_m.valid = '1' or obs_p.valid = '1' then
tally.pair(obs_m.valid = obs_p.valid and obs_m.mosi = obs_p.mosi
and obs_m.miso = obs_p.miso and obs_m.nbits = obs_p.nbits
and obs_m.edges = obs_p.edges and obs_m.partial = obs_p.partial);
end if;
end if;
end process score_proc;
main : process is
procedure set_cfg (n : natural; pol : std_logic; pha : std_logic; lsb : std_logic;
d : std_logic_vector(DW - 1 downto 0)) is
begin
wait until falling_edge(clk);
req <= (data => d, nbits => to_unsigned(n, LEN_W),
cpol => pol, cpha => pha, lsb_first => lsb, fault => F_NONE);
exp_data <= d;
exp_len <= n;
for i in 0 to 5 loop wait until falling_edge(clk); end loop;
end procedure set_cfg;
procedure run_burst (ntxn : natural) is
variable k : natural := 0;
begin
k := 0;
wait until falling_edge(clk);
start <= '1';
while k < ntxn loop
wait until falling_edge(clk);
if m_done = '1' then
k := k + 1;
if k = ntxn then start <= '0'; end if;
end if;
end loop;
for i in 0 to GAP_C + LAG_C + 7 loop wait until falling_edge(clk); end loop;
end procedure run_burst;
constant PAT_A : std_logic_vector(DW - 1 downto 0) := x"00001A5C";
constant PAT_B : std_logic_vector(DW - 1 downto 0) := x"00000C3A";
variable w : natural;
variable cfgs : integer := 0;
variable b_r, b_w : integer;
variable clean_r, clean_w : integer;
variable bug_r, bug_w : integer;
variable pol, pha, lsb : std_logic;
begin
rst_n <= '1';
for i in 0 to 1 loop wait until falling_edge(clk); end loop;
rst_n <= '0';
for i in 0 to 3 loop wait until falling_edge(clk); end loop;
rst_n <= '1';
for i in 0 to 3 loop wait until falling_edge(clk); end loop;
-- A transaction request sits on the passive agent's inputs from here to the end.
pass_start <= '1';
score_en <= true;
-- ==============================================================
-- 1 + 2. THE PASSIVE AGENT SEES EVERYTHING AND DRIVES NOTHING.
-- ==============================================================
for iw in 0 to 1 loop
if iw = 0 then w := 8; else w := 13; end if;
for ipol in 0 to 1 loop
for ipha in 0 to 1 loop
for ilsb in 0 to 1 loop
if ipol = 0 then pol := '0'; else pol := '1'; end if;
if ipha = 0 then pha := '0'; else pha := '1'; end if;
if ilsb = 0 then lsb := '0'; else lsb := '1'; end if;
if iw = 0 then
set_cfg(w, pol, pha, lsb, PAT_A);
else
set_cfg(w, pol, pha, lsb, PAT_B);
end if;
run_burst(2);
cfgs := cfgs + 1;
end loop;
end loop;
end loop;
end loop;
report " " & integer'image(tally.n_m) & " transactions in " & integer'image(cfgs) &
" configurations, one active agent and two declared passive";
report " transactions the ACTIVE agent's monitor reported ....... " &
integer'image(tally.n_m);
report " transactions the PASSIVE agent's monitor reported ...... " &
integer'image(tally.n_p);
report " reports agreeing on every field ....................... " &
integer'image(tally.agree);
report " reports disagreeing ................................... " &
integer'image(tally.disagree);
report " cycles the passive agent's drive path was NOT high-Z ... " &
integer'image(tally.not_z_n) & " of " & integer'image(tally.cycles);
report " the passive agent's busy/done, with a request held on its inputs all run: " &
std_logic'image(p_busy) & " / " & std_logic'image(p_done);
if tally.n_m = 0 then
report " FAIL: no transactions were observed at all, so nothing below means anything";
errors <= errors + 1; wait for 1 ns;
end if;
if tally.n_p /= tally.n_m or tally.disagree /= 0 or tally.agree /= tally.n_m then
report " FAIL: the passive agent reported " & integer'image(tally.n_p) &
" transactions against the active agent's " & integer'image(tally.n_m) &
", with " & integer'image(tally.disagree) &
" disagreements; passive does not mean partial";
errors <= errors + 1; wait for 1 ns;
end if;
if tally.m_wrong /= 0 then
report " FAIL: the bus was already corrupted in the clean phase -- " &
integer'image(tally.m_wrong) & " words wrong";
errors <= errors + 1; wait for 1 ns;
end if;
report " 1. the passive agent reported all " & integer'image(tally.n_p) &
" transactions, field for field identical to the active agent's own monitor, across all " &
integer'image(cfgs) &
" configurations. Passive does not mean partial: an observer that misses transactions is broken, not modest";
if tally.not_z_n /= 0 then
report " FAIL: the passive agent's drive path left high impedance on " &
integer'image(tally.not_z_n) & " of " & integer'image(tally.cycles) & " cycles";
errors <= errors + 1; wait for 1 ns;
end if;
if tally.cycles = 0 then
report " FAIL: the high-impedance check ran for zero cycles, so it measured nothing";
errors <= errors + 1; wait for 1 ns;
end if;
if p_busy /= '0' or p_done /= '0' then
report " FAIL: the passive agent asserted a sequencer handshake; a passive agent that reports `done` turns a wiring mistake into a passing test";
errors <= errors + 1; wait for 1 ns;
end if;
report " 2. and it drove nothing, measured rather than asserted: its three pin outputs were 'Z' on ALL " &
integer'image(tally.cycles) &
" cycles sampled, and its sequencer handshake never asserted although a complete transaction request sat on its inputs for the entire run. There is no driver inside it to hear the request -- the generate branch that would have built one was not taken -- so the request produces nothing at all, which is why a test that waits for a passive agent should HANG rather than proceed on a fabricated handshake";
-- ==============================================================
-- 3 + 4. THE FLAG-GATED AGENT, WITH AND WITHOUT ONE MISSING GATE.
-- ==============================================================
b_r := tally.m_right; b_w := tally.m_wrong;
flag_gate_bug <= '0';
set_cfg(8, '0', '0', '0', PAT_A);
run_burst(3);
clean_r := tally.m_right - b_r;
clean_w := tally.m_wrong - b_w;
b_r := tally.m_right; b_w := tally.m_wrong;
flag_gate_bug <= '1'; -- exactly one pin's gate left out
set_cfg(8, '0', '0', '0', PAT_A);
run_burst(3);
bug_r := tally.m_right - b_r;
bug_w := tally.m_wrong - b_w;
report " the flag-gated agent, held passive throughout and never asked for a transaction:";
report " gates master's words correct master's words wrong";
report " all present " & integer'image(clean_r) & " " & integer'image(clean_w);
report " one missing " & integer'image(bug_r) & " " & integer'image(bug_w);
if clean_w /= 0 or clean_r = 0 then
report " FAIL: with every gate present the bus should be clean -- " &
integer'image(clean_r) & " correct, " & integer'image(clean_w) & " wrong";
errors <= errors + 1; wait for 1 ns;
end if;
if bug_w = 0 then
report " FAIL: with one gate missing the bus was not corrupted, so the flag-gated agent's failure mode was not reached and measurements 3 and 4 prove nothing";
errors <= errors + 1; wait for 1 ns;
end if;
report " 3. with every gate present the flag-gated agent is INDISTINGUISHABLE from the structurally passive one: " &
integer'image(clean_r) &
" clean words, nothing wrong, nothing to review. With exactly one gate left out -- still `passive`, still never asked for a transaction -- it corrupted " &
integer'image(bug_w) & " of " & integer'image(bug_r + bug_w) &
" of the master's words. No transaction was needed for the damage: an idle driver still holds its output at a definite level, and a definite level contending with another agent's definite level is a wire carrying 'X'";
report " 4. and read the failing check: `the master's transactions do not match what the master sent`. Nothing in it mentions the observer. A bus corrupted by a component DECLARED UNABLE TO TOUCH IT produces a bug report against whatever was being tested, and that misdirection is the real cost -- not the wrong answer, but the wrong answer pointing somewhere else";
wait for 1 ns;
if errors = 0 then
report "PASS: an agent is the object that knows how its driver, monitor and configuration connect, so that a second instance on a second bus costs one line instead of a paragraph -- and the interesting question about one is not what it does but what it CANNOT do. The passive agent here reported all " &
integer'image(tally.n_p) & " transactions in " & integer'image(cfgs) &
" configurations, field for field identical to the active agent's own monitor, because passive does not mean partial: an observer that misses transactions is broken, not modest. It drove nothing, and that was measured rather than asserted -- its three pin outputs were 'Z' on all " &
integer'image(tally.cycles) &
" cycles sampled, and its sequencer handshake never asserted although a complete transaction request sat on its inputs for the whole run, because the generate branch that would have built a driver was not taken and there is nothing inside it to hear the request. Against it stood the version almost everybody writes: a driver that exists, with its outputs masked by one runtime condition per pin. With every condition present the two are INDISTINGUISHABLE -- identical clean results, nothing to review, which is exactly why the flag version survives review for years. With exactly one gate left out, the flag-gated agent -- still declared passive, still never asked for a transaction -- corrupted " &
integer'image(bug_w) &
" of the master's words, because an idle driver still holds a definite level and a definite level contending with another is a wire carrying 'X'. And the failing check read `the master's transactions do not match what the master sent`, naming nothing that had anything to do with the cause. That is the whole argument, and it generalises past SPI and past agents: WHEN A GUARANTEE MATTERS, SPEND STRUCTURE ON IT RATHER THAN CONTROL FLOW -- the same reasoning that made Chapter 16.3 put the monitor behind a modport in SystemVerilog, and that VHDL applies by default through the `in` port mode, where an accidental drive is an analysis error rather than a mystery"
severity note;
else
report "FAIL: " & integer'image(errors) & " error(s)" severity error;
end if;
done_sim <= true;
wait for 100 ns;
std.env.stop;
end process main;
end architecture tb;7. Why a Verification Engineer Cares
Because the bug in section 4 is found by the wrong team.
A corrupted bus produces failures in whatever was being tested, so the investigation starts at the design, moves to the driver, and reaches the observer last — if it reaches it at all. The cost is measured in engineer-days spent proving that correct things are correct.
The habit that prevents it: ask what a component is structurally incapable of, not what it is configured not to do. For a passive agent that means checking whether a driver was constructed, not whether a flag is set. For a monitor it means checking the modport, not the body. Both questions are answerable in seconds and neither requires running anything.
And the passivity measurement is worth copying as a technique. Sampling a component's drive path every cycle and requiring it to be high impedance is three lines of bench code and turns an architectural claim into a number.
8. Why an FPGA or ASIC Engineer Cares
Because the same argument decides how a real multi-master or multi-slave bus behaves, and the failure looks identical.
Two drivers on one wire is a contention, and on silicon it is current rather than an X. A device whose output enable is gated by a condition rather than by a structural property — a tri-state buffer whose enable term forgets one case — produces exactly the bug in section 4 with heat instead of a simulation artefact. The design rule is the same: the enable for a shared net should be impossible to get wrong for one pin of a group, which usually means generating the group together rather than writing per-pin conditions.
And the idle-driver observation generalises: a driver that has not been asked to do anything is still driving. Reset values are output values. A block that holds its bus outputs at a definite level while "inactive" is contending with whatever else owns that bus, and "inactive" is not "absent".
9. Failure Signature — A Bug Report Against The Component That Works
Symptom a block fails integration. Its own block-level suite is
clean and its author cannot reproduce the failure in
isolation. In the integrated environment it fails
consistently.
What happened a component declared passive is driving one pin of the
shared bus. Its driver was built and gated per pin, and one
pin's gate was missing -- added later, copied wrong, or
written for a bus with fewer signals.
What would have a per-cycle check that every declared-passive component's
caught it drive path is high impedance, and a structural rule that
passivity removes the driver rather than masking it.
The tell the failure appears only in the integrated environment and
the failing check names a component that passes in
isolation. When a block is clean alone and broken in
company, suspect the bus before suspecting the block --
and count the things connected to it that are allowed to
drive.10. Common Misconceptions
"A passive agent is an agent with driving switched off." That is the flag version, and section 4 measures what one missing condition costs. A passive agent should have no driver to switch off.
"Passive means it observes less." It observes exactly the same traffic — 32 of 32, field for field identical here. An observer that misses transactions is broken, not modest.
"A passive agent needs no sequencer connection, so leaving one wired is harmless." A passive agent with a sequencer is a wiring mistake that will one day be exercised. The UVM version makes it fatal at end-of-elaboration for that reason.
"An idle driver drives nothing." An idle driver holds its outputs at their reset values, which are definite levels. Contention needs no transaction.
"A tri-state gate on each pin is equivalent to not having a driver." Only if every gate is present and correct, which is a property of a person rather than of the design. The gates are indistinguishable from the structural version right up until one of them is missing — which is the measurement in section 4.
"The corrupted-bus failure would obviously point at the observer." It points at whatever was being tested, because that is what the failing check is about. The observer appears nowhere in the report.
11. Reason It Through
The flag-gated agent was never asked for a transaction and still corrupted every word. Explain why, without referring to any transfer.
Its driver was elaborated and has been in its reset state since time zero, holding mosi at a definite 0. Once that output reaches the bus, every bit the master drives as 1 is a contention and resolves to X. Driving requires no transaction; it requires only that the output exists and is connected.
Why is the passive agent's done tied low rather than left unconnected or driven from something plausible?
Because a false handshake converts a wiring mistake into a passing test. A test that waits on a passive agent's done should hang, so that the mistake is found on the first run rather than credited as a result.
The two monitors' reports are compared cycle by cycle, including the equality of their valid signals. Why is comparing only the transactions insufficient?
Because a passive agent that emits late, or emits fewer transactions, would still produce matching fields for the transactions it did emit. Requiring the valid signals to agree every cycle is what makes "reported all 32" a statement about timing as well as content.
Give the version of section 5's principle that applies to a design rather than to a testbench.
An output enable for a shared net should be impossible to get wrong for one pin of a group — generate the group together, from one term, rather than writing a condition per pin. The failure mode of per-pin conditions is a bus that one block drives while claiming not to, and on silicon that is current rather than an X.
A block passes its own suite and fails integration. What should be checked before the block?
Everything connected to its bus that is allowed to drive. A block that is clean alone and broken in company is evidence about the bus, not about the block, and the cheapest first question is how many components on that bus have a driver at all.
12. Understanding Check
13. Summary
An agent is the object that knows how a driver, a monitor and their shared configuration connect, so that a second instance on a second bus costs one line instead of a paragraph — and the interesting question about one is what it cannot do. The structurally passive agent here reported all 32 transactions in sixteen configurations, field for field identical to the active agent's own monitor, because passive does not mean partial. It drove nothing, measured rather than asserted: high impedance on all 2512 cycles sampled, and a sequencer handshake that never asserted although a complete transaction request sat on its inputs for the whole run, because the generate branch that would have built a driver was not taken. Against it stood the version almost everybody writes — a driver that exists behind one runtime condition per pin — and with every condition present the two were indistinguishable. With exactly one gate left out, the flag-gated agent, still declared passive and still never asked for a transaction, corrupted every word the master sent, because an idle driver holds a definite level and a definite level contending with another is a wire carrying X. And the failing check named the component that was working. When a guarantee matters, spend structure on it rather than control flow.
14. What Comes Next
The environment is complete: a plan with checkers and exercise evidence, a transaction object that can reach the space, an interface that holds the sampling discipline, a driver that owns the timing, a monitor that reads only pins, an independent model with a scoreboard behind it, and agents whose passivity is structural. Module 17 puts it to work — sequences, tests, and the coverage closure argument that decides when a protocol is verified rather than merely exercised.
Continue learning
Related tutorials
- Related topic
A Reusable UVM SPI Agent
Three arbitration policies deliver identical items and identical functional coverage, and completely different traffic: round robin switches between sequences 23 times and strict priority once. A coverage report cannot tell you which one ran, so it cannot tell you whether two sequences ever overlapped.
- Related topic
Full-Duplex Exchange
Every SPI transfer moves a bit in both directions on every edge, whether the software wanted it to or not. Where dummy bytes come from, why bytes received during a command phase exist but mean nothing, and why read and write are interpretations rather than modes.
- Related topic
Extracting Protocol Rules and the Verification Plan
Eight pin-observable SPI rules, each with a checker and an exercised counter, because a checker alone cannot tell never-broken from never-reached. Legal traffic violates nothing and exercises all eight; eight injected faults produce a diagonal violation matrix; and one plan row is proved to have no checker at all.
- Related topic
Driver Architecture
A driver owns every timing number in the protocol, so it is the one component that must be checked against something not written to agree with it. Thirty-two legal transactions violate nothing and exercise all eight rules; seven injected faults fire exactly the rules predicted.
