When there is no interface

Drive a design through a legacy BFM module that has no interface by passing an abstract class handle, give each instance its own handle, and explain why emulation splits a bench into two top modules.

Lesson
U12.3
Time
25 min
Runs on
Verilator with UVM
Check
5 questions

The lane design arrives with a bus functional model (BFM) from another team: a Verilog module whose tasks write and wait_result are the supported way to use the pins. A module is not an interface, so virtual legacy_bfm does not compile, and the team will not convert the file. The UVM driver, a class, still has to call those tasks.

A handle instead of a virtual interface

A class handle where a virtual interface cannot goArrows are calls, except set and get on the config database: get returns a copy of the stored handle and the entry stays for any other reader.
A class handle where a virtual interface cannot goArrows are calls, except set and get on the config database: `get` returns a copy of the stored handle and the entry stays for any other reader. hvl bfm_driver Holds lane_bfm_apibfm; calls bfm.send(x) hvl bfm_monitor Calls next_input,next_result config store uvm_config_db Entry "bfm" foruvm_test_top.lane0;get leaves it in place hdl lane_bfm_proxy Concrete class insidelane_wrap hdl legacy_bfm Module tasks write,wait_result design incr_rtl pins valid, x, out_valid, y set at time 0get returns thehandlesend(x)bfm.write(x)drivesnext_result
  1. Publish0 ns (illustrative)

    At time 0, each lane_wrap creates its proxy and stores it as a lane_bfm_api for its own agent's path.

    More context

    Lane 0's wrapper stores at "uvm_test_top.lane0" and lane 1's at "uvm_test_top.lane1". A shared scope such as "uvm_test_top.*" would let the later set win for both agents.

    Follow the calls, values and waits
    1. data lane_bfm_api handle: lane_bfm_proxy → uvm_config_db
  2. Fetch

    In build_phase the agent gets the handle and gives it to its driver and monitor. They know only the abstract class.

    More context

    get copies the handle; the entry stays in the database. The run's first line, "agent lane0 drives lane0; agent lane1 drives lane1", confirms each agent got its own lane.

    Follow the calls, values and waits
    1. data lane_bfm_api handle: uvm_config_db → bfm_driver
  3. Call10 ns (illustrative)

    bfm.send(10) dispatches to the proxy, which calls bfm.write(10) in the same module instance. The task drives the pins at 10 ns.

    More context

    write waits for the falling edge at 10 ns, then raises valid with x = 10. The driver's call is still open: the module task holds it.

    Follow the calls, values and waits
    1. call send(10): bfm_driver → lane_bfm_proxy
    2. call bfm.write(10): lane_bfm_proxy → legacy_bfm
    3. data valid, x = 10: legacy_bfm → incr_rtl pins
    4. Waiting: bfm_driver — in bfm.send(10)
  4. Return20 ns (illustrative)

    write returns at the next falling edge, 20 ns, so send returns to the driver, which can take its next item.

    More context

    Measured by the predict item: "send returned at 20 and 40 ns". The design registered the beat at the rising edge of 15 ns.

    Follow the calls, values and waits
    1. return task returns: legacy_bfm → lane_bfm_proxy
    2. return task returns: lane_bfm_proxy → bfm_driver
    3. Released: bfm_driver
  5. Observe25 ns (illustrative)

    The monitor's next_result runs inside the wrapper too, so it can wait on the wrapper's wires and the BFM's wait_result.

    More context

    wait_result sees out_valid at the rising edge of 25 ns and the call returns 11; the scoreboard logs "25 ns lane0 y=11 matches".

    Follow the calls, values and waits
    1. call next_result(r): bfm_monitor → lane_bfm_proxy
    2. return r = 11: lane_bfm_proxy → bfm_monitor
Read the diagram as text
  • bfm_driver: Holds lane_bfm_api bfm; calls bfm.send(x)
  • bfm_monitor: Calls next_input, next_result
  • uvm_config_db: Entry "bfm" for uvm_test_top.lane0; get leaves it in place
  • lane_bfm_proxy: Concrete class inside lane_wrap
  • legacy_bfm: Module tasks write, wait_result
  • incr_rtl pins: valid, x, out_valid, y
  • lane_bfm_proxy → uvm_config_db: set at time 0
  • uvm_config_db → bfm_driver: get returns the handle
  • bfm_driver → lane_bfm_proxy: send(x)
  • lane_bfm_proxy → legacy_bfm: bfm.write(x)
  • legacy_bfm → incr_rtl pins: drives
  • bfm_monitor → lane_bfm_proxy: next_result

