Adapter Pattern in UVM: Bridging the Register Model to Your Bus
Your register model speaks one language — addresses, data, read/write. Your bus driver speaks another — APB signals, AXI channels, protocol-specific fields. The Adapter pattern bridges this gap, and UVM's uvm_reg_adapter is the textbook implementation. This post shows how it works, where the traps are, and how to build adapters that make your register model work with any bus.
- The Problem: When Two Interfaces Don't Speak the Same Language
- Gang of Four: The Adapter Pattern
- UVM's Adapter: uvm_reg_adapter
- Building an APB Register Adapter
- Scaling Up: AXI Register Adapter
- Advanced: Adapter + Factory
- Quick Reference
The Problem: When Two Interfaces Don't Speak the Same Language
Every time you write reg_model.CTRL_REG.write(status, 32'hDEAD_BEEF), an adapter is quietly doing the translation.
The UVM register layer operates in its own abstraction. When you call write() or read() on a register, the register model constructs a uvm_reg_bus_op — a generic struct that captures the operation in protocol-neutral terms:
// uvm_reg_bus_op — the register model's language
typedef struct {
uvm_access_e kind; // UVM_READ or UVM_WRITE
uvm_reg_addr_t addr; // Register address
uvm_reg_data_t data; // Read/write data
int unsigned n_bits; // Transfer size in bits
uvm_reg_byte_en_t byte_en; // Byte enable mask
uvm_status_e status; // UVM_IS_OK, UVM_NOT_OK, etc.
} uvm_reg_bus_op;
Meanwhile, your bus driver expects something entirely different. An APB agent, for instance, works with an APB transaction object:
// apb_txn — the bus driver's language
class apb_txn extends uvm_sequence_item;
rand bit [31:0] paddr; // APB address
rand bit [31:0] pwdata; // Write data
bit [31:0] prdata; // Read data (driven by DUT)
rand bit pwrite; // 1 = write, 0 = read
rand bit [3:0] pstrb; // Write strobes (byte enables)
bit pslverr; // Slave error response
`uvm_object_utils(apb_txn)
function new(string name = "apb_txn");
super.new(name);
endfunction
endclass
These two interfaces describe the same physical operation — a read or write to a register — but they are completely incompatible types. Different field names (addr vs paddr). Different type representations (kind is an enum, pwrite is a single bit). Different semantics (byte_en is a generic mask, pstrb follows APB protocol rules). You cannot pass one where the other is expected.
The naive solution is to handle the conversion manually inside every register sequence:
The Naive Approach: Manual Conversion in Every Sequence
class my_reg_sequence extends uvm_reg_sequence;
`uvm_object_utils(my_reg_sequence)
function new(string name = "my_reg_sequence");
super.new(name);
endfunction
task body();
uvm_status_e status;
uvm_reg_data_t data;
apb_txn txn;
// Write CTRL_REG — manual conversion
txn = apb_txn::type_id::create("txn");
txn.paddr = reg_model.CTRL_REG.get_address();
txn.pwdata = 32'hDEAD_BEEF;
txn.pwrite = 1;
txn.pstrb = 4'hF;
start_item(txn);
finish_item(txn);
if (txn.pslverr)
`uvm_error("REG", "Write to CTRL_REG failed")
// Read STATUS_REG — same conversion, again
txn = apb_txn::type_id::create("txn");
txn.paddr = reg_model.STATUS_REG.get_address();
txn.pwrite = 0;
txn.pstrb = 4'h0;
start_item(txn);
finish_item(txn);
data = txn.prdata;
if (txn.pslverr)
`uvm_error("REG", "Read from STATUS_REG failed")
endtask
endclass
This works. It also creates three problems that will scale with your testbench:
- Duplicated conversion logic — Every sequence that touches the register model repeats the same
uvm_reg_bus_op-to-apb_txntranslation. Ten register sequences means ten copies of the same conversion code. - Fragile to change — Add a field to
apb_txn(say,pprotfor APB5) and you must update every sequence that constructs APB transactions from register operations. Miss one, and that sequence silently sends transactions with a defaultpprotvalue. - No register layer integration — You've bypassed UVM's register access machinery entirely. No automatic prediction. No mirror updates. No built-in sequences like
uvm_reg_hw_reset_seq. You're maintaining a parallel register access path that doesn't talk to the register model's tracking infrastructure.
The core issue is clear: you have two incompatible interfaces that need to work together, and the conversion logic between them doesn't belong in your test sequences. It belongs in a dedicated translation layer — a single component that knows both languages and converts between them.
There's a pattern for this — and UVM already implements it.
Gang of Four: The Adapter Pattern
"Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn't otherwise because of incompatible interfaces."
— Design Patterns: Elements of Reusable Object-Oriented Software (Gamma et al., 1994)
The key word here is interface, not behavior. The Adapter pattern does not change what a class does — it changes how you talk to it. Think of a travel power adapter: it does not convert voltage or alter the electricity flowing through it. It simply reshapes the plug so it fits a different socket. The same current flows; only the physical interface changes.
In a UVM context, the register model thinks it is talking to a generic bus. The APB driver thinks it is receiving APB transactions. Neither one knows the other exists. The adapter sits between them, converting the register model's abstract read/write operations into concrete APB transactions and back again. No behavior is added, removed, or modified — just translated from one interface to the other.
Class Adapter (Inheritance Variant)
In the class adapter, the Adapter inherits the Target interface and also extends the Adaptee, gaining direct access to its internals. The Adapter overrides the Target's request() method and implements it by calling the Adaptee's specificRequest() internally.
classDiagram
class Target {
+request()
}
class Adaptee {
+specificRequest()
}
class Adapter {
+request()
}
Target <|-- Adapter : extends
Adapter --|> Adaptee : extends
note for Adapter "Inherits Target interface\ncalls Adaptee.specificRequest()"
There is one caveat for SystemVerilog: it does not support multiple inheritance. You cannot extend both the Target and the Adaptee simultaneously. In practice, the adapter extends one class and wraps the other. This is exactly what UVM chose for uvm_reg_adapter — you extend it and implement the translation methods. The adapter is the Target (via inheritance) and uses the Adaptee (via internal calls).
Object Adapter (Composition Variant)
The object adapter takes a different approach. Instead of inheriting from the Adaptee, it holds a reference to an Adaptee instance. The Adapter still extends the Target, but it delegates to the Adaptee object through composition rather than inheritance.
classDiagram
class Target {
+request()
}
class Adaptee {
+specificRequest()
}
class Adapter {
-adaptee : Adaptee
+request()
}
Target <|-- Adapter : extends
Adapter o-- Adaptee : has-a
note for Adapter "Holds Adaptee reference\ndelegates to adaptee.specificRequest()"
This variant is more flexible. Because the adapter holds a reference rather than inheriting a concrete class, it can work with any subclass of the Adaptee — you can swap in a different Adaptee at runtime without modifying the adapter itself. If you are building custom adapters where you want to choose the target bus protocol dynamically, the object adapter gives you that freedom.
Class vs. Object Adapter Comparison
| Aspect | Class Adapter | Object Adapter |
|---|---|---|
| Mechanism | Inheritance | Composition |
| Flexibility | Tied to one specific Adaptee | Works with any Adaptee subclass |
| Access | Can override Adaptee internals | Only public interface of Adaptee |
| UVM usage | uvm_reg_adapter (you extend it) |
Custom adapters with runtime flexibility |
Adapter vs. Other Patterns
One distinction is worth nailing down before we move on. The Adapter converts interface — how you talk to a class — not behavior — what the class does. This is what separates it from two patterns we will cover later in this series. The Strategy pattern swaps out entire algorithms behind a stable interface. The Decorator pattern layers additional behavior on top of an existing object. The Adapter does neither. It is a pure translator: same data in, same data out, different shape at each end.
UVM's Adapter: uvm_reg_adapter
UVM's uvm_reg_adapter is a textbook Class Adapter. You extend it and implement two methods: reg2bus() translates a register operation into a bus transaction (forward path), and bus2reg() translates the bus response back into register model terms (reverse path). The register map calls these automatically every time you invoke reg.write() or reg.read() — your test sequences never see the conversion happening.
This is the "you already use this" moment. If you have ever connected a register model to a bus agent, you wrote an adapter. The class you extended was uvm_reg_adapter. The two methods you implemented — reg2bus() and bus2reg() — are the Adapter pattern's translation interface. The only thing missing was the name.
reg2bus() — Forward Translation
The register map calls reg2bus() every time a register access is initiated. It receives a uvm_reg_bus_op struct describing the operation in abstract terms, and you return a protocol-specific transaction that the bus driver can execute.
// Signature — you must override this
virtual function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
// Input: uvm_reg_bus_op fields
// rw.kind — UVM_READ or UVM_WRITE
// rw.addr — register address
// rw.data — write data (for writes)
// rw.byte_en — byte enable mask
// rw.n_bits — register width in bits
// Output: return your protocol-specific transaction
// e.g., apb_txn, axi_txn, ahb_txn
This is where you create the bus transaction and map register fields to protocol-specific fields. Address becomes PADDR. Write/read kind becomes PWRITE. Byte enables become PSTRB. The adapter knows both languages and performs the translation.
bus2reg() — Reverse Translation
After the driver completes the bus transfer, the register map calls bus2reg() to extract the result. It receives the completed bus transaction and you fill in the register operation's response fields.
// Signature — you must override this
virtual function void bus2reg(uvm_sequence_item bus_item, ref uvm_reg_bus_op rw);
// Input: completed bus transaction (from driver)
// bus_item — your protocol transaction with response data
// Output: fill in rw fields
// rw.data — read data (for reads) or write data (for writes)
// rw.status — UVM_IS_OK or UVM_NOT_OK
This is where you extract the response. prdata becomes rw.data. pslverr becomes rw.status. The register map uses these to update the register model's mirror and report success or failure back to the calling sequence.
Configuration Flags
Two properties in the constructor control how the register map interacts with the adapter:
provides_responses— Set to 1 if your bus protocol uses a separate response channel. AXI, for example, returns read data on the R channel and write responses on the B channel — separate items from the original request. APB returns the response on the same transaction item. If you set this wrong, the register map either hangs waiting for a response that never comes (set to 1 when it should be 0) or reads stale data from the request item (set to 0 when it should be 1).supports_byte_enable— Set to 1 if your bus can do byte-level access via strobes (PSTRB,WSTRB). When enabled, partial register writes use byte enables directly. When disabled, the register map performs a read-modify-write sequence instead.
The Full Register Access Pipeline
Here is the complete flow when you call reg.write(status, value). The adapter sits at the center, translating in both directions:
sequenceDiagram
participant Test as Test Sequence
participant Reg as uvm_reg
participant Map as uvm_reg_map
participant Adapter as uvm_reg_adapter
participant Sqr as Sequencer
participant Drv as Driver
participant DUT
Test->>Reg: reg.write(status, data)
Reg->>Map: do_write(uvm_reg_bus_op)
Map->>Adapter: reg2bus(rw)
Adapter-->>Map: apb_txn
Map->>Sqr: start_item / finish_item
Sqr->>Drv: get_next_item
Drv->>DUT: APB transfer
DUT-->>Drv: response (PRDATA, PSLVERR)
Drv->>Sqr: item_done
Map->>Adapter: bus2reg(apb_txn, rw)
Adapter-->>Map: rw.data, rw.status
Map->>Reg: update mirror
Reg-->>Test: return status
The adapter appears twice in this pipeline: once on the way out (reg2bus, converting the abstract register operation into a concrete APB transaction) and once on the way back (bus2reg, converting the APB response into the register model's status and data). Everything before the adapter speaks register model. Everything after it speaks APB. The adapter is the boundary.
Why This Is an Adapter
Map it to the GoF roles. The register map is the Client — it expects a generic bus interface (abstract register operations). The APB driver is the Adaptee — it only understands apb_txn. Your apb_reg_adapter subclass is the Adapter — it extends uvm_reg_adapter (the Target) and translates between the Client's language and the Adaptee's language. The register model never knows it is talking to an APB bus. The APB driver never knows it is serving a register model. The adapter makes them work together without either side changing.
Building an APB Register Adapter
Theory is useful, but you learn adapters by building one. Let's walk through a complete APB register adapter — the transaction it produces, the two translation methods, how it connects to the environment, and the mistakes that will cost you hours if you don't catch them early.
The APB Transaction Class
Before writing the adapter, you need to know what it's translating to. Here's the APB transaction class your driver expects:
class apb_txn extends uvm_sequence_item;
rand bit [31:0] paddr;
rand bit [31:0] pwdata;
bit [31:0] prdata;
rand bit pwrite;
rand bit [3:0] pstrb;
bit pslverr;
`uvm_object_utils(apb_txn)
function new(string name = "apb_txn");
super.new(name);
endfunction
endclass
Notice the split between rand and non-rand fields. paddr, pwdata, pwrite, and pstrb are stimulus — the adapter sets these on the way out. prdata and pslverr are responses — the driver fills these in after the DUT responds, and the adapter reads them on the way back. This is the object the adapter must produce in reg2bus() and consume in bus2reg().
The Complete Adapter
Here is the full apb_reg_adapter. It's compact — about 30 lines — because APB is a simple protocol with a 1:1 mapping between register operations and bus transactions.
class apb_reg_adapter extends uvm_reg_adapter;
`uvm_object_utils(apb_reg_adapter)
function new(string name = "apb_reg_adapter");
super.new(name);
supports_byte_enable = 1;
provides_responses = 0; // APB response on same item
endfunction
virtual function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
apb_txn txn = apb_txn::type_id::create("txn");
txn.paddr = rw.addr;
txn.pwrite = (rw.kind == UVM_WRITE);
if (rw.kind == UVM_WRITE) begin
txn.pwdata = rw.data;
txn.pstrb = rw.byte_en[3:0];
end
return txn;
endfunction
virtual function void bus2reg(uvm_sequence_item bus_item, ref uvm_reg_bus_op rw);
apb_txn txn;
if (!$cast(txn, bus_item))
`uvm_fatal("CAST", "bus2reg cast failed")
rw.data = (txn.pwrite) ? txn.pwdata : txn.prdata;
rw.status = txn.pslverr ? UVM_NOT_OK : UVM_IS_OK;
endfunction
endclass
Let's break both methods down.
reg2bus() — Forward Translation
The register map calls reg2bus() every time you do a reg.write() or reg.read(). It receives a uvm_reg_bus_op struct and must return a protocol-specific transaction.
apb_txn::type_id::create("txn")— The transaction is created through the Factory, not withnew(). This is critical: if someone overridesapb_txnwith a coverage-instrumented subclass via the Factory, thiscreate()call will pick up the override. Usingnew apb_txn()would bypass the Factory entirely and silently break that override chain.txn.paddr = rw.addr— Direct address mapping. The register model provides the address from the register map; the adapter passes it straight through to the APB address field.txn.pwrite = (rw.kind == UVM_WRITE)— The register model expresses direction as an enum (UVM_READorUVM_WRITE). APB uses a single bit. This line converts between the two representations.txn.pwdata = rw.data— Write data is only mapped for write operations. For reads,pwdatadoesn't matter — the DUT ignores it.txn.pstrb = rw.byte_en[3:0]— Byte enables from the register model map to APB'sPSTRB. The slice[3:0]is necessary becauserw.byte_enis wider than 4 bits, while APB'sPSTRBis 4 bits for a 32-bit data bus.
The constructor sets two configuration flags that affect the register map's behavior. supports_byte_enable = 1 tells the register map that this bus can do byte-level access — so partial register writes will use PSTRB rather than performing a read-modify-write. provides_responses = 0 tells the register map that APB responses arrive on the same transaction item — the driver fills in prdata and pslverr on the same apb_txn object that was sent out. Protocols like AXI, where read data comes back on a separate channel, set this to 1.
bus2reg() — Reverse Translation
After the driver completes the APB transfer, the register map calls bus2reg() to extract the result.
$cast(txn, bus_item)— The method signature takes a genericuvm_sequence_item. You must$castit toapb_txnto access the APB-specific fields. If the cast fails, something is seriously wrong — a different transaction type ended up on this sequencer — so`uvm_fatalis appropriate.rw.data = (txn.pwrite) ? txn.pwdata : txn.prdata— This line is subtle. For reads, the data the register model cares about isprdata— the value the DUT returned. For writes, it'spwdata— the value that was written. The register model usesrw.datato update its mirror, so returning the correct value for each direction matters.rw.status = txn.pslverr ? UVM_NOT_OK : UVM_IS_OK— Maps the APB slave error signal to the register model's status enum. This line is easy to forget — and forgetting it is one of the most common adapter bugs.
Connecting the Adapter in the Environment
The adapter doesn't do anything until you wire it into the register access path. This happens in your environment's connect_phase:
class apb_env extends uvm_env;
apb_agent agent;
my_reg_model reg_model;
apb_reg_adapter adapter;
function void build_phase(uvm_phase phase);
super.build_phase(phase);
agent = apb_agent::type_id::create("agent", this);
reg_model = my_reg_model::type_id::create("reg_model");
reg_model.build();
adapter = apb_reg_adapter::type_id::create("adapter");
endfunction
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
reg_model.default_map.set_sequencer(agent.sequencer, adapter);
endfunction
endclass
The key line is reg_model.default_map.set_sequencer(agent.sequencer, adapter). This single call does two things: it tells the register map which sequencer to send translated transactions to, and it tells the map which adapter to use for the translation. After this connection, every reg.write() and reg.read() call flows through the adapter automatically — your test sequences never touch apb_txn directly.
Note that the adapter is created via type_id::create(), not new(). This is the same Factory discipline we covered in the first post in this series. It means you can override the adapter with an error-injecting or coverage-collecting variant at test time — something we'll explore in the Advanced section.
Common Pitfalls
- Forgetting to set
rw.statusinbus2reg()— The default value ofrw.statusisUVM_NOT_OK. If you don't explicitly set it, every register access will look like it failed. Your register model's mirror will stop updating, and you'll spend hours debugging a scoreboard mismatch that's actually an adapter bug. - Wrong
provides_responsesvalue — For APB, this must be 0. If you set it to 1, the register map will wait for a separate response item that never arrives — your simulation hangs. Conversely, setting it to 0 on a protocol like AXI (where it should be 1) causes the map to read response data from the request item before the actual response has arrived. - Not using
type_id::create()inreg2bus()— If you writeapb_txn txn = new("txn")instead ofapb_txn::type_id::create("txn"), the Factory is bypassed. Any override onapb_txn— coverage wrappers, protocol checkers, debug instrumentation — will be silently ignored. - Forgetting
$castinbus2reg()— Without the cast, you can't access protocol-specific fields likeprdataandpslverr. The compiler won't always catch this — you'll get a runtime fatal when the method is first called.
Scaling Up: AXI Register Adapter
Why AXI Is Different
APB is the friendliest bus you will ever adapt. One register operation maps to one bus transaction. The response comes back on the same item your driver sent out. There is no pipelining, no burst, no out-of-order completion. You write a twenty-line adapter and move on with your life.
AXI is a different animal. It separates read and write into independent channels — an address channel, a data channel, and a response channel for each direction. It supports burst transfers, out-of-order responses, and multiple outstanding transactions. A single register write touches the AW channel (address), the W channel (data + strobes), and the B channel (write response) — three handshakes instead of one. A register read touches AR (address) and R (data + response). The adapter must map a single uvm_reg_bus_op into this multi-channel world. The good news: the adapter interface stays exactly the same. You still implement reg2bus() and bus2reg(). Only the implementation changes.
Key Differences from APB
provides_responses = 1— This is the most critical difference. In APB, the driver fills inprdataandpslverron the same transaction item it received. The register map reuses that item forbus2reg(). In AXI, read data arrives on the R channel and write responses arrive on the B channel — separate items from the original request. Settingprovides_responses = 1tells the register map to wait for a distinct response item rather than reusing the request.- Burst length fixed to 0 — AXI encodes burst length as
AxLEN, where the actual beat count isAxLEN + 1. For single-beat register access, you setburst_len = 0(one beat). AWSIZE/ARSIZEderived from register width — For a standard 32-bit register bus, this is3'b010(4 bytes per beat). If your design has 64-bit registers, you would use3'b011.WSTRBfrombyte_en— Same concept as APB's PSTRB, mapped directly from the register operation's byte enable field.- Response mapping — AXI responses use a 2-bit enum:
OKAYmaps toUVM_IS_OK;SLVERRandDECERRboth map toUVM_NOT_OK.
AXI Adapter Implementation
Here is the complete adapter. Compare it with the APB version from the previous section — the structure is identical, but the details reflect AXI's richer transaction model.
class axi_reg_adapter extends uvm_reg_adapter;
`uvm_object_utils(axi_reg_adapter)
function new(string name = "axi_reg_adapter");
super.new(name);
supports_byte_enable = 1;
provides_responses = 1; // Separate response channels
endfunction
virtual function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
axi_txn txn = axi_txn::type_id::create("txn");
txn.addr = rw.addr;
txn.op = (rw.kind == UVM_WRITE) ? AXI_WRITE : AXI_READ;
txn.burst_type = INCR;
txn.burst_len = 0; // Single beat for register access
txn.burst_size = 3'b010; // 4 bytes
if (rw.kind == UVM_WRITE) begin
txn.data = new[1];
txn.data[0] = rw.data;
txn.wstrb = rw.byte_en[3:0];
end
return txn;
endfunction
virtual function void bus2reg(uvm_sequence_item bus_item, ref uvm_reg_bus_op rw);
axi_txn txn;
if (!$cast(txn, bus_item))
`uvm_fatal("CAST", "bus2reg cast failed")
rw.data = txn.data[0];
rw.status = (txn.resp == OKAY) ? UVM_IS_OK : UVM_NOT_OK;
endfunction
endclass
Notice what changed and what did not. The constructor now sets provides_responses = 1. The reg2bus() method populates burst fields (burst_type, burst_len, burst_size) that APB does not have, and it packs write data into a dynamic array since AXI models burst data as an array of beats. The bus2reg() method reads from txn.data[0] (first beat of the response) and maps the AXI response enum instead of a single error bit. Everything else — the method signatures, the $cast, the type_id::create() call — is identical.
Side-by-Side Comparison
| Aspect | APB Adapter | AXI Adapter |
|---|---|---|
provides_responses |
0 (same item) | 1 (separate channel) |
supports_byte_enable |
1 (PSTRB) | 1 (WSTRB) |
| Burst handling | N/A | burst_len = 0 (single beat) |
| Response mapping | pslverr → status | resp enum → status |
| Complexity | ~20 lines | ~30 lines |
The Takeaway
The Adapter pattern scales cleanly. The interface contract — reg2bus() and bus2reg() — stays identical regardless of how complex the underlying bus protocol is. Whether you are targeting APB, AXI, AHB, or a proprietary NoC interface, the register model still calls reg.write(status, value) the same way. Only the adapter implementation changes. Ten extra lines absorbed all of AXI's channel separation, burst mechanics, and response encoding. That is the power of the pattern: bus complexity is encapsulated inside the adapter, completely invisible to the rest of the testbench.
Advanced: Adapter + Factory
The Adapter pattern gives you a translation contract — reg2bus() and bus2reg() convert between the register model's world and your bus protocol's world. But who decides which adapter gets created? That is where the Factory pattern comes in. Because the environment creates the adapter via type_id::create(), any test can override which adapter class gets instantiated — without touching the environment or the register model.
This is the composition that makes UVM powerful. The Adapter defines what translation looks like. The Factory controls which translator gets used. Together, they let you swap register access behavior at test time with a single override call. You get error injection, coverage collection, or transaction logging — all by extending the base adapter and letting the Factory do the wiring.
Error-Injecting Adapter
Suppose you want to verify that your scoreboard and register model handle bus errors gracefully. Rather than building error injection into the driver or adding conditional logic to the environment, you extend the base adapter and override bus2reg() to randomly corrupt the status:
class apb_error_reg_adapter extends apb_reg_adapter;
`uvm_object_utils(apb_error_reg_adapter)
function new(string name = "apb_error_reg_adapter");
super.new(name);
endfunction
virtual function void bus2reg(uvm_sequence_item bus_item, ref uvm_reg_bus_op rw);
super.bus2reg(bus_item, rw);
// Inject error on ~10% of accesses
if ($urandom_range(0, 9) == 0)
rw.status = UVM_NOT_OK;
endfunction
endclass
The forward path (reg2bus()) is untouched — bus transactions are perfectly formed. Only the reverse path injects faults, which is exactly what bus errors look like from the register model's perspective.
Coverage Adapter
You can also extend the adapter to collect functional coverage on register accesses. This coverage adapter adds a covergroup that samples every reg2bus() call, tracking read vs. write distribution and byte-enable patterns:
class apb_coverage_reg_adapter extends apb_reg_adapter;
`uvm_object_utils(apb_coverage_reg_adapter)
covergroup reg_access_cg with function sample(uvm_reg_bus_op rw);
cp_kind: coverpoint rw.kind { bins read = {UVM_READ}; bins write = {UVM_WRITE}; }
cp_byte_en: coverpoint rw.byte_en[3:0] { bins full = {4'hF}; bins partial[] = {[4'h1:4'hE]}; }
endgroup
function new(string name = "apb_coverage_reg_adapter");
super.new(name);
reg_access_cg = new();
endfunction
virtual function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
reg_access_cg.sample(rw);
return super.reg2bus(rw);
endfunction
endclass
Every register access automatically gets sampled. No changes to your sequences, no changes to your register model, no extra monitor plumbing.
Swapping Adapters via Factory Override
Now the payoff. Your tests choose which adapter to use with a single Factory override — before super.build_phase() triggers the environment's type_id::create() call:
class error_injection_test extends base_test;
function void build_phase(uvm_phase phase);
apb_reg_adapter::type_id::set_type_override(
apb_error_reg_adapter::get_type());
super.build_phase(phase);
endfunction
endclass
class coverage_test extends base_test;
function void build_phase(uvm_phase phase);
apb_reg_adapter::type_id::set_type_override(
apb_coverage_reg_adapter::get_type());
super.build_phase(phase);
endfunction
endclass
Look at what stays the same across these tests:
- The environment — still calls
apb_reg_adapter::type_id::create("adapter")inbuild_phase. It does not know or care which subclass it gets back. - The register model — still calls
reg.write()andreg.read(). The adapter is invisible to it. - The sequences — completely unchanged. They interact with the register model, not with the adapter.
Only the adapter behavior changes — and the test controls that with one line.
The Override in Action
Here is the sequence of events when you run the error injection test:
sequenceDiagram
participant Test
participant Factory
participant Env
participant adapter as apb_error_reg_adapter
participant reg_model
Test->>Factory: set_type_override(apb_reg_adapter → apb_error_reg_adapter)
Env->>Factory: create("adapter")
Factory-->>Env: returns apb_error_reg_adapter instance
reg_model->>adapter: reg2bus(rw) — normal translation
adapter-->>reg_model: bus2reg(rw) — with injected errors
The test registers the override before the environment builds. When the environment calls create(), the Factory returns the error-injecting subclass instead of the base adapter. From that point on, every register access flows through the overridden bus2reg() — and roughly 10% of them come back with UVM_NOT_OK.
This is the same Factory override mechanism from the first post in this series. The Adapter provides the translation contract — the two methods that convert between register operations and bus transactions. The Factory lets you swap implementations of that contract at test time. Together, they give you pluggable register access behavior without touching a single line of environment code. One pattern defines the interface, the other controls instantiation, and your testbench stays clean.
Quick Reference
Method Reference
| Method / Property | Defined In | What It Does | Override? |
|---|---|---|---|
reg2bus(rw) | uvm_reg_adapter | Converts a register operation into a protocol-specific bus transaction | Yes — must implement |
bus2reg(bus_item, rw) | uvm_reg_adapter | Converts a completed bus response back into register-level results | Yes — must implement |
provides_responses | uvm_reg_adapter | Tells the register map whether the bus uses a separate response channel | Set in constructor |
supports_byte_enable | uvm_reg_adapter | Tells the register map whether the bus supports byte-level access | Set in constructor |
set_sequencer(sqr, adapter) | uvm_reg_map | Connects a sequencer and adapter to the register map for bus access | No — call in connect_phase |
Common Mistakes
| Mistake | Fix |
|---|---|
Forgetting to set rw.status in bus2reg() | Always set status explicitly. The default is UVM_NOT_OK, which makes every register access look like it failed. |
Wrong provides_responses value | APB = 0 (response on the same item). AXI = 1 (separate response channel). Wrong value causes simulation hangs or missed responses. |
Using new() instead of type_id::create() in reg2bus() | Always use type_id::create() when constructing the bus transaction so that Factory overrides on the transaction type are respected. |
Forgetting $cast in bus2reg() | The bus_item argument is uvm_sequence_item. Always $cast it to your specific transaction type before accessing protocol fields. |
Factory override placed after super.build_phase() | The override must come before super.build_phase() so the environment picks it up when it calls type_id::create(). |
| Not creating the adapter via Factory | Use apb_reg_adapter::type_id::create() in the environment, not new(). Otherwise Factory overrides for the adapter itself will not work. |
What's Next: Structural Patterns
The Adapter is the first of four Structural patterns in this series. Here is what's coming:
| Pattern | What It Does | UVM Example |
|---|---|---|
| Adapter (this post) | Converts one interface to another | uvm_reg_adapter — reg2bus() / bus2reg() |
| Decorator | Adds behavior without changing the interface | Coverage wrappers, logging monitors |
| Facade | Simplifies a complex subsystem behind a single interface | Agent encapsulating driver + monitor + sequencer |
| Proxy | Controls access to an object | uvm_reg_field access policies |
Previous: Prototype Pattern — Cloning transactions, deep vs shallow copy, and do_copy()
Next: Decorator Pattern — Adding behavior without changing interfaces
Comments (0)
Leave a Comment