Part 6 · Agents & Protocol IP · Intermediate

Agent Wrapper Role: Contract First, Internals Second

Define what the wrapper owns and exposes, enforce encapsulation, and keep public APIs stable while internal classes evolve.

Wrapper as integration contract

The wrapper should expose only the surfaces consumers need: start traffic through sequencer, observe traffic through analysis ports, and set behavior through cfg.

diagram
[AGT] public contract

publish:
  - sequencer handle (active usage)
  - agent-level analysis port(s)
  - cfg summary/introspection APIs

hide:
  - monitor internal sampling logic
  - driver protocol micro-steps
  - internal helper queues/state
diagram
[UVM][AGT] boundary payoff

stable API:
  env/test code remains unchanged across refactors

hidden internals:
  monitor/driver implementation can evolve independently
  • Expose capabilities, not implementation details.

  • Treat wrapper API as versioned contract for VIP users.

  • Hide child internals unless explicit extension point is required.


Canonical wrapper implementation

systemverilog
class can_agent extends uvm_agent;
  `uvm_component_utils(can_agent)

  // public surface
  can_sequencer sqr;
  uvm_analysis_port #(can_item) ap;
  can_cfg cfg;

  // private internals
  local can_driver drv;
  local can_monitor mon;

  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#(can_cfg)::get(this, "", "cfg", cfg))
      `uvm_fatal("NOCFG", "missing can_cfg")

    ap = new("ap", this);
    mon = can_monitor::type_id::create("mon", this);
    uvm_config_db#(can_cfg)::set(this, "mon", "cfg", cfg);

    if (cfg.is_active == UVM_ACTIVE) begin
      sqr = can_sequencer::type_id::create("sqr", this);
      drv = can_driver::type_id::create("drv", this);
      uvm_config_db#(can_cfg)::set(this, "drv", "cfg", cfg);
    end
  endfunction

  function void connect_phase(uvm_phase phase);
    super.connect_phase(phase);
    mon.ap.connect(ap);
    if (cfg.is_active == UVM_ACTIVE)
      drv.seq_item_port.connect(sqr.seq_item_export);
  endfunction
endclass
diagram
[AGT] integration usage

tests:
  seq.start(env.can_agt.sqr)

checkers:
  env.can_agt.ap.connect(sb.actual_in)

no direct env/test dependence on env.can_agt.mon internals
  • Use factory create for all child components.

  • Re-export monitor stream via wrapper-level analysis ports.

  • Keep mode handling centralized in wrapper build logic.


Extension without leakage

Encapsulation still allows extension through factory overrides and documented wrapper hooks. The key is to avoid exposing raw child handles as external dependencies.

systemverilog
class trace_can_monitor extends can_monitor;
  `uvm_component_utils(trace_can_monitor)
  virtual function void emit_trace(can_item tr);
    `uvm_info("CAN_TRACE", tr.convert2string(), UVM_MEDIUM)
  endfunction
endclass

initial begin
  uvm_factory::get().set_type_override_by_type(
    can_monitor::get_type(),
    trace_can_monitor::get_type()
  );
end
diagram
[UVM][AGT] extension strategy

override children via factory
    ▼
wrapper public API unchanged
    ▼
existing tests/env wiring remain stable

Key takeaways

  • Wrapper role is to define and protect the integration contract.

  • Encapsulation enables safer refactors and controlled extension.

  • Public API should stay minimal: cfg, sequencer path, analysis path.

  • Factory overrides work best with stable wrapper boundaries.

Common pitfalls

  • Publishing driver/monitor internals as external dependencies.

  • Embedding test-specific behavior directly into wrapper core code.

  • Bypassing wrapper analysis export and wiring consumers to hidden ports.

  • Using global mutable state instead of cfg-forwarded behavior.