Strategy Pattern in UVM: Protocol-Agnostic Traffic with Adapters and Strategy Classes

The Observer pattern showed us how to decouple notifications — letting components broadcast events without knowing who listens. Now we look at the next Behavioral pattern: Strategy — how to decouple how an algorithm runs from the code that calls it.

The Problem: Tests Chained to Protocols

You have a register test that runs perfectly on your APB peripheral agent. Management asks for AXI support. Then iLink. Do you copy the test three times?

// BAD: three near-identical sequences, one per protocol
class apb_mem_write_seq extends uvm_sequence;
  task body();
    apb_seq_item item = apb_seq_item::type_id::create("item");
    item.addr  = 32'h1000;
    item.data  = 32'hDEAD_BEEF;
    item.write = 1;
    item.psel  = 1; item.penable = 0;  // APB-specific
    start_item(item); finish_item(item);
  endtask
endclass

class axi_mem_write_seq extends uvm_sequence;
  task body();
    axi_seq_item item = axi_seq_item::type_id::create("item");
    item.addr      = 32'h1000;
    item.data      = 32'hDEAD_BEEF;
    item.write     = 1;
    item.awvalid   = 1; item.wstrb = 4'hF;  // AXI-specific
    start_item(item); finish_item(item);
  endtask
endclass
// ... and a third for iLink
  • The same test intent (write 0xDEADBEEF to address 0x1000) is duplicated across protocol-specific sequences
  • Adding iLink means writing a third sequence, a third test, and three times the maintenance
  • A bug in the write logic must be fixed in three places
  • Sequences are untestable in isolation — they can only run against a specific agent type

What you want is a sequence that describes what to transfer — address, data, operation — and a separate object that knows how to translate that into APB, AXI, or iLink bus cycles. That separation is the Strategy pattern. And you've already used it every time you wrote a uvm_reg_adapter.

Gang of Four: The Strategy Pattern

Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from the clients that use it.

classDiagram
    class Context {
        -strategy: Strategy
        +setStrategy(s: Strategy)
        +executeAlgorithm()
    }
    class Strategy {
        <<interface>>
        +algorithm()
    }
    class ConcreteStrategyA {
        +algorithm()
    }
    class ConcreteStrategyB {
        +algorithm()
    }
    class ConcreteStrategyC {
        +algorithm()
    }
    Context o--> Strategy
    Strategy <|.. ConcreteStrategyA
    Strategy <|.. ConcreteStrategyB
    Strategy <|.. ConcreteStrategyC

Context holds a reference to a Strategy interface and delegates all algorithm work to it — Context never knows the concrete type behind that reference. Because every ConcreteStrategy implements the same interface, they are fully interchangeable: swapping the strategy object at runtime changes Context's behavior entirely without touching a single line of Context's own code.

Pattern Algorithm location Varies how? Runtime swap?
Strategy Separate class, injected into Context Via composition — swap the whole object Yes — set a new strategy at any time
Template Method Base class skeleton + overridden steps in subclasses Via inheritance — subclass overrides steps No — fixed at class definition time

Rule of thumb: if you need to swap the whole algorithm at runtime without subclassing the Context, reach for Strategy. If the overall structure is fixed and you only need to vary a few steps, Template Method is simpler.

UVM's Strategy: uvm_reg_adapter Revealed

Every time you extended uvm_reg_adapter and implemented reg2bus() and bus2reg(), you wrote a Strategy. The register map is the Context. The adapter is the Strategy. The APB adapter, the AXI adapter, the iLink adapter are ConcreteStrategies.

