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
- Gang of Four: The Strategy Pattern
- UVM's Strategy: uvm_reg_adapter Revealed
- Building Protocol-Agnostic Traffic
- Scaling Up: The Protocol Registry
- Advanced: Registry + Config-Driven Selection
- Quick Reference
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_enablemismatch — if the protocol supports byte enables but the adapter flag is not set, the register model won't passbyte_entoreg2bus(). 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. Useread()thenget_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 addpselorawvalidto the abstract txn, the abstraction collapses. All protocol-specific fields belong inside the strategy. - Strategy not injected — if
uvm_config_dblookup fails and the driver has no strategy,strategy.drive()dereferences null. Always fatal on missing strategy inbuild_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_strategysubclass (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:
- Factory —
type_id::create()instantiates agents, sequences, strategy classes - Adapter —
uvm_reg_adaptertranslatesuvm_reg_bus_opto 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
- Proxy —
uvm_regmediates hardware register access; sequencerlock()arbitrates agents - Observer —
uvm_callback/uvm_event_pooldecouple notifications from the notified - Strategy —
bus_protocol_strategy/uvm_reg_adapterswap 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; returnsuvm_sequence_itembus2reg(uvm_sequence_item bus_item, ref uvm_reg_bus_op rw)— translate protocol response → abstract opsupports_byte_enable— set1if protocol supports byte-lane enablesbyte_enables_size— width of byte enable bus in bytesmap.set_sequencer(seqr, adapter)— wire a strategy to a map + sequencer inconnect_phase
bus_protocol_strategy Interface Summary
drive(abstract_bus_txn item, uvm_sequencer_base seqr)— execute protocol-specific transfercheck(abstract_bus_txn item)— validate response, updateitem.dataon readprotocol_registry::get().register(key, type)— register at package elaboration timeprotocol_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
Comments (0)
Leave a Comment