Skip to content
VLSI Mentor

UART · Module 16

Monitor, Agent and Active/Passive Reuse

Recovering frames independently of the driver, the analysis port that broadcasts without coupling — built as running code with its dangerous property measured — and the flag that makes one agent serve two testbenches.

The monitor is the component this curriculum has already been most careful about. Chapter 14.2 §3 gave it one input port and made it bring its own bit period; Chapter 14.4 measured what happens when it is told the wrong one. None of that changes here.

What changes is how it publishes what it saw — and that mechanism has a property most people do not know about until it costs them a debugging week.

1. The Monitor, Unchanged

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class uart_monitor extends uvm_monitor;
    `uvm_component_utils(uart_monitor)

    virtual uart_line_if vif;
    uart_cfg             m_cfg;

    // The port. Note the type: it is parameterised on the TRANSACTION, not
    // on who receives it -- the monitor has no idea who is listening.
    uvm_analysis_port #(uart_frame_item) ap;

    function new(string name, uvm_component parent);
        super.new(name, parent);
        ap = new("ap", this);
    endfunction

    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        if (!uvm_config_db#(virtual uart_line_if)::get(this, "", "vif", vif))
            `uvm_fatal("NOVIF", "no virtual interface for the UART monitor")
        if (!uvm_config_db#(uart_cfg)::get(this, "", "cfg", m_cfg))
            `uvm_fatal("NOCFG", "no uart_cfg for the UART monitor")
    endfunction

    task run_phase(uvm_phase phase);
        forever begin
            uart_frame_item t;
            recover_frame(t);      // Chapter 14.4's algorithm, unchanged:
                                   // find a start edge, wait 1.5 of ITS OWN
                                   // bit periods, sample at one-bit intervals
            ap.write(t);           // and publish. This call cannot fail.
        end
    endtask
endclass

2. The Analysis Port Cannot Be Refused

A diagram of a UVM monitor publishing through an analysis port. The monitor observes the interface and calls write on its analysis port. The port fans out to three subscribers: a scoreboard, a coverage collector and a protocol log. Each subscriber receives every transaction. There is no return path of any kind from any subscriber back to the port or to the monitor, because the write call is a void function with no status, no ready signal and no failure mode.monitorreconstructs txnanalysis_portwrite(txn)Scoreboardanalysis_impCoverageanalysis_impProtocol loganalysis_impap.write(txn)write()write()write()12
Figure 1 — the monitor's analysis port fanning out to three subscribers. There is no return path in this picture, and that is not a simplification: write() is a void function with no status, no ready and no failure mode.

This is the property worth knowing:

uvm_analysis_port::write() is a void function. No return value, no ready signal, no backpressure, no failure mode.

Compare it with uvm_tlm_fifo::put(), which blocks, or try_put(), which can refuse. The analysis port can do neither. That absence is the entire design, and it is what buys:

  • a monitor that broadcasts without knowing who is listening, or whether anyone is;
  • a guarantee that connecting a second subscriber cannot change the first one's timing;
  • and therefore an agent that can be reused in an environment with a completely different set of consumers.

And it costs exactly one thing, which §3 measures.

3. An Analysis Port That Runs

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
//===========================================================================
//  uart_analysis_bus_v — the ANALYSIS PORT, in Verilog-2001
//
//  NOT SYNTHESIZABLE. A verification component.
//
//  THE ONE PROPERTY THAT DEFINES AN ANALYSIS PORT: it cannot be refused.
//
//  `uvm_analysis_port::write()` is a void function. It has no return value,
//  no ready signal, no backpressure and no failure mode. A monitor calls it
//  and the call returns, whatever the subscribers are doing. Compare that
//  with `uvm_tlm_fifo::put()`, which blocks, or `try_put()`, which can
//  refuse.
//
//  Notice what is NOT in this port list: there is no `ready` input from any
//  subscriber, and there is no way to add one. That absence is the entire
//  design. It is why:
//
//    - a monitor can broadcast without knowing who is listening, or whether
//      anyone is;
//    - connecting a second subscriber cannot change the first one's timing;
//    - and a subscriber that does slow work inside write() does not stall
//      the monitor -- it stalls the SIMULATION, or silently misses traffic,
//      depending on how it was written.
//
//  The last point is the one that bites. An analysis port never tells you
//  that a subscriber could not keep up, because there is no channel on
//  which it could.
//
//  THE BROADCAST IS SIMULTANEOUS. Every subscriber sees the same item on
//  the same cycle. UVM makes no ordering guarantee between subscribers, and
//  a testbench that depends on one seeing it first is relying on the order
//  the connections happened to be made in.
//===========================================================================
`timescale 1ns/1ps

module uart_analysis_bus_v #(
    parameter NSUB = 3,                 // how many subscribers are connected
    parameter W    = 9
) (
    input  wire              clk,
    input  wire              rst_n,

    // ---- the producer: a monitor calling write() ------------------------
    input  wire              write_i,
    input  wire [W-1:0]      item_i,
    //  NOTE: there is deliberately no `ready` output here.

    // ---- the subscribers ------------------------------------------------
    output reg  [NSUB-1:0]   sub_valid_o,
    output reg  [NSUB*W-1:0] sub_item_o,

    // ---- observation -----------------------------------------------------
    output reg  [31:0]       n_written_o
);
    integer s;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            sub_valid_o <= {NSUB{1'b0}};
            sub_item_o  <= {(NSUB*W){1'b0}};
            n_written_o <= 32'd0;
        end else begin
            sub_valid_o <= {NSUB{1'b0}};     // one cycle per write
            if (write_i) begin
                for (s = 0; s < NSUB; s = s + 1) begin
                    sub_valid_o[s]        <= 1'b1;
                    sub_item_o[s*W +: W]  <= item_i;
                end
                n_written_o <= n_written_o + 1;
            end
        end
    end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
//===========================================================================
//  uart_analysis_bus — the ANALYSIS PORT, in SystemVerilog
//
//  NOT SYNTHESIZABLE. A verification component.
//
//  THE ONE PROPERTY THAT DEFINES AN ANALYSIS PORT: it cannot be refused.
//
//  `uvm_analysis_port::write()` is a void function. It has no return value,
//  no ready signal, no backpressure and no failure mode. A monitor calls it
//  and the call returns, whatever the subscribers are doing. Compare that
//  with `uvm_tlm_fifo::put()`, which blocks, or `try_put()`, which can
//  refuse.
//
//  Notice what is NOT in this port list: there is no `ready` input from any
//  subscriber, and there is no way to add one. That absence is the entire
//  design. It is why:
//
//    - a monitor can broadcast without knowing who is listening, or whether
//      anyone is;
//    - connecting a second subscriber cannot change the first one's timing;
//    - and a subscriber that does slow work inside write() does not stall
//      the monitor -- it stalls the SIMULATION, or silently misses traffic,
//      depending on how it was written.
//
//  The last point is the one that bites. An analysis port never tells you
//  that a subscriber could not keep up, because there is no channel on
//  which it could.
//
//  THE BROADCAST IS SIMULTANEOUS. Every subscriber sees the same item on
//  the same cycle. UVM makes no ordering guarantee between subscribers, and
//  a testbench that depends on one seeing it first is relying on the order
//  the connections happened to be made in.
//===========================================================================
`timescale 1ns/1ps

module uart_analysis_bus #(
    parameter NSUB = 3,                 // how many subscribers are connected
    parameter W    = 9
) (
    input  wire              clk,
    input  wire              rst_n,

    // ---- the producer: a monitor calling write() ------------------------
    input  wire              write_i,
    input  wire [W-1:0]      item_i,
    //  NOTE: there is deliberately no `ready` output here.

    // ---- the subscribers ------------------------------------------------
    output logic  [NSUB-1:0]   sub_valid_o,
    output logic  [NSUB*W-1:0] sub_item_o,

    // ---- observation -----------------------------------------------------
    output logic  [31:0]       n_written_o
);
    int s;

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            sub_valid_o <= {NSUB{1'b0}};
            sub_item_o  <= {(NSUB*W){1'b0}};
            n_written_o <= 32'd0;
        end else begin
            sub_valid_o <= {NSUB{1'b0}};     // one cycle per write
            if (write_i) begin
                for (s = 0; s < NSUB; s = s + 1) begin
                    sub_valid_o[s]        <= 1'b1;
                    sub_item_o[s*W +: W]  <= item_i;
                end
                n_written_o <= n_written_o + 1;
            end
        end
    end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
--===========================================================================
--  uart_analysis_bus — the ANALYSIS PORT, in VHDL-2008
--
--  NOT SYNTHESIZABLE. A verification component.
--
--  THE ONE PROPERTY THAT DEFINES AN ANALYSIS PORT: it cannot be refused.
--
--  uvm_analysis_port::write() is a void function. No return value, no ready
--  signal, no backpressure, no failure mode. A monitor calls it and the call
--  returns, whatever the subscribers are doing. Compare uvm_tlm_fifo::put(),
--  which blocks, or try_put(), which can refuse.
--
--  Notice what is NOT in this port list: there is no `ready` input from any
--  subscriber, and no way to add one. That absence is the entire design.
--  It is why a monitor can broadcast without knowing who is listening, why
--  connecting a second subscriber cannot change the first one's timing, and
--  why a subscriber that does slow work inside write() does not stall the
--  monitor -- it silently misses traffic instead.
--
--  An analysis port never tells you a subscriber could not keep up, because
--  there is no channel on which it could.
--===========================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity uart_analysis_bus is
    generic (
        NSUB : positive := 3;           -- how many subscribers are connected
        W    : positive := 9
    );
    port (
        clk   : in std_logic;
        rst_n : in std_logic;

        -- the producer: a monitor calling write()
        write_i : in std_logic;
        item_i  : in std_logic_vector(W-1 downto 0);
        --  NOTE: there is deliberately no `ready` output here.

        -- the subscribers
        sub_valid_o : out std_logic_vector(NSUB-1 downto 0);
        sub_item_o  : out std_logic_vector(NSUB*W-1 downto 0);

        n_written_o : out natural
    );
end entity uart_analysis_bus;

architecture model of uart_analysis_bus is
    signal written : natural := 0;
begin

    bcast : process (clk, rst_n)
    begin
        if rst_n = '0' then
            sub_valid_o <= (others => '0');
            sub_item_o  <= (others => '0');
            written     <= 0;
        elsif rising_edge(clk) then
            sub_valid_o <= (others => '0');       -- one cycle per write
            if write_i = '1' then
                for s in 0 to NSUB-1 loop
                    sub_valid_o(s) <= '1';
                    sub_item_o((s+1)*W-1 downto s*W) <= item_i;
                end loop;
                written <= written + 1;
            end if;
        end if;
    end process bcast;

    n_written_o <= written;

end architecture model;

The suite that measures it

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
//===========================================================================
//  tb_uart_analysis_bus_v — self-checking Verilog-2001 testbench
//
//  An analysis port is defined by what it CANNOT do, so most of this suite
//  is about absences:
//
//    - every subscriber sees every item, on the same cycle;
//    - connecting more subscribers changes nothing for the existing ones;
//    - and there is no way for a subscriber to refuse or delay a write.
//
//  The last one is demonstrated rather than asserted: a subscriber that
//  cannot keep up is modelled, and the suite measures what it MISSES. That
//  loss is invisible to the producer by construction, which is the single
//  most important thing to understand about analysis ports.
//===========================================================================
`timescale 1ns/1ps

module tb_uart_analysis_bus_v;

    localparam NSUB = 3;
    localparam W    = 9;

    reg clk = 1'b0;
    always #5 clk = ~clk;
    reg rst_n = 1'b0;

    reg          write = 1'b0;
    reg  [W-1:0] item  = {W{1'b0}};
    wire [NSUB-1:0]   sub_valid;
    wire [NSUB*W-1:0] sub_item;
    wire [31:0]       n_written;

    uart_analysis_bus_v #(.NSUB(NSUB), .W(W)) dut (
        .clk(clk), .rst_n(rst_n),
        .write_i(write), .item_i(item),
        .sub_valid_o(sub_valid), .sub_item_o(sub_item),
        .n_written_o(n_written));

    //---- three subscribers, modelled with different behaviour ------------
    // sub 0 : keeps up perfectly
    // sub 1 : keeps up perfectly (to prove 0 and 1 agree exactly)
    // sub 2 : SLOW -- busy for BUSY_CYC cycles after each item it accepts,
    //         and therefore misses anything that arrives while it is busy
    localparam BUSY_CYC = 4;

    integer seen [0:NSUB-1];
    integer missed2 = 0;
    integer busy2   = 0;
    integer disagree = 0;
    reg [W-1:0] last [0:NSUB-1];

    integer s;
    always @(posedge clk) if (rst_n) begin
        // subscribers 0 and 1 take everything
        for (s = 0; s < 2; s = s + 1)
            if (sub_valid[s]) begin
                seen[s] = seen[s] + 1;
                last[s] = sub_item[s*W +: W];
            end
        // subscriber 2 is busy some of the time
        if (busy2 > 0) busy2 = busy2 - 1;
        if (sub_valid[2]) begin
            if (busy2 == 0) begin
                seen[2] = seen[2] + 1;
                last[2] = sub_item[2*W +: W];
                busy2   = BUSY_CYC;
            end else begin
                // The write still happened. Nothing told the producer.
                missed2 = missed2 + 1;
            end
        end
        // whenever two subscribers are both delivered to, the item must match
        if (sub_valid[0] && sub_valid[1] &&
            (sub_item[0*W +: W] !== sub_item[1*W +: W])) disagree = disagree + 1;
    end

    integer checks = 0, failures = 0;
    task check;
        input cond;
        input [8*80-1:0] name;
        begin
            checks = checks + 1;
            if (cond) $display("  PASS %0s", name);
            else begin failures = failures + 1; $display("  FAIL %0s", name); end
        end
    endtask

    integer i, base0;

    task post;
        input [W-1:0] d;
        begin
            @(negedge clk) write = 1'b1; item = d;
            @(negedge clk) write = 1'b0;
        end
    endtask

    initial begin
        #10_000_000;
        $display("  FAIL watchdog: simulation did not finish");
        $display("== %0d checks, %0d failures ==", checks+1, failures+1);
        $display("   RESULT: VERILOG ANALYSIS-BUS TESTS FAILED (timeout)");
        $finish;
    end

    initial begin
        $display("== uart_analysis_bus_v : self-checking Verilog testbench ==");
        for (i = 0; i < NSUB; i = i + 1) begin seen[i] = 0; last[i] = 0; end

        rst_n = 1'b0;
        repeat (4) @(negedge clk);
        check(sub_valid === {NSUB{1'b0}}, "reset: no subscriber is being written to");
        rst_n = 1'b1;
        repeat (2) @(negedge clk);

        //=== one write reaches every subscriber, on the same cycle ==========
        post(9'h0A5);
        repeat (2) @(negedge clk);
        check(n_written == 1, "one write is counted once by the producer");
        check(seen[0] == 1 && seen[1] == 1 && seen[2] == 1,
              "and reaches ALL THREE subscribers");
        check(last[0] == 9'h0A5 && last[1] == 9'h0A5 && last[2] == 9'h0A5,
              "each of them receiving the same item");

        //=== the broadcast is simultaneous ==================================
        // sub_valid is a single vector: there is no ordering between
        // subscribers to depend on, by construction.
        check(disagree == 0,
              "subscribers never receive different items on the same write");

        //=== a burst, with everyone keeping up ==============================
        for (i = 0; i < 20; i = i + 1) begin
            post(9'h010 + i[8:0]);
            repeat (BUSY_CYC) @(negedge clk);     // slow enough for sub 2
        end
        check(seen[0] == 21 && seen[1] == 21,
              "twenty more writes: the fast subscribers saw all of them");
        check(seen[2] == 21, "and so did the slow one, given time between writes");
        check(missed2 == 0, "nothing was missed while the pace allowed it");

        //=== THE property: a slow subscriber cannot push back ===============
        // Back-to-back writes, faster than subscriber 2 can accept.
        base0 = seen[0];
        for (i = 0; i < 60; i = i + 1) post(9'h080 + i[8:0]);
        repeat (4) @(negedge clk);

        check(seen[0] - base0 == 60,
              "60 back-to-back writes all reached the fast subscriber");
        check(n_written == 81, "and the producer counted every one of them");
        check(missed2 > 0,
              "the SLOW subscriber missed some -- and could not say so");
        $display("  [info] slow subscriber missed %0d of 60 writes", missed2);

        //=== there is no backpressure path at all ============================
        // Not a measurement -- a statement about the port list. The producer
        // has no input from any subscriber, so nothing a subscriber does can
        // change when write_i is accepted.
        check(n_written == 81,
              "the producer's count is unaffected by any subscriber's state");
        check(seen[0] == seen[1],
              "and two subscribers with identical behaviour agree exactly");

        //=== adding a subscriber changes nothing for the others =============
        // Subscriber 2 has been missing items throughout. Subscribers 0 and 1
        // are unaffected by that, which is the reuse property.
        check(seen[0] > seen[2],
              "the slow subscriber is behind the others");
        check(seen[0] == seen[1],
              "yet the fast ones are still exactly in step with each other");

        //=== quiet means quiet ==============================================
        base0 = seen[0];
        repeat (30) @(negedge clk);
        check(seen[0] == base0, "with no write, no subscriber is disturbed");
        check(sub_valid === {NSUB{1'b0}}, "and no valid is left asserted");

        $display("== %0d checks, %0d failures ==", checks, failures);
        if (failures == 0) $display("   RESULT: ALL VERILOG ANALYSIS-BUS TESTS PASSED");
        else               $display("   RESULT: VERILOG ANALYSIS-BUS TESTS FAILED");
        $finish;
    end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
//===========================================================================
//  tb_uart_analysis_bus — self-checking SystemVerilog testbench
//
//  An analysis port is defined by what it CANNOT do, so most of this suite
//  is about absences:
//
//    - every subscriber sees every item, on the same cycle;
//    - connecting more subscribers changes nothing for the existing ones;
//    - and there is no way for a subscriber to refuse or delay a write.
//
//  The last one is demonstrated rather than asserted: a subscriber that
//  cannot keep up is modelled, and the suite measures what it MISSES. That
//  loss is invisible to the producer by construction, which is the single
//  most important thing to understand about analysis ports.
//===========================================================================
`timescale 1ns/1ps

module tb_uart_analysis_bus;

    localparam NSUB = 3;
    localparam W    = 9;

    logic clk = 1'b0;
    always #5 clk = ~clk;
    logic rst_n = 1'b0;

    logic          write = 1'b0;
    logic  [W-1:0] item  = {W{1'b0}};
    wire [NSUB-1:0]   sub_valid;
    wire [NSUB*W-1:0] sub_item;
    wire [31:0]       n_written;

    uart_analysis_bus #(.NSUB(NSUB), .W(W)) dut (
        .clk(clk), .rst_n(rst_n),
        .write_i(write), .item_i(item),
        .sub_valid_o(sub_valid), .sub_item_o(sub_item),
        .n_written_o(n_written));

    //---- three subscribers, modelled with different behaviour ------------
    // sub 0 : keeps up perfectly
    // sub 1 : keeps up perfectly (to prove 0 and 1 agree exactly)
    // sub 2 : SLOW -- busy for BUSY_CYC cycles after each item it accepts,
    //         and therefore misses anything that arrives while it is busy
    localparam BUSY_CYC = 4;

    int seen [0:NSUB-1];
    int missed2 = 0;
    int busy2   = 0;
    int disagree = 0;
    logic [W-1:0] last [0:NSUB-1];

    int s;
    always @(posedge clk) if (rst_n) begin
        // subscribers 0 and 1 take everything
        for (s = 0; s < 2; s = s + 1)
            if (sub_valid[s]) begin
                seen[s] = seen[s] + 1;
                last[s] = sub_item[s*W +: W];
            end
        // subscriber 2 is busy some of the time
        if (busy2 > 0) busy2 = busy2 - 1;
        if (sub_valid[2]) begin
            if (busy2 == 0) begin
                seen[2] = seen[2] + 1;
                last[2] = sub_item[2*W +: W];
                busy2   = BUSY_CYC;
            end else begin
                // The write still happened. Nothing told the producer.
                missed2++;
            end
        end
        // whenever two subscribers are both delivered to, the item must match
        if (sub_valid[0] && sub_valid[1] &&
            (sub_item[0*W +: W] !== sub_item[1*W +: W])) disagree++;
    end

    int checks = 0, failures = 0;
    task automatic check(input logic cond, input string name);
        checks++;
        if (cond) $display("  PASS %0s", name);
        else begin failures++; $display("  FAIL %0s", name); end
    endtask

    int i, base0;

    task post;
        input [W-1:0] d;
        begin
            @(negedge clk) write = 1'b1; item = d;
            @(negedge clk) write = 1'b0;
        end
    endtask

    initial begin
        #10_000_000;
        $display("  FAIL watchdog: simulation did not finish");
        $display("== %0d checks, %0d failures ==", checks+1, failures+1);
        $display("   RESULT: SYSTEMVERILOG ANALYSIS-BUS TESTS FAILED (timeout)");
        $finish;
    end

    initial begin
        $display("== uart_analysis_bus : self-checking Verilog testbench ==");
        for (i = 0; i < NSUB; i = i + 1) begin seen[i] = 0; last[i] = 0; end

        rst_n = 1'b0;
        repeat (4) @(negedge clk);
        check(sub_valid === {NSUB{1'b0}}, "reset: no subscriber is being written to");
        rst_n = 1'b1;
        repeat (2) @(negedge clk);

        //=== one write reaches every subscriber, on the same cycle ==========
        post(9'h0A5);
        repeat (2) @(negedge clk);
        check(n_written == 1, "one write is counted once by the producer");
        check(seen[0] == 1 && seen[1] == 1 && seen[2] == 1,
              "and reaches ALL THREE subscribers");
        check(last[0] == 9'h0A5 && last[1] == 9'h0A5 && last[2] == 9'h0A5,
              "each of them receiving the same item");

        //=== the broadcast is simultaneous ==================================
        // sub_valid is a single vector: there is no ordering between
        // subscribers to depend on, by construction.
        check(disagree == 0,
              "subscribers never receive different items on the same write");

        //=== a burst, with everyone keeping up ==============================
        for (i = 0; i < 20; i = i + 1) begin
            post(9'h010 + i[8:0]);
            repeat (BUSY_CYC) @(negedge clk);     // slow enough for sub 2
        end
        check(seen[0] == 21 && seen[1] == 21,
              "twenty more writes: the fast subscribers saw all of them");
        check(seen[2] == 21, "and so did the slow one, given time between writes");
        check(missed2 == 0, "nothing was missed while the pace allowed it");

        //=== THE property: a slow subscriber cannot push back ===============
        // Back-to-back writes, faster than subscriber 2 can accept.
        base0 = seen[0];
        for (i = 0; i < 60; i = i + 1) post(9'h080 + i[8:0]);
        repeat (4) @(negedge clk);

        check(seen[0] - base0 == 60,
              "60 back-to-back writes all reached the fast subscriber");
        check(n_written == 81, "and the producer counted every one of them");
        check(missed2 > 0,
              "the SLOW subscriber missed some -- and could not say so");
        $display("  [info] slow subscriber missed %0d of 60 writes", missed2);

        //=== there is no backpressure path at all ============================
        // Not a measurement -- a statement about the port list. The producer
        // has no input from any subscriber, so nothing a subscriber does can
        // change when write_i is accepted.
        check(n_written == 81,
              "the producer's count is unaffected by any subscriber's state");
        check(seen[0] == seen[1],
              "and two subscribers with identical behaviour agree exactly");

        //=== adding a subscriber changes nothing for the others =============
        // Subscriber 2 has been missing items throughout. Subscribers 0 and 1
        // are unaffected by that, which is the reuse property.
        check(seen[0] > seen[2],
              "the slow subscriber is behind the others");
        check(seen[0] == seen[1],
              "yet the fast ones are still exactly in step with each other");

        //=== quiet means quiet ==============================================
        base0 = seen[0];
        repeat (30) @(negedge clk);
        check(seen[0] == base0, "with no write, no subscriber is disturbed");
        check(sub_valid === {NSUB{1'b0}}, "and no valid is left asserted");

        $display("== %0d checks, %0d failures ==", checks, failures);
        if (failures == 0) $display("   RESULT: ALL SYSTEMVERILOG ANALYSIS-BUS TESTS PASSED");
        else               $display("   RESULT: SYSTEMVERILOG ANALYSIS-BUS TESTS FAILED");
        $finish;
    end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
--===========================================================================
--  tb_uart_analysis_bus — self-checking VHDL-2008 testbench
--
--  An analysis port is defined by what it CANNOT do, so most of this suite
--  is about absences: every subscriber sees every item on the same cycle,
--  connecting more subscribers changes nothing for the existing ones, and
--  there is no way for a subscriber to refuse or delay a write.
--
--  The last is demonstrated rather than asserted: a subscriber that cannot
--  keep up is modelled, and the suite measures what it MISSES. That loss is
--  invisible to the producer by construction, which is the single most
--  important thing to understand about analysis ports.
--
--  Same 17 counted checks as the Verilog and SystemVerilog twins.
--===========================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity tb_uart_analysis_bus is
end entity tb_uart_analysis_bus;

architecture sim of tb_uart_analysis_bus is
    constant NSUB     : positive := 3;
    constant W        : positive := 9;
    constant TCLK     : time     := 10 ns;
    constant BUSY_CYC : natural  := 4;

    signal clk   : std_logic := '0';
    signal rst_n : std_logic := '0';
    signal done  : boolean   := false;

    signal write_s : std_logic := '0';
    signal item_s  : std_logic_vector(W-1 downto 0) := (others => '0');
    signal sub_valid : std_logic_vector(NSUB-1 downto 0);
    signal sub_item  : std_logic_vector(NSUB*W-1 downto 0);
    signal n_written : natural;

    type nat_arr is array (0 to NSUB-1) of natural;
    type vec_arr is array (0 to NSUB-1) of std_logic_vector(W-1 downto 0);
    signal seen     : nat_arr := (others => 0);
    signal last     : vec_arr := (others => (others => '0'));
    signal missed2  : natural := 0;
    signal disagree : natural := 0;
begin
    clk <= not clk after TCLK/2 when not done else '0';

    dut : entity work.uart_analysis_bus
        generic map (NSUB => NSUB, W => W)
        port map (clk => clk, rst_n => rst_n,
                  write_i => write_s, item_i => item_s,
                  sub_valid_o => sub_valid, sub_item_o => sub_item,
                  n_written_o => n_written);

    -- sub 0 and 1 keep up perfectly; sub 2 is busy for BUSY_CYC cycles after
    -- each item it accepts, and therefore misses anything arriving meanwhile.
    subs : process (clk)
        variable busy2 : natural := 0;
    begin
        if rising_edge(clk) and rst_n = '1' then
            for s in 0 to 1 loop
                if sub_valid(s) = '1' then
                    seen(s) <= seen(s) + 1;
                    last(s) <= sub_item((s+1)*W-1 downto s*W);
                end if;
            end loop;
            if busy2 > 0 then busy2 := busy2 - 1; end if;
            if sub_valid(2) = '1' then
                if busy2 = 0 then
                    seen(2) <= seen(2) + 1;
                    last(2) <= sub_item(3*W-1 downto 2*W);
                    busy2   := BUSY_CYC;
                else
                    -- The write still happened. Nothing told the producer.
                    missed2 <= missed2 + 1;
                end if;
            end if;
            if sub_valid(0) = '1' and sub_valid(1) = '1'
               and sub_item(W-1 downto 0) /= sub_item(2*W-1 downto W) then
                disagree <= disagree + 1;
            end if;
        end if;
    end process subs;

    watchdog : process
    begin
        wait for 10 ms;
        report "watchdog: simulation did not finish" severity failure;
    end process watchdog;

    stim : process
        variable checks, failures : natural := 0;
        variable base0 : natural;

        procedure check(cond : boolean; name : string) is
        begin
            checks := checks + 1;
            if cond then report "  PASS " & name severity note;
            else failures := failures + 1; report "  FAIL " & name severity error;
            end if;
        end procedure check;

        procedure post(d : std_logic_vector(W-1 downto 0)) is
        begin
            wait until falling_edge(clk); write_s <= '1'; item_s <= d;
            wait until falling_edge(clk); write_s <= '0';
        end procedure post;
    begin
        report "== uart_analysis_bus : self-checking VHDL testbench ==" severity note;
        rst_n <= '0';
        for i in 1 to 4 loop wait until falling_edge(clk); end loop;
        check(sub_valid = (sub_valid'range => '0'),
              "reset: no subscriber is being written to");
        rst_n <= '1';
        for i in 1 to 2 loop wait until falling_edge(clk); end loop;

        --=== one write reaches every subscriber, on the same cycle ==========
        post('0' & x"A5");
        for i in 1 to 2 loop wait until falling_edge(clk); end loop;
        check(n_written = 1, "one write is counted once by the producer");
        check(seen(0) = 1 and seen(1) = 1 and seen(2) = 1,
              "and reaches ALL THREE subscribers");
        check(last(0) = ('0' & x"A5") and last(1) = ('0' & x"A5")
              and last(2) = ('0' & x"A5"),
              "each of them receiving the same item");
        check(disagree = 0,
              "subscribers never receive different items on the same write");

        --=== a burst, with everyone keeping up ==============================
        for i in 0 to 19 loop
            post(std_logic_vector(to_unsigned(16#10# + i, W)));
            for k in 1 to BUSY_CYC loop wait until falling_edge(clk); end loop;
        end loop;
        check(seen(0) = 21 and seen(1) = 21,
              "twenty more writes: the fast subscribers saw all of them");
        check(seen(2) = 21, "and so did the slow one, given time between writes");
        check(missed2 = 0, "nothing was missed while the pace allowed it");

        --=== THE property: a slow subscriber cannot push back ===============
        base0 := seen(0);
        for i in 0 to 59 loop
            post(std_logic_vector(to_unsigned(16#80# + i, W)));
        end loop;
        for i in 1 to 4 loop wait until falling_edge(clk); end loop;

        check(seen(0) - base0 = 60,
              "60 back-to-back writes all reached the fast subscriber");
        check(n_written = 81, "and the producer counted every one of them");
        check(missed2 > 0,
              "the SLOW subscriber missed some -- and could not say so");
        report "  [info] slow subscriber missed " & integer'image(missed2)
             & " of 60 writes" severity note;

        --=== there is no backpressure path at all ============================
        check(n_written = 81,
              "the producer's count is unaffected by any subscriber's state");
        check(seen(0) = seen(1),
              "and two subscribers with identical behaviour agree exactly");

        --=== adding a subscriber changes nothing for the others =============
        check(seen(0) > seen(2), "the slow subscriber is behind the others");
        check(seen(0) = seen(1),
              "yet the fast ones are still exactly in step with each other");

        --=== quiet means quiet ==============================================
        base0 := seen(0);
        for i in 1 to 30 loop wait until falling_edge(clk); end loop;
        check(seen(0) = base0, "with no write, no subscriber is disturbed");
        check(sub_valid = (sub_valid'range => '0'),
              "and no valid is left asserted");

        report "== " & integer'image(checks) & " checks, "
                     & integer'image(failures) & " failures ==" severity note;
        if failures = 0 then
            report "   RESULT: ALL VHDL ANALYSIS-BUS TESTS PASSED" severity note;
        else
            report "   RESULT: VHDL ANALYSIS-BUS TESTS FAILED" severity error;
        end if;
        done <= true;
        wait;
    end process stim;
end architecture sim;

What it costs

The testbench models three subscribers. Two keep up perfectly. The third is busy for four cycles after each item it accepts — a subscriber doing real work inside write(), which is exactly what a coverage collector sampling a covergroup or a scoreboard doing a queue search is doing.

Then sixty writes arrive back to back:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  PASS 60 back-to-back writes all reached the fast subscriber
  PASS and the producer counted every one of them
  PASS the SLOW subscriber missed some -- and could not say so
  [info] slow subscriber missed 30 of 60 writes
  PASS the producer's count is unaffected by any subscriber's state
  PASS and two subscribers with identical behaviour agree exactly
== 17 checks, 0 failures ==

Verilog-2001    : 17 checks, 0 failures    missed 30 of 60
SystemVerilog   : 17 checks, 0 failures    missed 30 of 60
VHDL-2008       : 17 checks, 0 failures    missed 30 of 60

Thirty of sixty transactions lost, and nothing anywhere reported it.

4. The Agent, and the Flag That Earns It

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class uart_agent extends uvm_agent;
    `uvm_component_utils(uart_agent)

    uart_cfg          m_cfg;
    uart_sequencer    m_sqr;
    uart_driver       m_drv;
    uart_monitor      m_mon;

    // The agent republishes the monitor's port so the environment connects to
    // the AGENT, not into it. An env that reached in to m_agent.m_mon.ap would
    // break the moment the agent's internals changed.
    uvm_analysis_port #(uart_frame_item) ap;

    function new(string name, uvm_component parent);
        super.new(name, parent);
        ap = new("ap", this);
    endfunction

    function void build_phase(uvm_phase phase);
        super.build_phase(phase);
        if (!uvm_config_db#(uart_cfg)::get(this, "", "cfg", m_cfg))
            `uvm_fatal("NOCFG", "no uart_cfg for the UART agent")

        // The monitor is ALWAYS built.
        m_mon = uart_monitor::type_id::create("m_mon", this);

        // The sequencer and driver are built only when the agent is active.
        if (m_cfg.is_active) begin
            m_sqr = uart_sequencer::type_id::create("m_sqr", this);
            m_drv = uart_driver   ::type_id::create("m_drv", this);
        end
    endfunction

    function void connect_phase(uvm_phase phase);
        m_mon.ap.connect(ap);                       // republish
        if (m_cfg.is_active)
            m_drv.seq_item_port.connect(m_sqr.seq_item_export);
    endfunction
endclass

Four lines of if, and the same agent serves two completely different testbenches.

activepassive
sequencer, driverbuiltnot built at all
monitorbuiltbuilt
who drives the linkthis agentreal software on a real processor
used inblock-level UART verificationan SoC testbench where the UART is incidental

5. Mutation

Three defects installed in the analysis port:

#Defect installedResult
M5only subscriber 0 receives the writekilled, 7
M6one subscriber receives a stale itemkilled, 2
M7the producer's count is not incrementedkilled, 3

M6 is the interesting one. A stale item reaching one subscriber and not the others is the shape of failure that a single-subscriber testbench cannot detect at all — with one consumer there is nothing to disagree with. It is caught here by a check that compares two subscribers against each other on the same write:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
if (sub_valid[0] && sub_valid[1] &&
    (sub_item[0*W +: W] !== sub_item[1*W +: W])) disagree = disagree + 1;

Two observers of the same event are worth more than one, and the argument is the same one Chapter 12.2 §4 made about two consumers of a synchronised line: disagreement between them is observable, where a single wrong value is not.

6. Verification

Give the monitor one input and make it bring its own timing. Everything in §4 depends on it, three modules after the decision was made.

Republish the monitor's port from the agent. An environment that connects to m_agent.m_mon.ap is welded to the agent's internals.

Do no slow work inside write(). Queue the item and process it elsewhere; §3 measured 30 of 60 transactions lost to a subscriber doing four cycles of work.

Assert on subscriber counts. The analysis port has no channel to report a dropped transaction, so the only defence is a subscriber that knows how many it should have received.

Connect two subscribers and compare them. A stale or corrupted broadcast is invisible with one consumer and obvious with two.

And build the sequencer and driver only when active. A passive agent that still constructs a driver will connect a virtual interface it must not drive, and the symptom is two drivers fighting over the wire.

7. Debugging

8. Understanding Check

9. Summary

The monitor does not change — one input, its own bit period, its own parity definition. What changes is that it publishes through a port instead of being read through a hierarchical reference.

write() is a void function: no status, no ready, no backpressure, no failure mode. That absence is what lets a monitor broadcast without knowing who is listening.

And it costs exactly one thing, measured: a subscriber doing four cycles of work inside write() missed 30 of 60 transactions, with nothing anywhere reporting it.

So: queue in write(), never block, and assert on counts — the only defence is a subscriber that knows how many it should have seen.

Two subscribers compared against each other catch a corrupted broadcast that one subscriber cannot see at all.

is_active is four lines of if and it is the whole payback for the methodology: the same agent verifies the UART at block level and watches it in an SoC testbench where firmware is driving.

17 checks in each of three languages, 0 failures; three mutants, three killed.

10. What Comes Next

Chapter 16.5 connects the subscribers — the reference model, the scoreboard and the coverage collector — and builds the mechanism that decides when the test is allowed to stop, which has a failure mode that looks exactly like success.

Browse the full path on the UART tutorials index. For the monitor's algorithm, read back to Chapter 14.4.

Continue learning

Where this fits

Part of the UART curriculum.