GoF Role RAL mapping
Context uvm_reg_map — drives the operation, delegates protocol translation
Strategy (abstract) uvm_reg_adapter — defines reg2bus() / bus2reg()
ConcreteStrategyA apb_adapter extends uvm_reg_adapter
ConcreteStrategyB axi_adapter extends uvm_reg_adapter
ConcreteStrategyC ilink_adapter extends uvm_reg_adapter
Algorithm call map.do_bus_write() calls adapter.reg2bus() internally
// The Strategy interface — two abstract methods every adapter must implement
virtual class uvm_reg_adapter extends uvm_object;

  // reg2bus: translate abstract register op → protocol-specific sequence item
  // rw.kind  = UVM_READ or UVM_WRITE
  // rw.addr  = register address
  // rw.data  = write data (UVM_WRITE) or filled by bus2reg (UVM_READ)
  pure virtual function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);

  // bus2reg: translate protocol response → abstract register op
  pure virtual function void bus2reg(uvm_sequence_item bus_item,
                                     ref uvm_reg_bus_op rw);

  // Optional flags — part of the Strategy interface
  bit supports_byte_enable;  // set if protocol supports byte-lane enables
  int unsigned byte_enables_size;  // width of byte enable bus
endclass
class apb_adapter extends uvm_reg_adapter;
  `uvm_object_utils(apb_adapter)

  function new(string name = "apb_adapter");
    super.new(name);
    supports_byte_enable = 0;
  endfunction

  // ConcreteStrategy: translate to APB — single-phase handshake
  virtual function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
    apb_seq_item item = apb_seq_item::type_id::create("item");
    item.paddr  = rw.addr;
    item.pwdata = rw.data;
    item.pwrite = (rw.kind == UVM_WRITE);
    item.psel   = 1;
    return item;
  endfunction

  virtual function void bus2reg(uvm_sequence_item bus_item,
                                ref uvm_reg_bus_op rw);
    apb_seq_item item;
    if (!$cast(item, bus_item))
      `uvm_fatal("CAST", "bus2reg: expected apb_seq_item")
    rw.data   = item.prdata;
    rw.status = item.pslverr ? UVM_NOT_OK : UVM_IS_OK;
  endfunction