The concrete class is the only code that knows both worlds. The agent package never names a module.

The fix is the abstract/concrete class pattern. The agent's package declares an abstract class with pure virtual tasks: what a lane can do, with no pins in it. A concrete class that implements those tasks is declared inside the wrapper module, next to the BFM. An object of it travels to the agent through the config database, typed as the abstract class.

The driver then calls bfm.send(req.x) exactly as it would call a task through a virtual interface. Dispatch lands in the concrete class, which runs inside the wrapper and can call bfm.write there.

The abstract class

Everything the UVM side may ask of a lane.
virtual class lane_bfm_api;
  pure virtual task send(bit [7:0] v);                // drive one beat
  pure virtual task next_input(output bit [7:0] v);   // the next beat the design takes
  pure virtual task next_result(output bit [7:0] r);  // the next result
  pure virtual function string where();
endclass

virtual class means no object of it can be created. pure virtual means every class that extends it and is meant to be created must implement the method. A wrapper that forgets one fails to compile, which is much better than a lane that does nothing.

Keep the abstract class small and transaction-level: one beat, one result. If the driver needs a new capability later, the abstract class and every concrete class change together.

The concrete class lives with the pins

The wrapper. bfm, clk, valid and x are names in this module instance.
module lane_wrap #(parameter int LANE = 0) (input logic clk);
  // ... the design, the BFM and their wires
  class lane_bfm_proxy extends lane_bfm_api;
    task send(bit [7:0] v); bfm.write(v); endtask
    task next_input(output bit [7:0] v);
      do @(posedge clk); while (!valid);
      v = x;
    endtask
    // next_result and where() as in the example
  endclass
  lane_bfm_proxy proxy;
  initial begin
    proxy = new;
    uvm_config_db #(lane_bfm_api)::set(null, $sformatf("uvm_test_top.lane%0d", LANE), "bfm", proxy);
  end
endmodule

A class declared inside a module can use that module's names: its wires, its parameters, and the tasks of instances inside it. Each instance of lane_wrap therefore has its own lane_bfm_proxy, bound to its own BFM. The example has two lanes, and where() reports which lane a handle reaches.

Each wrapper stores its handle at its own agent's path. Store both at a shared scope such as "uvm_test_top.*", and the later set replaces the earlier one. One agent then drives the other lane's pins, and the type system cannot notice.

The BFM keeps its timing

A legacy BFM decides when its task returns, and the driver waits for it. write raises valid at one falling edge and lowers it at the next, so each beat costs two falling edges. The next call waits for another falling edge, leaving one idle cycle between beats.

Lane 0: two sends through the BFM
01234567from (ns)1015202530354045validx1011out_validsend10-returns-11-returns-25 ns edge samples y=1145 ns edge samples y=12

Each column holds a value from that time until the next. Falling edges at 10, 20, 30, 40 ns; rising edges at 15, 25, 35, 45 ns. A rising edge samples the column before it, so the result shown from 15 ns is seen at 25 ns.

Two consequences follow. A pipelined or back-to-back sequence cannot run faster than the BFM allows, whatever the driver does. And a module task is static by default, so two threads calling write at once share its argument. Keep one caller per BFM.

Dual top, protocol modules and emulation

An emulation partition (not the runnable example)Left: the workstation. Right: compiled into the emulator. Only messages cross between them.
An emulation partition (not the runnable example)Left: the workstation. Right: compiled into the emulator. Only messages cross between them. workstation Tests and agents Unchanged: calllane_bfm_api workstation Host proxy class Implementslane_bfm_api; turnscalls into messages link Message link Provided by theemulation flow emulator Clock and reset Generated inhardware emulator Transactor Synthesizable;replaces the BFMtasks emulator DUT Synthesizable RTL send(x)messagemessagepinsclk
  1. The bench calls the API

    An agent calls send(x) on its lane_bfm_api handle, exactly as in simulation. The handle now refers to a host proxy class on the workstation.

    More context

    Only the concrete class changes. The agent package and the abstract class are the same ones the runnable example uses.

    Follow the calls, values and waits
    1. call send(x): Tests and agents → Host proxy class
  2. The call crosses as a message

    The host proxy turns the call into a message, and the emulation flow carries it to the transactor.

    More context

    No class handle and no hierarchical reference crosses this boundary; on an emulator they have no meaning on the hardware side.

    Follow the calls, values and waits
    1. data message: x: Host proxy class → Message link
    2. data message: x: Message link → Transactor
  3. Hardware drives the pins

    The synthesizable transactor, clocked in the emulator, puts x on the DUT's pins.

    More context

    The clock and reset are generated in hardware, not by an always block in hdl_top. Nothing on this side is measured here: no emulator is available to this runner.

    Follow the calls, values and waits
    1. data clock edges: Clock and reset → Transactor
    2. data x on the pins: Transactor → DUT
