Skip to content
VLSI Mentor

UART · Module 16

The Driver and the Bit-Timing Problem

Driving a transaction onto a wire with no clock to synchronise to, and the sequencer that decides whose transaction goes next — built as running code in three languages, with both halves of its contract measured.

A driver for a clocked bus is straightforward: wait for a clocking-block edge, put the values on the wires, wait again. A UART driver has no such edge. The far end of a serial link is a device with its own oscillator, and the driver is that oscillator.

That difference shapes everything in this chapter, and it makes the sequencer's contract matter more than it does on a bus.

1. The Driver Has No Clock

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class uart_driver extends uvm_driver #(uart_frame_item);
    `uvm_component_utils(uart_driver)

    virtual uart_line_if vif;
    uart_cfg             m_cfg;

    function new(string name, uvm_component parent);
        super.new(name, parent);
    endfunction

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

    task run_phase(uvm_phase phase);
        vif.tx <= 1'b1;                       // idle MARK before anything
        forever begin
            seq_item_port.get_next_item(req); // BLOCKS until an item is ready
            drive_frame(req);
            seq_item_port.item_done();        // and only now may the next one come
        end
    endtask

    // The body is Chapter 14.2's send_frame task, reached through a virtual
    // interface instead of a hierarchical name. Note what it does NOT contain:
    // there is no @(posedge clk) anywhere in it. The delays are absolute
    // times, because the far end of a serial link has its own oscillator and
    // the driver is standing in for it.
    protected task drive_frame(uart_frame_item t);
        vif.tx <= 1'b0;  #(t.tbit_ns * 1ns);                 // start
        for (int i = 0; i < t.nbits; i++) begin
            vif.tx <= t.data[i];  #(t.tbit_ns * 1ns);        // data, LSB first
        end
        if (t.parity_mode != 0) begin
            vif.tx <= parity_of(t);  #(t.tbit_ns * 1ns);
        end
        vif.tx <= 1'b1;  #(t.tbit_ns * t.stop_halves / 2.0 * 1ns);
    endtask
endclass

2. The Sequencer's Two Contracts

A sequence diagram of the UVM sequencer to driver handshake. A sequence calls start_item, which blocks until the sequencer arbitrates and grants it. The sequence then randomises the item and calls finish_item, handing it to the sequencer. Meanwhile the driver has called get_next_item, which blocks until an item is available; the sequencer passes the granted item to the driver. The driver drives the item onto the interface, taking as long as it needs, and only when it has finished does it call item_done. That call releases the sequencer, which may then arbitrate again and grant the next waiting sequence.sequencesequencerdriverDUTstart_item(req)get_next_item(req)drive vif signalsresponseitem_done()finish_item()
Figure 1 — the sequencer-driver handshake. The sequence blocks in start_item until the sequencer grants it; the driver blocks in get_next_item until an item is available; and the sequencer holds that item in front of the driver until item_done. Three blocking points, and every one of them is load-bearing.

Strip away the classes and a sequencer is an arbiter with a blocking handshake. It has exactly two contracts, and they fail in different ways.

The handshake. An item placed in front of the driver stays there, unchanged, until the driver says item_done. A sequencer that moves on early loses transactions, and the loss is silent — the driver simply never saw them.

The arbitration. Every requesting sequence eventually gets served. A fixed-priority arbiter satisfies every other property a sequencer has and starves the last sequence forever.

3. A Sequencer That Runs

UVM's sequencer cannot execute on this toolchain (16.1 §5). The mechanism can, and seeing it at fifty lines is worth more than reading the class.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
//===========================================================================
//  uart_seq_arb_v — the SEQUENCER, in Verilog-2001
//
//  NOT SYNTHESIZABLE. This is a verification component, and it is the part
//  of UVM that people find hardest to picture — so it is built here small
//  enough to run.
//
//  WHAT A UVM SEQUENCER ACTUALLY DOES. Strip away the classes and it is an
//  arbiter with a blocking handshake:
//
//    - several SEQUENCES each want to send an item;
//    - exactly one is granted at a time;
//    - the granted item is held in front of the DRIVER until the driver
//      says it has finished with it;
//    - and then the next sequence gets its turn.
//
//  In UVM those four lines are `start_item`, arbitration, `get_next_item`
//  and `item_done`. Here they are req/gnt, a round-robin pointer, a held
//  valid, and a done input. The mechanism is identical; only the notation
//  is poorer.
//
//  WHY IT IS BUILT AT ALL. Icarus Verilog 13.0 cannot run UVM -- not the
//  library, which is absent, and not a hand-rolled substitute either: it
//  refuses queues of class handles, mailboxes, $cast and parameterised
//  classes, and it dispatches virtual methods to the BASE class. Each of
//  those was probed individually. So the sequencer is written the only way
//  it can be made to execute on this toolchain, which turns out to be a
//  useful way to see it.
//
//  ROUND-ROBIN IS A POLICY, NOT A LAW. UVM's default arbitration is
//  SEQ_ARB_FIFO; round-robin is chosen here because starvation is then a
//  property a testbench can actually assert on.
//===========================================================================
`timescale 1ns/1ps