endclass
class axi_adapter extends uvm_reg_adapter;
  `uvm_object_utils(axi_adapter)

  function new(string name = "axi_adapter");
    super.new(name);
    supports_byte_enable = 1;
    byte_enables_size    = 4;
  endfunction

  // ConcreteStrategy: translate to AXI — two-channel (AW+W / AR)
  virtual function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
    axi_seq_item item = axi_seq_item::type_id::create("item");
    item.addr    = rw.addr;
    item.data    = rw.data;
    item.write   = (rw.kind == UVM_WRITE);
    item.wstrb   = (rw.kind == UVM_WRITE) ? rw.byte_en : '0;
    item.len     = 0;  // single beat
    return item;
  endfunction

  virtual function void bus2reg(uvm_sequence_item bus_item,
                                ref uvm_reg_bus_op rw);
    axi_seq_item item;
    if (!$cast(item, bus_item))
      `uvm_fatal("CAST", "bus2reg: expected axi_seq_item")
    rw.data   = item.rdata;
    rw.status = (item.bresp == 2'b00) ? UVM_IS_OK : UVM_NOT_OK;
  endfunction
endclass

uvm_reg_adapter is named 'adapter' but implements Strategy — a reminder that GoF pattern names describe intent, not naming conventions. The Adapter post covered interface translation between incompatible types. Strategy is different: the interface is the same (reg2bus / bus2reg), and the implementations are interchangeable. uvm_reg_adapter is Strategy all the way down.

Pattern Use when Same interface? Varies how?
Strategy what is fixed, how varies Yes — all strategies share the same abstract methods Swapped at runtime via composition
Adapter interfaces are incompatible No — adapter translates between two different interfaces Fixed at integration time
Template Method skeleton fixed, some steps vary Via inheritance only Extended, not swapped

Building Protocol-Agnostic Traffic

Part A: RAL Traffic via uvm_reg_adapter

// mem_ctrl_reg_block — same register block, two adapters
class mem_ctrl_reg_block extends uvm_reg_block;
  `uvm_object_utils(mem_ctrl_reg_block)

  rand mem_timing_reg t_ras;  // Row Active time
  rand mem_timing_reg t_rcd;  // RAS-to-CAS delay
  rand mem_timing_reg t_rp;   // Row Precharge time

  uvm_reg_map apb_map;
  uvm_reg_map axi_map;

  function void build();
    t_ras = mem_timing_reg::type_id::create("t_ras");
    t_ras.configure(this, null, "t_ras");
    t_ras.build();

    // APB map: 32-bit bus, base addr 0x4000_0000
    apb_map = create_map("apb_map", 32'h4000_0000, 4, UVM_LITTLE_ENDIAN);
    apb_map.add_reg(t_ras, 32'h00, "RW");
    apb_map.add_reg(t_rcd, 32'h04, "RW");
    apb_map.add_reg(t_rp,  32'h08, "RW");

    // AXI map: same registers, same offsets, different base addr
    axi_map = create_map("axi_map", 32'h8000_0000, 4, UVM_LITTLE_ENDIAN);
    axi_map.add_reg(t_ras, 32'h00, "RW");
    axi_map.add_reg(t_rcd, 32'h04, "RW");
    axi_map.add_reg(t_rp,  32'h08, "RW");
  endfunction
endclass
class mem_env extends uvm_env;
  mem_ctrl_reg_block regblock;
  apb_agent  apb_agnt;
  axi_agent  axi_agnt;

  apb_adapter apb_adp;
  axi_adapter axi_adp;

  function void build_phase(uvm_phase phase);
    regblock = mem_ctrl_reg_block::type_id::create("regblock", this);
    regblock.build();
    regblock.lock_model();

    apb_agnt = apb_agent::type_id::create("apb_agnt", this);
    axi_agnt = axi_agent::type_id::create("axi_agnt", this);

    apb_adp  = apb_adapter::type_id::create("apb_adp");
    axi_adp  = axi_adapter::type_id::create("axi_adp");
  endfunction

  function void connect_phase(uvm_phase phase);
    // Wire each strategy (adapter) to its map + sequencer
    regblock.apb_map.set_sequencer(apb_agnt.sequencer, apb_adp);
    regblock.axi_map.set_sequencer(axi_agnt.sequencer, axi_adp);
  endfunction
endclass
class mem_timing_config_test extends uvm_test;
  `uvm_component_utils(mem_timing_config_test)

  mem_env env;

  task run_phase(uvm_phase phase);
    uvm_reg_map active_map;

    phase.raise_objection(this);

    // Strategy selected via config — same test body for APB or AXI
    if (!uvm_config_db#(uvm_reg_map)::get(this, "", "active_map", active_map))
      active_map = env.regblock.apb_map;  // default to APB

    // These calls are identical regardless of which map (strategy) is active
    env.regblock.t_ras.write(status, 32'h8, .map(active_map));
    env.regblock.t_rcd.write(status, 32'h4, .map(active_map));
    env.regblock.t_rp.write(status,  32'h4, .map(active_map));

    env.regblock.t_ras.read(status, val, .map(active_map));
    `uvm_info("TEST", $sformatf("t_ras readback: 0x%0h", val), UVM_MEDIUM)

    phase.drop_objection(this);
  endtask
endclass
  • Adapter wired to the wrong map — silent mismatch: APB adapter on the AXI map produces garbled transactions. Always verify map.set_sequencer(seqr, adapter) pairs at connect_phase.
  • supports_byte_enable mismatch — if the protocol supports byte enables but the adapter flag is not set, the register model won't pass byte_en to reg2bus(). Data corruption with no error.
  • Forgetting lock_model() — register model must be locked before use or calls after locking throw a fatal.
  • Mirror vs desired: reg.get() returns desired (what you wrote), reg.get_mirrored_value() returns the last read-back value. Use read() then get_mirrored_value() for hardware verification.

Part B: Non-RAL Traffic via bus_protocol_strategy

// Abstract transaction — describes WHAT, not HOW
class abstract_bus_txn extends uvm_sequence_item;
  `uvm_object_utils(abstract_bus_txn)

  rand bit [31:0] addr;
  rand bit [31:0] data;
  rand bit        write;    // 1 = write, 0 = read
  rand int unsigned burst_len;  // number of beats (1 for single)

  // No protocol-specific fields — APB PSEL, AXI AWVALID, iLink headers, etc.
  // All protocol details live in the strategy, not here.

  function new(string name = "abstract_bus_txn");
    super.new(name);
    burst_len = 1;
  endfunction
endclass
virtual class bus_protocol_strategy extends uvm_object;
  `uvm_object_utils(bus_protocol_strategy)

  // drive: translate abstract txn → protocol-specific bus cycles and send
  pure virtual task drive(abstract_bus_txn item, uvm_sequencer_base seqr);

  // check: validate protocol response and update item.data on read
  pure virtual function void check(abstract_bus_txn item);

  function new(string name = "bus_protocol_strategy");
    super.new(name);
  endfunction
endclass
class apb_protocol_strategy extends bus_protocol_strategy;
  `uvm_object_utils(apb_protocol_strategy)

  virtual task drive(abstract_bus_txn item, uvm_sequencer_base seqr);
    apb_seq_item apb_item = apb_seq_item::type_id::create("apb_item");
    apb_item.paddr  = item.addr;
    apb_item.pwdata = item.data;
    apb_item.pwrite = item.write;
    apb_item.psel   = 1;
    apb_item.penable = 0;

    // Drive through the APB sequencer
    apb_item.start(seqr);

    // Update read data on item
    if (!item.write)
      item.data = apb_item.prdata;
  endtask

  virtual function void check(abstract_bus_txn item);
    // APB-specific: pslverr checked in drive(); nothing extra here
  endfunction
endclass
class axi_protocol_strategy extends bus_protocol_strategy;
  `uvm_object_utils(axi_protocol_strategy)

  virtual task drive(abstract_bus_txn item, uvm_sequencer_base seqr);
    axi_seq_item axi_item = axi_seq_item::type_id::create("axi_item");
    axi_item.addr  = item.addr;
    axi_item.data  = item.data;
    axi_item.write = item.write;
    axi_item.wstrb = item.write ? 4'hF : '0;
    axi_item.len   = item.burst_len - 1;  // AXI: len = beats - 1

    axi_item.start(seqr);

    if (!item.write)
      item.data = axi_item.rdata;
  endtask

  virtual function void check(abstract_bus_txn item);
    // AXI-specific: bresp checked inside axi_seq_item.start()
  endfunction
endclass
class proto_agnostic_driver extends uvm_driver #(abstract_bus_txn);
  `uvm_component_utils(proto_agnostic_driver)

  bus_protocol_strategy strategy;  // injected in build_phase

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    if (!uvm_config_db#(bus_protocol_strategy)::get(
        this, "", "bus_strategy", strategy))
      `uvm_fatal("CFG", "bus_strategy not set in config_db")
  endfunction

  task run_phase(uvm_phase phase);
    abstract_bus_txn item;
    forever begin
      seq_item_port.get_next_item(item);
      strategy.drive(item, null);  // strategy handles sequencer lookup
      seq_item_port.item_done();
    end
  endtask
endclass
// This sequence runs unchanged on APB, AXI, or iLink — strategy is injected
class mem_burst_write_seq extends uvm_sequence #(abstract_bus_txn);
  `uvm_object_utils(mem_burst_write_seq)

  task body();
    abstract_bus_txn item;
    for (int i = 0; i < 4; i++) begin
      item = abstract_bus_txn::type_id::create($sformatf("item_%0d", i));
      start_item(item);
      if (!item.randomize() with {
        addr      == 32'h2000 + (i * 4);
        write     == 1;
        burst_len == 1;
      }) `uvm_fatal("RAND", "Randomization failed")
      finish_item(item);
    end
  endtask
endclass
  • Leaking protocol fields into abstract_bus_txn — once you add psel or awvalid to the abstract txn, the abstraction collapses. All protocol-specific fields belong inside the strategy.
  • Strategy not injected — if uvm_config_db lookup fails and the driver has no strategy, strategy.drive() dereferences null. Always fatal on missing strategy in build_phase.
  • Strategy shared across driver instances — if two drivers share one strategy handle and the strategy has state (e.g., a burst counter), they race. Each driver must get its own strategy instance.

Scaling Up: The Protocol Registry

Passing bus_protocol_strategy handles via uvm_config_db works for one or two protocols. Once you have APB, AXI, AHB, iLink, and custom fabric interfaces, the test must import every strategy package and construct every object. The test shouldn't know concrete strategy types.

// Singleton registry: maps string key → strategy type
class protocol_registry;
  // Singleton accessor
  static function protocol_registry get();
    static protocol_registry inst;
    if (inst == null) inst = new();
    return inst;
  endfunction

  // Internal map: string key → uvm_object_wrapper (factory type handle)
  protected uvm_object_wrapper type_map[string];

  // Register a strategy type under a key — call at package elaboration time
  function void register(string key, uvm_object_wrapper strategy_type);
    if (type_map.exists(key))
      `uvm_warning("REG", $sformatf("protocol_registry: overwriting key '%s'", key))
    type_map[key] = strategy_type;
  endfunction

  // Get a new strategy instance by key
  function bus_protocol_strategy get_strategy(string key);
    uvm_object obj;
    if (!type_map.exists(key))
      `uvm_fatal("REG", $sformatf("protocol_registry: unknown key '%s'", key))
    obj = type_map[key].create_object(key);
    if (!$cast(get_strategy, obj))
      `uvm_fatal("REG", $sformatf("protocol_registry: '%s' is not a bus_protocol_strategy", key))
  endfunction
endclass
// In apb_protocol_pkg.sv — registers at package import time
package apb_protocol_pkg;
  import uvm_pkg::*;
  import bus_protocol_pkg::*;

  class apb_protocol_strategy extends bus_protocol_strategy;
    // ... implementation as in Section 4 ...
  endclass

  // Package-level initial block registers the strategy
  initial begin
    protocol_registry::get().register(
      "apb", apb_protocol_strategy::get_type());
  end
endpackage
function void build_phase(uvm_phase phase);
  string protocol_key;
  bus_protocol_strategy strat;

  // Test sets the key; environment resolves the type — no import of concrete class needed
  if (!uvm_config_db#(string)::get(this, "apb_agnt.*", "bus_protocol", protocol_key))
    protocol_key = "apb";  // default

  strat = protocol_registry::get().get_strategy(protocol_key);
  uvm_config_db#(bus_protocol_strategy)::set(this, "apb_agnt.driver", "bus_strategy", strat);
endfunction

Extending to a new protocol — zero environment changes:

  • Write one new bus_protocol_strategy subclass (e.g., ilink_protocol_strategy)
  • Register it in the iLink package: protocol_registry::get().register("ilink", ilink_protocol_strategy::get_type())
  • Test sets "bus_protocol" = "ilink" — environment, driver, sequences untouched

The registry is a factory, not a flyweight — two agents querying "apb" get independent instances. This prevents shared strategy state causing races between drivers.

package bus_protocol_keys;
  parameter string APB   = "apb";
  parameter string AXI   = "axi";
  parameter string ILINK = "ilink";
endpackage

Define keys as package constants — a string typo ("apb" vs "apbb") silently creates a fatal at runtime. Constants give you compile-time safety.

Advanced: Registry + Config-Driven Selection

class multi_proto_test extends uvm_test;
  `uvm_component_utils(multi_proto_test)

  task run_phase(uvm_phase phase);
    phase.raise_objection(this);

    // Each agent gets its own protocol — set once, resolved by environment
    uvm_config_db#(string)::set(this, "env.apb_agnt.*",  "bus_protocol", bus_protocol_keys::APB);
    uvm_config_db#(string)::set(this, "env.axi_agnt.*",  "bus_protocol", bus_protocol_keys::AXI);
    uvm_config_db#(string)::set(this, "env.link_agnt.*", "bus_protocol", bus_protocol_keys::ILINK);

    // Sequences are identical — strategy handles protocol differences
    mem_burst_write_seq seq = mem_burst_write_seq::type_id::create("seq");
    seq.start(env.apb_agnt.sequencer);
    seq.start(env.axi_agnt.sequencer);
    seq.start(env.link_agnt.sequencer);

    phase.drop_objection(this);
  endtask
endclass
sequenceDiagram
    participant Test
    participant config_db
    participant Env
    participant Registry as protocol_registry
    participant Driver
    participant Strategy
    participant Bus

    Test->>config_db: set("bus_protocol", "axi") per agent
    Env->>config_db: get("bus_protocol") in build_phase
    config_db-->>Env: "axi"
    Env->>Registry: get_strategy("axi")
    Registry-->>Env: new axi_protocol_strategy instance
    Env->>config_db: set("bus_strategy", axi_strategy) to driver
    Driver->>config_db: get("bus_strategy") in build_phase
    config_db-->>Driver: axi_protocol_strategy handle
    Driver->>Strategy: drive(abstract_bus_txn)
    Strategy->>Bus: AXI AWVALID/WVALID/BREADY handshake
    Bus-->>Strategy: BRESP
    Strategy-->>Driver: item.data updated (on read)

All seven patterns working together in one testbench:

  • Factorytype_id::create() instantiates agents, sequences, strategy classes
  • Adapteruvm_reg_adapter translates uvm_reg_bus_op to protocol-specific items (and IS a Strategy)
  • Decorator — analysis subscribers (coverage, scoreboard) attach to monitors without modifying them
  • Facade — a virtual sequence hides multi-agent choreography behind one API
  • Proxyuvm_reg mediates hardware register access; sequencer lock() arbitrates agents
  • Observeruvm_callback / uvm_event_pool decouple notifications from the notified
  • Strategybus_protocol_strategy / uvm_reg_adapter swap protocol algorithms without touching sequences

Seven patterns. Seven orthogonal concerns. Each one solves a different problem — none of them replaces another.

Quick Reference

GoF Role → UVM Mapping

GoF Role RAL mapping Non-RAL mapping
Context uvm_reg_map proto_agnostic_driver
Strategy (abstract) uvm_reg_adapter bus_protocol_strategy
ConcreteStrategy apb_adapter, axi_adapter apb_protocol_strategy, axi_protocol_strategy, ilink_protocol_strategy
Algorithm call reg2bus() / bus2reg() drive() / check()
Strategy injection map.set_sequencer(seqr, adapter) uvm_config_db + protocol_registry

uvm_reg_adapter API Cheatsheet

  • reg2bus(const ref uvm_reg_bus_op rw) — translate abstract op → protocol item; returns uvm_sequence_item
  • bus2reg(uvm_sequence_item bus_item, ref uvm_reg_bus_op rw) — translate protocol response → abstract op
  • supports_byte_enable — set 1 if protocol supports byte-lane enables
  • byte_enables_size — width of byte enable bus in bytes
  • map.set_sequencer(seqr, adapter) — wire a strategy to a map + sequencer in connect_phase

bus_protocol_strategy Interface Summary

  • drive(abstract_bus_txn item, uvm_sequencer_base seqr) — execute protocol-specific transfer
  • check(abstract_bus_txn item) — validate response, update item.data on read
  • protocol_registry::get().register(key, type) — register at package elaboration time
  • protocol_registry::get().get_strategy(key) — get new instance by string key

Common Mistakes

Mistake Fix
Adapter wired to wrong reg map Verify map.set_sequencer(seqr, adapter) pair per agent in connect_phase
supports_byte_enable not set Match adapter flag to DUT bus width; byte_enables_size must match too
Protocol-specific fields in abstract_bus_txn Keep txn fields generic; translate inside strategy only
Registry key typo creates silent null Define keys as parameter string constants in a shared package
Two drivers sharing one strategy instance protocol_registry.get_strategy() returns a new instance per call
Forgetting lock_model() Call after all add_reg() calls; required before any register access

Previous: Observer Pattern — Reacting to events with callbacks and event pools

Next: Chain of Responsibility — Layered handlers from address decoders to protocol stacks

Author
Milan Kubavat
Sharing knowledge about silicon verification, hardware design, and engineering insights.

Comments (0)

Leave a Comment