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

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_txn translation. Ten register sequences means ten copies of the same conversion code.
  • Fragile to change — Add a field to apb_txn (say, pprot for APB5) and you must update every sequence that constructs APB transactions from register operations. Miss one, and that sequence silently sends transactions with a default pprot value.
  • 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 interfacehow you talk to a class — not behaviorwhat 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 with new(). This is critical: if someone overrides apb_txn with a coverage-instrumented subclass via the Factory, this create() call will pick up the override. Using new 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_READ or UVM_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, pwdata doesn't matter — the DUT ignores it.
  • txn.pstrb = rw.byte_en[3:0] — Byte enables from the register model map to APB's PSTRB. The slice [3:0] is necessary because rw.byte_en is wider than 4 bits, while APB's PSTRB is 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 generic uvm_sequence_item. You must $cast it to apb_txn to access the APB-specific fields. If the cast fails, something is seriously wrong — a different transaction type ended up on this sequencer — so `uvm_fatal is appropriate.
  • rw.data = (txn.pwrite) ? txn.pwdata : txn.prdata — This line is subtle. For reads, the data the register model cares about is prdata — the value the DUT returned. For writes, it's pwdata — the value that was written. The register model uses rw.data to 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.status in bus2reg() — The default value of rw.status is UVM_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_responses value — 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() in reg2bus() — If you write apb_txn txn = new("txn") instead of apb_txn::type_id::create("txn"), the Factory is bypassed. Any override on apb_txn — coverage wrappers, protocol checkers, debug instrumentation — will be silently ignored.
  • Forgetting $cast in bus2reg() — Without the cast, you can't access protocol-specific fields like prdata and pslverr. 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 in prdata and pslverr on the same transaction item it received. The register map reuses that item for bus2reg(). In AXI, read data arrives on the R channel and write responses arrive on the B channel — separate items from the original request. Setting provides_responses = 1 tells 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 is AxLEN + 1. For single-beat register access, you set burst_len = 0 (one beat).
  • AWSIZE / ARSIZE derived from register width — For a standard 32-bit register bus, this is 3'b010 (4 bytes per beat). If your design has 64-bit registers, you would use 3'b011.
  • WSTRB from byte_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: OKAY maps to UVM_IS_OK; SLVERR and DECERR both map to UVM_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") in build_phase. It does not know or care which subclass it gets back.
  • The register model — still calls reg.write() and reg.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 / PropertyDefined InWhat It DoesOverride?
reg2bus(rw)uvm_reg_adapterConverts a register operation into a protocol-specific bus transactionYes — must implement
bus2reg(bus_item, rw)uvm_reg_adapterConverts a completed bus response back into register-level resultsYes — must implement
provides_responsesuvm_reg_adapterTells the register map whether the bus uses a separate response channelSet in constructor
supports_byte_enableuvm_reg_adapterTells the register map whether the bus supports byte-level accessSet in constructor
set_sequencer(sqr, adapter)uvm_reg_mapConnects a sequencer and adapter to the register map for bus accessNo — call in connect_phase

Common Mistakes

MistakeFix
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 valueAPB = 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 FactoryUse 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:

PatternWhat It DoesUVM Example
Adapter (this post)Converts one interface to anotheruvm_reg_adapterreg2bus() / bus2reg()
DecoratorAdds behavior without changing the interfaceCoverage wrappers, logging monitors
FacadeSimplifies a complex subsystem behind a single interfaceAgent encapsulating driver + monitor + sequencer
ProxyControls access to an objectuvm_reg_field access policies

Previous: Prototype Pattern — Cloning transactions, deep vs shallow copy, and do_copy()

Next: Decorator Pattern — Adding behavior without changing interfaces

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

Comments (0)

Leave a Comment