Read the diagram as text
  • Tests and agents: Unchanged: call lane_bfm_api
  • Host proxy class: Implements lane_bfm_api; turns calls into messages
  • Message link: Provided by the emulation flow
  • Clock and reset: Generated in hardware
  • Transactor: Synthesizable; replaces the BFM tasks
  • DUT: Synthesizable RTL
  • Tests and agents → Host proxy class: send(x)
  • Host proxy class → Message link: message
  • Message link → Transactor: message
  • Transactor → DUT: pins
  • Clock and reset → Transactor: clk

Conceptual. In the runnable example, lane_bfm_proxy and the BFM tasks still sit in hdl_top; that is a simulation arrangement. The table below lists what must move or be rewritten.

Dual top puts the hardware in one top module, hdl_top, and the class-based bench in another, hvl_top, which calls run_test. In the example the two meet only through the config database. That is a simulation arrangement: hdl_top still holds the class lane_bfm_proxy and the BFM's behavioural tasks.

Emulation is why the split matters. An emulator compiles the hardware side, which must then be synthesizable and free of class code. The figure shows that partition: the proxy moves to the workstation, the BFM becomes a synthesizable transactor, and each call crosses as a message. A hierarchical reference from a class into the DUT has no meaning there.

PieceRunnable example (simulation)Emulation partition
Agent, driver, monitor, lane_bfm_apiIn hvl_topUnchanged
Concrete classlane_bfm_proxy, inside lane_wrap in hdl_topA host proxy on the workstation that sends messages
BFMBehavioural tasks that wait on @(negedge clk)A synthesizable transactor driven by messages
Hand-offconfig_db set from lane_wrap's initial blockThe bench creates the host proxy; the emulation flow links it to the transactor
ClockAn always block in hdl_topGenerated in the emulator

What must change to emulate. The UVM side stays as it is, which is the payoff of reaching the pins only through lane_bfm_api.

A protocol module wraps one DUT interface with what belongs to it: the interface or BFM, assertions, and the code that registers the handle. A change in how the bench connects stays inside that module and never reaches the top. The example's lane_wrap is a small protocol module.

Try it

The first line comes from end_of_elaboration_phase: each agent's handle reaches its own lane. Then each scoreboard checks its two results.

Change the scope in lane_wrap to "uvm_test_top.*" and rerun: both agents get the same handle. Then delete the next_result task from lane_bfm_proxy and see the compiler refuse the class.

RUNVerilator + UVM 2020.3.1 · simulator service
Edit the code, then run it
Expected among the UVM messages: agent lane0 drives lane0; agent lane1 drives lane1 25 ns lane0 y=11 matches 25 ns lane1 y=21 matches 45 ns lane0 y=12 matches 45 ns lane1 y=22 matches UVM_ERROR : 0

Check yourself

Predict the outputThis test is the example's, with a report of when lane 0's driver saw each send return and when its monitor saw each result. The clock's rising edges are at 5, 15, 25 ns and so on. What line does the $display print?
`timescale 1ns/1ns
`include "uvm_macros.svh"
import uvm_pkg::*;

// The design: a registered incrementer with plain ports and no interface.
module incr_rtl (input  logic       clk,
                 input  logic       valid,
                 input  logic [7:0] x,
                 output logic       out_valid,
                 output logic [7:0] y);
  initial begin
    out_valid = 0;
    y = 0;
  end
  always @(posedge clk) begin
    out_valid <= valid;
    y         <= x + 1;
  end
endmodule

// A legacy bus functional model from another team. Its tasks are the supported way to use the
// pins, and the file may not be edited. A module cannot be reached by a virtual interface.
module legacy_bfm (input  logic       clk,
                   output logic       valid,
                   output logic [7:0] x,
                   input  logic       out_valid,
                   input  logic [7:0] y);
  initial begin
    valid = 0;
    x = 0;
  end
  task write(input logic [7:0] v);          // one beat: valid high from one falling edge to the next
    @(negedge clk);
    valid = 1;
    x     = v;
    @(negedge clk);
    valid = 0;
  endtask
  task wait_result(output logic [7:0] r);   // the next result, at the rising edge that shows it
    do @(posedge clk); while (!out_valid);
    r = y;
  endtask
endmodule