module uart_seq_arb_v #(
    parameter NSEQ = 4,                 // how many sequences share the driver
    parameter W    = 9                  // transaction payload width
) (
    input  wire                clk,
    input  wire                rst_n,

    // ---- the sequence side: NSEQ producers -----------------------------
    input  wire [NSEQ-1:0]     req_i,       // this sequence has an item ready
    input  wire [NSEQ*W-1:0]   item_i,      // flattened: sequence k at [k*W +: W]
    output reg  [NSEQ-1:0]     gnt_o,       // ONE cycle, when an item is taken

    // ---- the driver side: one consumer ---------------------------------
    output reg  [W-1:0]        drv_item_o,
    output reg                 drv_valid_o, // held until drv_done_i
    input  wire                drv_done_i,  // the driver's item_done()

    // ---- observation ----------------------------------------------------
    output reg  [31:0]         n_granted_o,
    output reg  [3:0]          last_seq_o   // which sequence was served last
);

    reg [3:0] rr_ptr;                   // round-robin pointer
    reg [3:0] pick;
    reg       found;
    integer   i, k;

    // Choose the next requesting sequence, starting one past the last served.
    // Written as a loop rather than a priority encoder so the rotation is
    // visible: a fixed-priority arbiter starves sequence NSEQ-1 forever, and
    // that is the defect the testbench is built to catch.
    always @* begin
        found = 1'b0;
        pick  = 4'd0;
        for (i = 0; i < NSEQ; i = i + 1) begin
            k = (rr_ptr + i) % NSEQ;
            if (!found && req_i[k]) begin
                found = 1'b1;
                pick  = k[3:0];
            end
        end
    end

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            gnt_o       <= {NSEQ{1'b0}};
            drv_item_o  <= {W{1'b0}};
            drv_valid_o <= 1'b0;
            n_granted_o <= 32'd0;
            rr_ptr      <= 4'd0;
            last_seq_o  <= 4'd0;
        end else begin
            gnt_o <= {NSEQ{1'b0}};      // grant is ONE cycle by construction

            if (!drv_valid_o) begin
                // idle: take an item if anyone is asking
                if (found) begin
                    drv_item_o  <= item_i[pick*W +: W];
                    drv_valid_o <= 1'b1;
                    gnt_o[pick] <= 1'b1;
                    n_granted_o <= n_granted_o + 1;
                    last_seq_o  <= pick;
                    rr_ptr      <= (pick + 1) % NSEQ;
                end
            end else if (drv_done_i) begin
                // the driver has finished: release, and allow the next pick
                drv_valid_o <= 1'b0;
            end
            // While drv_valid_o is high and drv_done_i is low, NOTHING moves.
            // That is the blocking half of get_next_item, and it is the whole
            // reason a sequencer exists rather than a fan-in mux.
        end
    end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
//===========================================================================
//  uart_seq_arb — the SEQUENCER, in SystemVerilog
//
//  NOT SYNTHESIZABLE. This is a verification component, and it is the part
//  of UVM that people find hardest to picture — so it is built here small
//  enough to run.
//
//  WHAT A UVM SEQUENCER ACTUALLY DOES. Strip away the classes and it is an
//  arbiter with a blocking handshake:
//
//    - several SEQUENCES each want to send an item;
//    - exactly one is granted at a time;
//    - the granted item is held in front of the DRIVER until the driver
//      says it has finished with it;
//    - and then the next sequence gets its turn.
//
//  In UVM those four lines are `start_item`, arbitration, `get_next_item`
//  and `item_done`. Here they are req/gnt, a round-robin pointer, a held
//  valid, and a done input. The mechanism is identical; only the notation
//  is poorer.
//
//  WHY IT IS BUILT AT ALL. Icarus Verilog 13.0 cannot run UVM -- not the
//  library, which is absent, and not a hand-rolled substitute either: it
//  refuses queues of class handles, mailboxes, $cast and parameterised
//  classes, and it dispatches virtual methods to the BASE class. Each of
//  those was probed individually. So the sequencer is written the only way
//  it can be made to execute on this toolchain, which turns out to be a
//  useful way to see it.
//
//  ROUND-ROBIN IS A POLICY, NOT A LAW. UVM's default arbitration is
//  SEQ_ARB_FIFO; round-robin is chosen here because starvation is then a
//  property a testbench can actually assert on.
//===========================================================================
`timescale 1ns/1ps

module uart_seq_arb #(
    parameter NSEQ = 4,                 // how many sequences share the driver
    parameter W    = 9                  // transaction payload width
) (
    input  wire                clk,
    input  wire                rst_n,

    // ---- the sequence side: NSEQ producers -----------------------------
    input  wire [NSEQ-1:0]     req_i,       // this sequence has an item ready
    input  wire [NSEQ*W-1:0]   item_i,      // flattened: sequence k at [k*W +: W]
    output logic  [NSEQ-1:0]     gnt_o,       // ONE cycle, when an item is taken

    // ---- the driver side: one consumer ---------------------------------
    output logic  [W-1:0]        drv_item_o,
    output logic                 drv_valid_o, // held until drv_done_i
    input  wire                drv_done_i,  // the driver's item_done()

    // ---- observation ----------------------------------------------------
    output logic  [31:0]         n_granted_o,
    output logic  [3:0]          last_seq_o   // which sequence was served last
);

    logic [3:0] rr_ptr;                   // round-robin pointer
    logic [3:0] pick;
    logic       found;
    int   i, k;

    // Choose the next requesting sequence, starting one past the last served.
    // Written as a loop rather than a priority encoder so the rotation is
    // visible: a fixed-priority arbiter starves sequence NSEQ-1 forever, and
    // that is the defect the testbench is built to catch.
    always @* begin
        found = 1'b0;
        pick  = 4'd0;
        for (i = 0; i < NSEQ; i = i + 1) begin
            k = (rr_ptr + i) % NSEQ;
            if (!found && req_i[k]) begin
                found = 1'b1;
                pick  = k[3:0];
            end
        end
    end

    always @(posedge clk or negedge rst_n) begin
        if (!rst_n) begin
            gnt_o       <= {NSEQ{1'b0}};
            drv_item_o  <= {W{1'b0}};
            drv_valid_o <= 1'b0;
            n_granted_o <= 32'd0;
            rr_ptr      <= 4'd0;
            last_seq_o  <= 4'd0;
        end else begin
            gnt_o <= {NSEQ{1'b0}};      // grant is ONE cycle by construction

            if (!drv_valid_o) begin
                // idle: take an item if anyone is asking
                if (found) begin
                    drv_item_o  <= item_i[pick*W +: W];
                    drv_valid_o <= 1'b1;
                    gnt_o[pick] <= 1'b1;
                    n_granted_o <= n_granted_o + 1;
                    last_seq_o  <= pick;
                    rr_ptr      <= (pick + 1) % NSEQ;
                end
            end else if (drv_done_i) begin
                // the driver has finished: release, and allow the next pick
                drv_valid_o <= 1'b0;
            end
            // While drv_valid_o is high and drv_done_i is low, NOTHING moves.
            // That is the blocking half of get_next_item, and it is the whole
            // reason a sequencer exists rather than a fan-in mux.
        end
    end
endmodule

VHDL has no UVM either — its equivalents are OSVVM and UVVM, neither installed here. The arbitration is the same rotation, expressed with a mod and a loop.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
--===========================================================================
--  uart_seq_arb — the SEQUENCER, in VHDL-2008
--
--  NOT SYNTHESIZABLE. A verification component.
--
--  WHAT A UVM SEQUENCER ACTUALLY DOES. Strip away the classes and it is an
--  arbiter with a blocking handshake: several sequences want to send items,
--  exactly one is granted at a time, the granted item is held in front of
--  the driver until the driver says it has finished, and then the next
--  sequence gets its turn.
--
--  In UVM those are start_item, arbitration, get_next_item and item_done.
--  Here they are req/gnt, a round-robin pointer, a held valid and a done
--  input. The mechanism is identical; only the notation is poorer.
--
--  WHY IT IS BUILT AT ALL. Neither of this curriculum's simulators can run
--  UVM. There is no uvm_pkg on the machine, and Icarus Verilog 13.0 cannot
--  run a hand-rolled substitute either: it refuses queues of class handles,
--  mailboxes, $cast and parameterised classes, and it dispatches virtual
--  methods to the BASE class. Each was probed individually. VHDL has no UVM
--  at all -- its equivalents are OSVVM and UVVM, neither installed here.
--
--  So the mechanism is written the only way it can be made to execute,
--  which turns out to be a useful way to see it.
--===========================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity uart_seq_arb is
    generic (
        NSEQ : positive := 4;           -- how many sequences share the driver
        W    : positive := 9            -- transaction payload width
    );
    port (
        clk   : in std_logic;
        rst_n : in std_logic;

        -- the sequence side: NSEQ producers
        req_i  : in  std_logic_vector(NSEQ-1 downto 0);
        item_i : in  std_logic_vector(NSEQ*W-1 downto 0);
        gnt_o  : out std_logic_vector(NSEQ-1 downto 0);

        -- the driver side: one consumer
        drv_item_o  : out std_logic_vector(W-1 downto 0);
        drv_valid_o : out std_logic;
        drv_done_i  : in  std_logic;

        -- observation
        n_granted_o : out natural;
        last_seq_o  : out natural
    );
end entity uart_seq_arb;

architecture model of uart_seq_arb is
    signal rr_ptr    : natural := 0;
    signal pick      : natural := 0;
    signal found     : boolean := false;
    signal valid_s   : std_logic := '0';
    signal granted_s : natural := 0;
    signal last_s    : natural := 0;
begin

    -- Choose the next requesting sequence, starting one past the last served.
    -- Written as a rotation rather than a priority encoder because a
    -- fixed-priority arbiter starves the last sequence forever, and that is
    -- the defect the testbench is built to catch.
    arb : process (all)
        variable f : boolean;
        variable p : natural;
        variable k : natural;
    begin
        f := false; p := 0;
        for i in 0 to NSEQ-1 loop
            k := (rr_ptr + i) mod NSEQ;
            if not f and req_i(k) = '1' then
                f := true; p := k;
            end if;
        end loop;
        found <= f;
        pick  <= p;
    end process arb;

    seq : process (clk, rst_n)
    begin
        if rst_n = '0' then
            gnt_o       <= (others => '0');
            drv_item_o  <= (others => '0');
            valid_s     <= '0';
            granted_s   <= 0;
            rr_ptr      <= 0;
            last_s      <= 0;
        elsif rising_edge(clk) then
            gnt_o <= (others => '0');       -- grant is ONE cycle by construction

            if valid_s = '0' then
                if found then
                    drv_item_o  <= item_i((pick+1)*W-1 downto pick*W);
                    valid_s     <= '1';
                    gnt_o(pick) <= '1';
                    granted_s   <= granted_s + 1;
                    last_s      <= pick;
                    rr_ptr      <= (pick + 1) mod NSEQ;
                end if;
            elsif drv_done_i = '1' then
                valid_s <= '0';
            end if;
            -- While valid_s is high and drv_done_i is low, NOTHING moves.
            -- That is the blocking half of get_next_item, and it is the whole
            -- reason a sequencer exists rather than a fan-in mux.
        end if;
    end process seq;

    drv_valid_o <= valid_s;
    n_granted_o <= granted_s;
    last_seq_o  <= last_s;

end architecture model;

4. Measuring Both Contracts

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
//===========================================================================
//  tb_uart_seq_arb_v — self-checking Verilog-2001 testbench
//
//  The sequencer has two contracts and they fail in different ways:
//
//    THE HANDSHAKE.  An item placed in front of the driver stays there,
//                    unchanged, until the driver says item_done. A
//                    sequencer that moves on early loses transactions and
//                    the loss is silent -- the driver simply never saw them.
//
//    THE ARBITRATION. Every requesting sequence eventually gets served. A
//                    fixed-priority arbiter satisfies every other check in
//                    this file and starves the last sequence forever.
//
//  INDEPENDENCE: the testbench records what each sequence OFFERED and what
//  the driver RECEIVED, and compares the two lists. It does not read the
//  arbiter's pointer to predict who should win -- that would be re-deriving
//  the answer from the state that produced it.
//===========================================================================
`timescale 1ns/1ps

module tb_uart_seq_arb_v;

    localparam NSEQ = 4;
    localparam W    = 9;

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

    reg  [NSEQ-1:0]   req = {NSEQ{1'b0}};
    reg  [NSEQ*W-1:0] item = {(NSEQ*W){1'b0}};
    wire [NSEQ-1:0]   gnt;
    wire [W-1:0]      drv_item;
    wire              drv_valid;
    reg               drv_done = 1'b0;
    wire [31:0]       n_granted;
    wire [3:0]        last_seq;

    uart_seq_arb_v #(.NSEQ(NSEQ), .W(W)) dut (
        .clk(clk), .rst_n(rst_n),
        .req_i(req), .item_i(item), .gnt_o(gnt),
        .drv_item_o(drv_item), .drv_valid_o(drv_valid), .drv_done_i(drv_done),
        .n_granted_o(n_granted), .last_seq_o(last_seq));

    //---- what each sequence offered, and what the driver received --------
    reg  [W-1:0] offered [0:1023];
    reg  [W-1:0] received[0:1023];
    integer n_offer = 0, n_recv = 0;
    integer served  [0:NSEQ-1];          // fairness tally
    integer mism = 0, held_bad = 0, wide_gnt = 0, multi_gnt = 0;

    reg [W-1:0] held_item;
    reg         held_valid;
    reg [NSEQ-1:0] gnt_prev;

    integer g;
    always @(posedge clk) if (rst_n) begin
        // exactly one grant at a time, and never wider than one cycle
        if (gnt != 0) begin
            if ((gnt & (gnt - 1)) != 0) multi_gnt = multi_gnt + 1;
            if (gnt_prev != 0)          wide_gnt  = wide_gnt + 1;
            for (g = 0; g < NSEQ; g = g + 1)
                if (gnt[g]) begin
                    served[g] = served[g] + 1;
                    offered[n_offer] = item[g*W +: W];
                    n_offer = n_offer + 1;
                end
        end
        gnt_prev = gnt;

        // the item in front of the driver must not change while it is held
        if (held_valid && drv_valid && (drv_item !== held_item))
            held_bad = held_bad + 1;
        held_item  = drv_item;
        held_valid = drv_valid;

        // the driver consumes on done
        if (drv_valid && drv_done) begin
            received[n_recv] = drv_item;
            n_recv = n_recv + 1;
        end
    end

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

    integer i, j, base_recv, min_s, max_s;

    // A driver that takes `lat` cycles to process each item.
    task drive_for;
        input integer ncycles;
        input integer lat;
        integer c, hold;
        begin
            c = 0; hold = 0;
            while (c < ncycles) begin
                @(negedge clk);
                drv_done = 1'b0;
                if (drv_valid) begin
                    hold = hold + 1;
                    if (hold >= lat) begin drv_done = 1'b1; hold = 0; end
                end
                c = c + 1;
            end
            @(negedge clk) drv_done = 1'b0;
        end
    endtask

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

    initial begin
        $display("== uart_seq_arb_v : self-checking Verilog testbench ==");
        for (i = 0; i < NSEQ; i = i + 1) served[i] = 0;
        held_valid = 1'b0; gnt_prev = {NSEQ{1'b0}};

        rst_n = 1'b0;
        repeat (4) @(negedge clk);
        check(gnt === {NSEQ{1'b0}} && drv_valid === 1'b0,
              "reset: no grant and nothing in front of the driver");
        rst_n = 1'b1;
        repeat (2) @(negedge clk);

        //=== one sequence, one item =========================================
        for (i = 0; i < NSEQ; i = i + 1) item[i*W +: W] = 9'h100 + i[8:0];
        @(negedge clk) req[0] = 1'b1;
        repeat (3) @(negedge clk);
        check(drv_valid === 1'b1, "a lone request reaches the driver");
        check(drv_item == 9'h100,  "carrying that sequence's item");
        check(n_granted == 1,      "and is granted exactly once");

        //=== THE blocking property ==========================================
        // Nothing moves while the driver has not said item_done. This is the
        // half that makes it a sequencer rather than a mux.
        @(negedge clk) req = 4'b1111;      // everyone else asks too
        repeat (20) @(negedge clk);
        check(n_granted == 1,
              "20 cycles with every sequence asking: still ONE grant");
        check(drv_item == 9'h100, "and the held item never changed");
        check(held_bad == 0,      "the item in front of the driver is stable");

        //=== release, and the next one is served ============================
        @(negedge clk) drv_done = 1'b1;
        @(negedge clk) drv_done = 1'b0;
        repeat (3) @(negedge clk);
        check(n_granted == 2, "item_done releases the sequencer");
        check(last_seq != 4'd0, "and the next grant goes to a DIFFERENT sequence");

        //=== fairness under full contention =================================
        req = 4'b1111;
        for (i = 0; i < NSEQ; i = i + 1) served[i] = 0;
        drive_for(2000, 3);
        min_s = served[0]; max_s = served[0];
        for (i = 1; i < NSEQ; i = i + 1) begin
            if (served[i] < min_s) min_s = served[i];
            if (served[i] > max_s) max_s = served[i];
        end
        $display("  [info] served: %0d %0d %0d %0d",
                 served[0], served[1], served[2], served[3]);
        check(min_s > 0, "under full contention EVERY sequence is served");
        check(max_s - min_s <= 1,
              "and they are served equally -- round robin, not priority");

        //=== the driver received exactly what was granted ===================
        @(negedge clk) req = 4'b0000;
        repeat (20) @(negedge clk);
        drive_for(40, 1);
        check(n_recv > 400, "the driver consumed a few hundred items");
        check(n_recv == n_offer || n_recv == n_offer - 1,
              "items received equals items granted (allowing one in flight)");
        mism = 0;
        for (i = 0; i < n_recv; i = i + 1)
            if (received[i] !== offered[i]) mism = mism + 1;
        check(mism == 0, "and every one carried the granted sequence's payload");

        //=== grant hygiene ==================================================
        check(multi_gnt == 0, "never more than one sequence granted at a time");
        check(wide_gnt == 0,  "no grant pulse was ever wider than one cycle");

        //=== a slow driver does not lose items ==============================
        base_recv = n_recv;
        @(negedge clk) req = 4'b0101;
        drive_for(600, 12);                 // twelve cycles per item
        check(n_recv - base_recv > 20, "a slow driver still makes progress");
        mism = 0;
        for (i = base_recv; i < n_recv; i = i + 1)
            if (received[i] !== offered[i]) mism = mism + 1;
        check(mism == 0, "and loses nothing while it is slow");

        //=== withdrawing a request ==========================================
        @(negedge clk) req = 4'b0000;
        repeat (4) @(negedge clk);
        drive_for(20, 1);
        base_recv = n_granted;
        repeat (20) @(negedge clk);
        check(n_granted == base_recv,
              "with no sequence asking, nothing is granted");
        check(drv_valid === 1'b0, "and the driver is left idle");

        $display("== %0d checks, %0d failures ==", checks, failures);
        if (failures == 0) $display("   RESULT: ALL VERILOG SEQ-ARB TESTS PASSED");
        else               $display("   RESULT: VERILOG SEQ-ARB TESTS FAILED");
        $finish;
    end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
//===========================================================================
//  tb_uart_seq_arb — self-checking SystemVerilog testbench
//
//  The sequencer has two contracts and they fail in different ways:
//
//    THE HANDSHAKE.  An item placed in front of the driver stays there,
//                    unchanged, until the driver says item_done. A
//                    sequencer that moves on early loses transactions and
//                    the loss is silent -- the driver simply never saw them.
//
//    THE ARBITRATION. Every requesting sequence eventually gets served. A
//                    fixed-priority arbiter satisfies every other check in
//                    this file and starves the last sequence forever.
//
//  INDEPENDENCE: the testbench records what each sequence OFFERED and what
//  the driver RECEIVED, and compares the two lists. It does not read the
//  arbiter's pointer to predict who should win -- that would be re-deriving
//  the answer from the state that produced it.
//===========================================================================
`timescale 1ns/1ps

module tb_uart_seq_arb;

    localparam NSEQ = 4;
    localparam W    = 9;

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

    logic  [NSEQ-1:0]   req = {NSEQ{1'b0}};
    logic  [NSEQ*W-1:0] item = {(NSEQ*W){1'b0}};
    wire [NSEQ-1:0]   gnt;
    wire [W-1:0]      drv_item;
    wire              drv_valid;
    logic               drv_done = 1'b0;
    wire [31:0]       n_granted;
    wire [3:0]        last_seq;

    uart_seq_arb #(.NSEQ(NSEQ), .W(W)) dut (
        .clk(clk), .rst_n(rst_n),
        .req_i(req), .item_i(item), .gnt_o(gnt),
        .drv_item_o(drv_item), .drv_valid_o(drv_valid), .drv_done_i(drv_done),
        .n_granted_o(n_granted), .last_seq_o(last_seq));

    //---- what each sequence offered, and what the driver received --------
    logic  [W-1:0] offered [0:1023];
    logic  [W-1:0] received[0:1023];
    int n_offer = 0, n_recv = 0;
    int served  [0:NSEQ-1];          // fairness tally
    int mism = 0, held_bad = 0, wide_gnt = 0, multi_gnt = 0;

    logic [W-1:0] held_item;
    logic         held_valid;
    logic [NSEQ-1:0] gnt_prev;

    int g;
    always @(posedge clk) if (rst_n) begin
        // exactly one grant at a time, and never wider than one cycle
        if (gnt != 0) begin
            if ((gnt & (gnt - 1)) != 0) multi_gnt++;
            if (gnt_prev != 0)          wide_gnt  = wide_gnt + 1;
            for (g = 0; g < NSEQ; g = g + 1)
                if (gnt[g]) begin
                    served[g] = served[g] + 1;
                    offered[n_offer] = item[g*W +: W];
                    n_offer++;
                end
        end
        gnt_prev = gnt;

        // the item in front of the driver must not change while it is held
        if (held_valid && drv_valid && (drv_item !== held_item))
            held_bad++;
        held_item  = drv_item;
        held_valid = drv_valid;

        // the driver consumes on done
        if (drv_valid && drv_done) begin
            received[n_recv] = drv_item;
            n_recv++;
        end
    end

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

    int i, j, base_recv, min_s, max_s;

    // A driver that takes `lat` cycles to process each item.
    task drive_for;
        input int ncycles;
        input int lat;
        int c, hold;
        begin
            c = 0; hold = 0;
            while (c < ncycles) begin
                @(negedge clk);
                drv_done = 1'b0;
                if (drv_valid) begin
                    hold++;
                    if (hold >= lat) begin drv_done = 1'b1; hold = 0; end
                end
                c++;
            end
            @(negedge clk) drv_done = 1'b0;
        end
    endtask

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

    initial begin
        $display("== uart_seq_arb : self-checking Verilog testbench ==");
        for (i = 0; i < NSEQ; i = i + 1) served[i] = 0;
        held_valid = 1'b0; gnt_prev = {NSEQ{1'b0}};

        rst_n = 1'b0;
        repeat (4) @(negedge clk);
        check(gnt === {NSEQ{1'b0}} && drv_valid === 1'b0,
              "reset: no grant and nothing in front of the driver");
        rst_n = 1'b1;
        repeat (2) @(negedge clk);

        //=== one sequence, one item =========================================
        for (i = 0; i < NSEQ; i = i + 1) item[i*W +: W] = 9'h100 + i[8:0];
        @(negedge clk) req[0] = 1'b1;
        repeat (3) @(negedge clk);
        check(drv_valid === 1'b1, "a lone request reaches the driver");
        check(drv_item == 9'h100,  "carrying that sequence's item");
        check(n_granted == 1,      "and is granted exactly once");

        //=== THE blocking property ==========================================
        // Nothing moves while the driver has not said item_done. This is the
        // half that makes it a sequencer rather than a mux.
        @(negedge clk) req = 4'b1111;      // everyone else asks too
        repeat (20) @(negedge clk);
        check(n_granted == 1,
              "20 cycles with every sequence asking: still ONE grant");
        check(drv_item == 9'h100, "and the held item never changed");
        check(held_bad == 0,      "the item in front of the driver is stable");

        //=== release, and the next one is served ============================
        @(negedge clk) drv_done = 1'b1;
        @(negedge clk) drv_done = 1'b0;
        repeat (3) @(negedge clk);
        check(n_granted == 2, "item_done releases the sequencer");
        check(last_seq != 4'd0, "and the next grant goes to a DIFFERENT sequence");

        //=== fairness under full contention =================================
        req = 4'b1111;
        for (i = 0; i < NSEQ; i = i + 1) served[i] = 0;
        drive_for(2000, 3);
        min_s = served[0]; max_s = served[0];
        for (i = 1; i < NSEQ; i = i + 1) begin
            if (served[i] < min_s) min_s = served[i];
            if (served[i] > max_s) max_s = served[i];
        end
        $display("  [info] served: %0d %0d %0d %0d",
                 served[0], served[1], served[2], served[3]);
        check(min_s > 0, "under full contention EVERY sequence is served");
        check(max_s - min_s <= 1,
              "and they are served equally -- round robin, not priority");

        //=== the driver received exactly what was granted ===================
        @(negedge clk) req = 4'b0000;
        repeat (20) @(negedge clk);
        drive_for(40, 1);
        check(n_recv > 400, "the driver consumed a few hundred items");
        check(n_recv == n_offer || n_recv == n_offer - 1,
              "items received equals items granted (allowing one in flight)");
        mism = 0;
        for (i = 0; i < n_recv; i = i + 1)
            if (received[i] !== offered[i]) mism++;
        check(mism == 0, "and every one carried the granted sequence's payload");

        //=== grant hygiene ==================================================
        check(multi_gnt == 0, "never more than one sequence granted at a time");
        check(wide_gnt == 0,  "no grant pulse was ever wider than one cycle");

        //=== a slow driver does not lose items ==============================
        base_recv = n_recv;
        @(negedge clk) req = 4'b0101;
        drive_for(600, 12);                 // twelve cycles per item
        check(n_recv - base_recv > 20, "a slow driver still makes progress");
        mism = 0;
        for (i = base_recv; i < n_recv; i = i + 1)
            if (received[i] !== offered[i]) mism++;
        check(mism == 0, "and loses nothing while it is slow");

        //=== withdrawing a request ==========================================
        @(negedge clk) req = 4'b0000;
        repeat (4) @(negedge clk);
        drive_for(20, 1);
        base_recv = n_granted;
        repeat (20) @(negedge clk);
        check(n_granted == base_recv,
              "with no sequence asking, nothing is granted");
        check(drv_valid === 1'b0, "and the driver is left idle");

        $display("== %0d checks, %0d failures ==", checks, failures);
        if (failures == 0) $display("   RESULT: ALL SYSTEMVERILOG SEQ-ARB TESTS PASSED");
        else               $display("   RESULT: SYSTEMVERILOG SEQ-ARB TESTS FAILED");
        $finish;
    end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
--===========================================================================
--  tb_uart_seq_arb — self-checking VHDL-2008 testbench
--
--  The sequencer has two contracts and they fail in different ways:
--
--    THE HANDSHAKE.  An item placed in front of the driver stays there,
--                    unchanged, until the driver says item_done. A
--                    sequencer that moves on early loses transactions, and
--                    the loss is silent -- the driver simply never saw them.
--
--    THE ARBITRATION. Every requesting sequence eventually gets served. A
--                    fixed-priority arbiter satisfies every other check in
--                    this file and starves the last sequence forever.
--
--  INDEPENDENCE: the testbench records what each sequence OFFERED and what
--  the driver RECEIVED, and compares the two lists. It does not read the
--  arbiter's pointer to predict who should win.
--
--  Same 20 counted checks as the Verilog and SystemVerilog twins.
--===========================================================================
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;

entity tb_uart_seq_arb is
end entity tb_uart_seq_arb;

architecture sim of tb_uart_seq_arb is
    constant NSEQ : positive := 4;
    constant W    : positive := 9;
    constant TCLK : time     := 10 ns;

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

    signal req  : std_logic_vector(NSEQ-1 downto 0) := (others => '0');
    signal item : std_logic_vector(NSEQ*W-1 downto 0) := (others => '0');
    signal gnt  : std_logic_vector(NSEQ-1 downto 0);
    signal drv_item  : std_logic_vector(W-1 downto 0);
    signal drv_valid : std_logic;
    signal drv_done  : std_logic := '0';
    signal n_granted : natural;
    signal last_seq  : natural;

    type log_arr is array (0 to 1023) of std_logic_vector(W-1 downto 0);
    type tally   is array (0 to NSEQ-1) of natural;
    signal offered, received : log_arr := (others => (others => '0'));
    signal n_offer, n_recv   : natural := 0;
    signal served            : tally   := (others => 0);
    signal held_bad, wide_gnt, multi_gnt : natural := 0;
begin
    clk <= not clk after TCLK/2 when not done else '0';

    dut : entity work.uart_seq_arb
        generic map (NSEQ => NSEQ, W => W)
        port map (clk => clk, rst_n => rst_n,
                  req_i => req, item_i => item, gnt_o => gnt,
                  drv_item_o => drv_item, drv_valid_o => drv_valid,
                  drv_done_i => drv_done,
                  n_granted_o => n_granted, last_seq_o => last_seq);

    obs : process (clk)
        variable held_item  : std_logic_vector(W-1 downto 0) := (others => '0');
        variable held_valid : std_logic := '0';
        variable gnt_prev   : std_logic_vector(NSEQ-1 downto 0) := (others => '0');
        variable ones       : natural;
    begin
        if rising_edge(clk) and rst_n = '1' then
            ones := 0;
            for g in 0 to NSEQ-1 loop
                if gnt(g) = '1' then ones := ones + 1; end if;
            end loop;
            if ones > 1 then multi_gnt <= multi_gnt + 1; end if;
            if ones > 0 and gnt_prev /= (gnt_prev'range => '0') then
                wide_gnt <= wide_gnt + 1;
            end if;
            for g in 0 to NSEQ-1 loop
                if gnt(g) = '1' then
                    served(g) <= served(g) + 1;
                    offered(n_offer) <= item((g+1)*W-1 downto g*W);
                    n_offer <= n_offer + 1;
                end if;
            end loop;
            gnt_prev := gnt;

            -- the item in front of the driver must not change while held
            if held_valid = '1' and drv_valid = '1' and drv_item /= held_item then
                held_bad <= held_bad + 1;
            end if;
            held_item  := drv_item;
            held_valid := drv_valid;

            if drv_valid = '1' and drv_done = '1' then
                received(n_recv) <= drv_item;
                n_recv <= n_recv + 1;
            end if;
        end if;
    end process obs;

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

    stim : process
        variable checks, failures : natural := 0;
        variable base_recv, min_s, max_s, mism : natural;
        -- `served` is driven by the observer process, so the stimulus cannot
        -- zero it: a second driver on an unresolved type is illegal in VHDL.
        -- A baseline is better practice in every language anyway -- it asks
        -- "how many were served DURING the fairness test", which is the
        -- actual question.
        variable base_served : tally;

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

        -- A driver that takes `lat` cycles to process each item.
        procedure drive_for(ncycles : natural; lat : natural) is
            variable hold : natural := 0;
        begin
            for c in 1 to ncycles loop
                wait until falling_edge(clk);
                drv_done <= '0';
                if drv_valid = '1' then
                    hold := hold + 1;
                    if hold >= lat then drv_done <= '1'; hold := 0; end if;
                end if;
            end loop;
            wait until falling_edge(clk); drv_done <= '0';
        end procedure drive_for;
    begin
        report "== uart_seq_arb : self-checking VHDL testbench ==" severity note;
        rst_n <= '0';
        for i in 1 to 4 loop wait until falling_edge(clk); end loop;
        check(gnt = (gnt'range => '0') and drv_valid = '0',
              "reset: no grant and nothing in front of the driver");
        rst_n <= '1';
        for i in 1 to 2 loop wait until falling_edge(clk); end loop;

        --=== one sequence, one item =========================================
        for i in 0 to NSEQ-1 loop
            item((i+1)*W-1 downto i*W) <= std_logic_vector(to_unsigned(16#100# + i, W));
        end loop;
        wait until falling_edge(clk); req(0) <= '1';
        for i in 1 to 3 loop wait until falling_edge(clk); end loop;
        check(drv_valid = '1', "a lone request reaches the driver");
        check(drv_item = std_logic_vector(to_unsigned(16#100#, W)),
              "carrying that sequence's item");
        check(n_granted = 1, "and is granted exactly once");

        --=== THE blocking property ==========================================
        wait until falling_edge(clk); req <= "1111";
        for i in 1 to 20 loop wait until falling_edge(clk); end loop;
        check(n_granted = 1,
              "20 cycles with every sequence asking: still ONE grant");
        check(drv_item = std_logic_vector(to_unsigned(16#100#, W)),
              "and the held item never changed");
        check(held_bad = 0, "the item in front of the driver is stable");

        --=== release, and the next one is served ============================
        wait until falling_edge(clk); drv_done <= '1';
        wait until falling_edge(clk); drv_done <= '0';
        for i in 1 to 3 loop wait until falling_edge(clk); end loop;
        check(n_granted = 2, "item_done releases the sequencer");
        check(last_seq /= 0, "and the next grant goes to a DIFFERENT sequence");

        --=== fairness under full contention =================================
        req <= "1111";
        wait until falling_edge(clk);
        base_served := served;
        drive_for(2000, 3);
        min_s := served(0) - base_served(0);
        max_s := min_s;
        for i in 1 to NSEQ-1 loop
            if served(i) - base_served(i) < min_s then
                min_s := served(i) - base_served(i);
            end if;
            if served(i) - base_served(i) > max_s then
                max_s := served(i) - base_served(i);
            end if;
        end loop;
        report "  [info] served: "
             & integer'image(served(0) - base_served(0)) & " "
             & integer'image(served(1) - base_served(1)) & " "
             & integer'image(served(2) - base_served(2)) & " "
             & integer'image(served(3) - base_served(3)) severity note;
        check(min_s > 0, "under full contention EVERY sequence is served");
        check(max_s - min_s <= 1,
              "and they are served equally -- round robin, not priority");

        --=== the driver received exactly what was granted ===================
        wait until falling_edge(clk); req <= "0000";
        for i in 1 to 20 loop wait until falling_edge(clk); end loop;
        drive_for(40, 1);
        check(n_recv > 400, "the driver consumed a few hundred items");
        check(n_recv = n_offer or n_recv = n_offer - 1,
              "items received equals items granted (allowing one in flight)");
        mism := 0;
        for i in 0 to n_recv-1 loop
            if received(i) /= offered(i) then mism := mism + 1; end if;
        end loop;
        check(mism = 0, "and every one carried the granted sequence's payload");

        --=== grant hygiene ==================================================
        check(multi_gnt = 0, "never more than one sequence granted at a time");
        check(wide_gnt = 0,  "no grant pulse was ever wider than one cycle");

        --=== a slow driver does not lose items ==============================
        base_recv := n_recv;
        wait until falling_edge(clk); req <= "0101";
        drive_for(600, 12);
        check(n_recv - base_recv > 20, "a slow driver still makes progress");
        mism := 0;
        for i in base_recv to n_recv-1 loop
            if received(i) /= offered(i) then mism := mism + 1; end if;
        end loop;
        check(mism = 0, "and loses nothing while it is slow");

        --=== withdrawing a request ==========================================
        wait until falling_edge(clk); req <= "0000";
        for i in 1 to 4 loop wait until falling_edge(clk); end loop;
        drive_for(20, 1);
        base_recv := n_granted;
        for i in 1 to 20 loop wait until falling_edge(clk); end loop;
        check(n_granted = base_recv,
              "with no sequence asking, nothing is granted");
        check(drv_valid = '0', "and the driver is left idle");

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

Twenty checks, identical across all three languages — and the fairness tally comes out the same in each:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  PASS a lone request reaches the driver
  PASS 20 cycles with every sequence asking: still ONE grant
  PASS and the held item never changed
  PASS the item in front of the driver is stable
  PASS item_done releases the sequencer
  PASS and the next grant goes to a DIFFERENT sequence
  [info] served: 125 124 125 125
  PASS under full contention EVERY sequence is served
  PASS and they are served equally -- round robin, not priority
  PASS items received equals items granted (allowing one in flight)
  PASS and every one carried the granted sequence's payload
  PASS never more than one sequence granted at a time
  PASS a slow driver still makes progress
  PASS and loses nothing while it is slow
== 20 checks, 0 failures ==

Verilog-2001    : 20 / 0    served 125 124 125 125
SystemVerilog   : 20 / 0    served 125 124 125 125
VHDL-2008       : 20 / 0    served 125 124 125 125

5. Mutation

Four defects installed in the sequencer, each verified to have changed the source before scoring:

#Defect installedResult
M1fixed priority — the round-robin pointer never moveskilled, 3
M2the item released without waiting for item_donekilled, 8
M3the grant held instead of pulsed for one cyclekilled, 6
M4the pointer set to pick rather than pick + 1killed, 3

M1 and M4 are worth comparing. Both break fairness; neither breaks anything else. M1 starves sequences 1 through 3 completely; M4 serves the same sequence twice in a row before moving on, which is a much milder unfairness — and both are caught by the same check, max_s - min_s <= 1, because that check asserts equality rather than merely "everyone got something".

A weaker check — min_s > 0, that nobody starved — catches M1 and lets M4 through. The suite has both, and the stronger one is the one that earns its place.

6. Verification

Test that nothing happens while the driver is busy. It is the only check that distinguishes a sequencer from a multiplexer, and it looks like a test of nothing.

Assert equality of service, not merely absence of starvation. min > 0 catches a fixed-priority arbiter and misses a pointer that advances by the wrong amount.

Compare what was offered against what was received, item by item. The sequencer's payload path is a place where a byte can be silently substituted, and only a list comparison sees it.

Check the grant is one cycle wide. A held grant means a sequence believes it has been served repeatedly, and it will send items nobody consumes.

Vary the driver's latency. A sequencer that works with a one-cycle driver and loses items with a twelve-cycle one has a handshake bug that a fast driver hides.

And use absolute time in the driver, never a clocking block. A driver synchronised to the DUT's clock cannot be wrong on purpose, and cannot test tolerance.

7. Debugging

8. Understanding Check

9. Summary

A UART driver has no clock to synchronise to — it is standing in for the far end's oscillator, so its delays are absolute times and a clocking block would make tolerance testing impossible.

A sequencer is an arbiter with a blocking handshake, and it has exactly two contracts: it holds, and it is fair.

The holding contract is the one that distinguishes it from a multiplexer, and the check that tests it looks like a test of nothing.

Fairness must be asserted as equality, not as the absence of starvation — a pointer that advances by the wrong amount passes the weaker form.

Twenty checks in each of three languages, 0 failures, with the same 125/124/125/125 tally in all three.

Four mutants, four killed, the largest blast radius belonging to the released-early handshake at eight failing checks.

10. What Comes Next

Chapter 16.4 does the same for the monitor: the analysis port that lets it broadcast without knowing who is listening, built as running code — and a measurement of the property that makes it dangerous.

Browse the full path on the UART tutorials index. For the transaction this driver consumes, read back to Chapter 16.2.

Continue learning

Where this fits

Part of the UART curriculum.