Skip to content
VLSI Mentor

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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.

A shared SPI bus with an active agent containing a driver and monitor, a structurally passive agent containing only a monitor with constant high-impedance outputs, and a flag-gated agent whose driver exists behind per-pin conditionssequencerdrivermonitorSCLK, CS, MOSIpassive: monitorpassive: outputsflag: driverone gate per pinanalysis12
Figure 1 — one bus, three agents. The active agent's driver is elaborated; the structurally passive agent's is not, so its pin outputs are constants decided before the simulation starts. The flag-gated agent's driver exists and is masked by one runtime condition per pin, which is the arrangement the measurement in section 4 breaks with a single missing gate.

2. Two Ways To Build A Passive Agent

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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
     touch

The 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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 / 0

Field 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   gates        master's words correct   master's words wrong
   all present                       3                      0
   one missing                       0                      3

With 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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 X

The 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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

Azvya Education Pvt. Ltd.VLSI Mentor
spi_agent.sv — the agent, with passivity as a parameter — and the flag-gated version alongside it
// 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
Azvya Education Pvt. Ltd.VLSI Mentor
spi_agent.v — the same design in Verilog-2001
// 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
Azvya Education Pvt. Ltd.VLSI Mentor
spi_agent.vhd — the same design in VHDL
-- 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

Azvya Education Pvt. Ltd.VLSI Mentor
spi_agent_tb.sv — three agents on one bus: what a passive agent reports, and what a missing gate does
// 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
Azvya Education Pvt. Ltd.VLSI Mentor
spi_agent_tb.v — the same bench in Verilog-2001
// 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
Azvya Education Pvt. Ltd.VLSI Mentor
spi_agent_tb.vhd — the same bench in VHDL
-- 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
   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