virtual class lane_bfm_api;
  pure virtual task send(bit [7:0] v);                    // drive one beat
  pure virtual task next_input(output bit [7:0] v);       // the next beat the design takes
  pure virtual task next_result(output bit [7:0] r);      // the next result
  pure virtual function string where();
endclass

// The wrapper holds the design, the BFM and the concrete class. The class is declared inside the
// module, so it can call this instance's BFM tasks and read this instance's wires directly.
module lane_wrap #(parameter int LANE = 0) (input logic clk);
  logic       valid, out_valid;
  logic [7:0] x, y;
  incr_rtl   dut (.clk(clk), .valid(valid), .x(x), .out_valid(out_valid), .y(y));
  legacy_bfm bfm (.clk(clk), .valid(valid), .x(x), .out_valid(out_valid), .y(y));

  class lane_bfm_proxy extends lane_bfm_api;
    task send(bit [7:0] v);
      bfm.write(v);
    endtask
    task next_input(output bit [7:0] v);
      do @(posedge clk); while (!valid);
      v = x;
    endtask
    task next_result(output bit [7:0] r);
      logic [7:0] t;
      bfm.wait_result(t);
      r = t;
    endtask
    function string where();
      return $sformatf("lane%0d", LANE);
    endfunction
  endclass

  lane_bfm_proxy proxy;
  initial begin
    proxy = new;
    uvm_config_db #(lane_bfm_api)::set(null, $sformatf("uvm_test_top.lane%0d", LANE), "bfm", proxy);
  end
endmodule

class incr_item extends uvm_sequence_item;
  `uvm_object_utils(incr_item)
  bit [7:0] x;
  function new(string name = "incr_item"); super.new(name); endfunction
endclass

class incr_seq extends uvm_sequence #(incr_item);
  `uvm_object_utils(incr_seq)
  bit [7:0] values[$];
  function new(string name = "incr_seq"); super.new(name); endfunction
  task body();
    foreach (values[i]) begin
      incr_item it = incr_item::type_id::create("it");
      start_item(it);
      it.x = values[i];
      finish_item(it);
    end
  endtask
endclass

// The driver knows only the abstract class. The BFM decides the timing.
class bfm_driver extends uvm_driver #(incr_item);
  `uvm_component_utils(bfm_driver)
  lane_bfm_api bfm;
  time         returned[$];                 // when each send call returned
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  task run_phase(uvm_phase phase);
    forever begin
      seq_item_port.get_next_item(req);
      bfm.send(req.x);
      returned.push_back($time);
      seq_item_port.item_done();
    end
  endtask
endclass

class bfm_monitor extends uvm_monitor;
  `uvm_component_utils(bfm_monitor)
  lane_bfm_api bfm;
  uvm_analysis_port #(incr_item) in_ap, out_ap;   // out_ap carries y in the x field
  time         result_at[$];
  function new(string name, uvm_component parent);
    super.new(name, parent);
    in_ap  = new("in_ap", this);
    out_ap = new("out_ap", this);
  endfunction
  task run_phase(uvm_phase phase);
    fork
      forever begin
        incr_item t = incr_item::type_id::create("t");
        bfm.next_input(t.x);
        in_ap.write(t);
      end
      forever begin
        incr_item r = incr_item::type_id::create("r");
        bfm.next_result(r.x);
        result_at.push_back($time);
        out_ap.write(r);
      end
    join
  endtask
endclass

class bfm_agent extends uvm_agent;
  `uvm_component_utils(bfm_agent)
  lane_bfm_api               bfm;
  uvm_sequencer #(incr_item) sqr;
  bfm_driver                 drv;
  bfm_monitor                mon;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    if (!uvm_config_db #(lane_bfm_api)::get(this, "", "bfm", bfm))
      `uvm_fatal("NOBFM", "no lane_bfm_api handle for this agent")
    sqr = uvm_sequencer #(incr_item)::type_id::create("sqr", this);
    drv = bfm_driver::type_id::create("drv", this);
    mon = bfm_monitor::type_id::create("mon", this);
    drv.bfm = bfm;
    mon.bfm = bfm;
  endfunction
  function void connect_phase(uvm_phase phase);
    drv.seq_item_port.connect(sqr.seq_item_export);
  endfunction
endclass

`uvm_analysis_imp_decl(_in)
`uvm_analysis_imp_decl(_out)

class bfm_scoreboard extends uvm_scoreboard;
  `uvm_component_utils(bfm_scoreboard)
  uvm_analysis_imp_in  #(incr_item, bfm_scoreboard) in_imp;
  uvm_analysis_imp_out #(incr_item, bfm_scoreboard) out_imp;
  string    lane;
  bit [7:0] expected[$];
  int       matched;
  function new(string name, uvm_component parent);
    super.new(name, parent);
    in_imp  = new("in_imp", this);
    out_imp = new("out_imp", this);
  endfunction
  function void write_in(incr_item t);
    expected.push_back(t.x + 1);
  endfunction
  function void write_out(incr_item r);
    bit [7:0] e;
    if (expected.size() == 0) begin
      `uvm_error("SB", $sformatf("%s: y=%0d with nothing predicted", lane, r.x))
      return;
    end
    e = expected.pop_front();
    if (r.x == e) begin
      matched++;
    end
    else `uvm_error("SB", $sformatf("%s: y=%0d, predicted %0d", lane, r.x, e))
  endfunction
  function void check_phase(uvm_phase phase);
    if (expected.size() != 0)
      `uvm_error("SB", $sformatf("%s: %0d prediction(s) never met a result", lane, expected.size()))
  endfunction
endclass

class bfm_test extends uvm_test;
  `uvm_component_utils(bfm_test)
  bfm_agent      lane0, lane1;
  bfm_scoreboard sb0, sb1;
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void build_phase(uvm_phase phase);
    lane0 = bfm_agent::type_id::create("lane0", this);
    lane1 = bfm_agent::type_id::create("lane1", this);
    sb0   = bfm_scoreboard::type_id::create("sb0", this);
    sb1   = bfm_scoreboard::type_id::create("sb1", this);
    sb0.lane = "lane0";
    sb1.lane = "lane1";
  endfunction
  function void connect_phase(uvm_phase phase);
    lane0.mon.in_ap.connect(sb0.in_imp);
    lane0.mon.out_ap.connect(sb0.out_imp);
    lane1.mon.in_ap.connect(sb1.in_imp);
    lane1.mon.out_ap.connect(sb1.out_imp);
  endfunction
  task run_phase(uvm_phase phase);
    incr_seq s0 = incr_seq::type_id::create("s0");
    incr_seq s1 = incr_seq::type_id::create("s1");
    s0.values = '{10, 11};
    s1.values = '{20, 21};
    phase.raise_objection(this);
    fork
      s0.start(lane0.sqr);
      s1.start(lane1.sqr);
    join
    #20;                                       // past the edge where the last result is shown
    phase.drop_objection(this);
  endtask
endclass

class timing_test extends bfm_test;
  `uvm_component_utils(timing_test)
  function new(string name, uvm_component parent); super.new(name, parent); endfunction
  function void report_phase(uvm_phase phase);
    $display("lane0: send returned at %0d and %0d ns, results at %0d and %0d ns",
             lane0.drv.returned[0], lane0.drv.returned[1], lane0.mon.result_at[0], lane0.mon.result_at[1]);
  endfunction
endclass

module hdl_top;
  bit clk;
  always #5 clk = ~clk;
  lane_wrap #(.LANE(0)) lane0 (clk);
  lane_wrap #(.LANE(1)) lane1 (clk);
endmodule

module hvl_top;
  initial begin
    uvm_root::get().set_timeout(2000ns, 1);
    run_test("timing_test");
  end
endmodule

// This runner elaborates one top module, tb. A simulator that accepts several tops runs
// hdl_top and hvl_top directly. Nothing in hvl_top names anything inside hdl_top.
module tb;
  hdl_top hdl ();
  hvl_top hvl ();
endmodule
Spot the bugEach option is the scope the wrappers use when they store their handles, uvm_config_db #(lane_bfm_api)::set(null, SCOPE, "bfm", proxy). The agents live at uvm_test_top.lane0 and uvm_test_top.lane1. Which scope sends an agent to the wrong lane?
ConceptWhy does this bench pass a class handle to the agent instead of a virtual interface?
ConceptA team plans to move its bench onto an emulator. Why does that push it toward dual top and protocol modules?
ConceptA new pure virtual task, reset_pins(), is added to lane_bfm_api, but nobody updates lane_bfm_proxy. When does the problem appear?

Remember

  • When the bench must call tasks of a module that is not an interface, pass a handle to an abstract class with pure virtual tasks instead of a virtual interface.
  • Declare the concrete class inside the wrapper module: it can call that instance's BFM tasks and read its wires, and each wrapper instance gets its own class.
  • Store each handle at its own agent's path; a shared scope lets the later set win and sends an agent to the wrong lane.
  • A legacy BFM keeps its own timing and, as a static module task, one caller at a time.
  • Dual top separates synthesizable hardware from the class-based bench so the hardware side can run in an emulator; protocol modules keep each interface's connection details together.