UVM RAL · Chapter 5 · Adapters
Custom Protocol Adapter
RAL is not limited to APB and AXI, because the adapter is a universal translator, so any memory-mapped register bus, including a proprietary in-house one, becomes reachable as soon as you write one custom adapter. The recipe never changes, because RAL always hands the adapter the same generic bus operation and expects the same six mappings back. You map the address to the bus's addressing, the kind to its direction or command, the data to its payload, the byte enables to its lane mask, and the response to status covering every error code, and you set two capability flags from the real bus and driver. Get those right and every register test runs unchanged. When a bus access is a multi-step protocol rather than a single transaction, you reach for a user-defined frontdoor instead. The lesson ends with an adapter that omits the command field the driver needs, so writes silently drive as reads.
Foundation12 min readUVM RALCustom AdapterProtocolreg2busFrontdoorPortability
Chapter 5 · Section 5.6 · Adapters
1. Why Should I Learn This?
Real designs are full of buses that are not APB or AXI — proprietary register interfaces, simplified in-house protocols, vendor-specific control buses — and the instinct when RAL 'does not support' one is to abandon it and hand-code tests in the bus's terms, throwing away all the portability of 5.1. The truth is that RAL supports every register bus, because the adapter is where the bus lives and you can write an adapter for anything. Knowing the general recipe is what lets you bring RAL to a bus it has never heard of in an afternoon, instead of writing a bespoke, non-portable test suite.
Learning the universal adapter recipe — and the one boundary where you need a user-defined frontdoor instead — is what generalizes the APB and AXI examples into a skill you can apply to any register interface you meet. It is the payoff of the whole chapter: one small translator, and RAL runs anywhere.
2. Industry Story — the proprietary bus that 'RAL did not support'
A team's block sits behind a proprietary register bus — a simple command-based interface with a cmd field (read/write), an address, data, and a response code. Believing RAL only works with standard buses, they hand-write register tests directly in the proprietary bus's transactions, addresses inlined, no register model. It works, and it rots exactly as 5.1 warned: a spec change breaks dozens of hand-coded accesses, and the tests cannot move to the next block on a different bus.
A senior engineer points out that the bus, being memory-mapped register access, is trivially adaptable: its access reduces to the same generic operation as APB's, so a custom adapter — mapping the cmd field from kind, the address, the data by kind, and the response code to status — makes the entire RAL model, maps, and tests run over the proprietary bus unchanged. They write it in an afternoon; the hand-coded suite is deleted; the register tests become portable and self-checking. The lesson the team internalizes: RAL is not limited to standard buses — the adapter is a universal translator, so any memory-mapped register bus is supported by one custom adapter following the same recipe, and hand-coding tests in the bus's terms needlessly throws away the model and its portability.
3. Concept — the universal recipe, and the frontdoor boundary
For any register bus, the adapter owes the same six mappings — this is the recipe:
- address → the bus's addressing. Convert RAL's byte address to the bus's granularity and format (word shift, muxed address/data, segment select — whatever the bus does).
- kind → the bus's direction/command. Set the field the bus uses to distinguish read from write — a
pwritebit, acmdcode, an opcode. This is often the field a custom bus has that the standard ones do not. - data → payload by kind. Write data outbound, read data inbound (in
bus2reg), as always. - byte enables → the bus's lane mask.
pstrb,wstrb, a custom mask — or nothing, withsupports_byte_enable = 0, if the bus cannot do sub-word writes. - response → status (every code). Translate all of the bus's error responses to
UVM_NOT_OK, not just one (theDECERRlesson, 5.5). - flags.
supports_byte_enableandprovides_responsesfrom the real bus and driver (3.3, 5.4).
And the boundary: this recipe covers any bus whose register access is a single translatable transaction. If a register access on your bus is a multi-step protocol — an indirect access (write an index, then data), a framed or serialized transfer, a command sequence — then a single-item adapter cannot express it, and you use a user-defined frontdoor (4.6) to run the sequence, with the adapter handling the individual transfers within it. Here is the recipe as a checklist:
4. Mental Model — the adapter is a universal translator with a fixed contract
5. Working Example — an adapter for a custom command-based bus
A proprietary creg bus with a cmd opcode, an address, data, and a response code — the recipe applied, including the custom cmd field:
class reg2creg_adapter extends uvm_reg_adapter;
`uvm_object_utils(reg2creg_adapter)
function new(string name = "reg2creg_adapter");
super.new(name);
supports_byte_enable = 0; // this bus has no lane mask -> field writes are RMW (3.3)
provides_responses = 1; // driver returns a response item with resp/rdata
endfunction
virtual function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
creg_item item = creg_item::type_id::create("item");
item.cmd = (rw.kind == UVM_WRITE) ? CREG_WR : CREG_RD; // kind -> the bus's COMMAND field
item.addr = rw.addr; // address (byte-addressed here)
item.wdata = rw.data; // write payload
return item;
endfunction
virtual function void bus2reg(uvm_sequence_item bus_item, ref uvm_reg_bus_op rw);
creg_item item;
if (!$cast(item, bus_item)) begin `uvm_fatal("ADPT","bad item") return; end
rw.kind = (item.cmd == CREG_WR) ? UVM_WRITE : UVM_READ;
rw.addr = item.addr;
rw.data = (item.cmd == CREG_WR) ? item.wdata : item.rdata; // payload by kind
// Map EVERY error code this bus defines, not just one:
rw.status = (item.resp == CREG_OK) ? UVM_IS_OK : UVM_NOT_OK; // any non-OK resp -> NOT_OK
endfunction
endclassBind it, and the entire generic RAL environment runs over the proprietary bus, unchanged:
reg_model.default_map.set_sequencer(creg_agent.sequencer, creg_adapter);
uvm_status_e s; logic [31:0] got;
cfg.write(s, 32'h13); // the SAME cfg_test_seq that ran on APB now runs on the creg bus
cfg.read (s, got);The only new thing versus APB is the cmd opcode field — the bus's way of saying read-or-write — which reg2bus sets from kind. Omit exactly that field and the driver cannot tell a write from a read, which is the next section.
6. Debugging Session — the command field reg2bus forgot
A custom adapter that omits the protocol-specific command field leaves the driver unable to tell a write from a read, so writes silently drive as reads
MISSING COMMAND FIELD// The creg bus uses a 'cmd' opcode to distinguish read from write. reg2bus forgets to set it:
virtual function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
creg_item item = creg_item::type_id::create("item");
// BUG: item.cmd never set -> defaults to CREG_RD (read). rw.kind is ignored.
item.addr = rw.addr;
item.wdata = rw.data;
return item;
endfunction
// A register write drives the bus with cmd = READ, so the DUT performs a READ, not a write.
cfg.write(s, 32'h13); // driver sees cmd=RD -> DUT is never written; the write does nothingRegister writes do not take effect — the block behaves as if it was never configured — while reads work fine. The write calls complete without error (a read to a valid address succeeds), so nothing flags a failure, and the mirror is even updated to the written value (the frontdoor auto-predicts, 6.2), so an immediate read-back against the mirror can pass while the DUT is actually unchanged. It looks like the DUT ignores writes, and time is spent on the RTL write path, which is correct — the writes were never issued as writes, because the bus command that distinguishes them was never set.
The custom bus uses a cmd opcode field to tell the driver whether a transaction is a read or a write, and reg2bus never sets it, so it defaults to read. RAL's kind — which correctly says UVM_WRITE — is simply not mapped onto the field the bus uses to express direction, so every write drives the bus as a read: the DUT performs a read, the intended write data is ignored, and the register is never updated. This is the recipe's second mapping (kind → the bus's direction/command) omitted, and it is the field a custom bus often has that the standard ones fold into an obvious signal (pwrite). reg2bus must fully specify the bus item, including every protocol-specific field the driver depends on; leaving one unset lets the driver mis-drive the access.
Set the command field from kind: item.cmd = (rw.kind == UVM_WRITE) ? CREG_WR : CREG_RD; (as in Section 5), so a write drives the bus as a write. More generally, reg2bus must set every field the driver needs to drive the transaction correctly, including protocol-specific ones the standard buses do not have — and a good check is to confirm a write actually changes the DUT (a frontdoor write then a backdoor peek, which reads the real hardware, not the auto-predicted mirror). The rule the bug teaches: the adapter recipe includes mapping kind to the bus's direction/command field, and a custom bus's command opcode is exactly that field — omit it and the driver cannot tell a write from a read, so writes silently drive as reads and never take effect. When writes seem ignored but reads work, check that reg2bus sets the bus's direction field.
7. Common Mistakes
- Believing RAL only supports standard buses. The adapter is universal; any memory-mapped register bus is one custom adapter away.
- Omitting a protocol-specific field in
reg2bus. A missing command/mode field lets the driver mis-drive the access (writes as reads). - Mapping only one of several response codes. Map every error the custom bus can return to
UVM_NOT_OK(5.5). - Using an adapter where a frontdoor is needed. A multi-step (indirect, framed) access needs a user-defined frontdoor (4.6), not a single-item adapter.
- Wrong
supports_byte_enable/provides_responses. Set them from the custom bus and driver, as for any adapter.
8. Industry Best Practices
- Apply the six-mapping recipe to any bus. Address, kind, data, byte enables, response (all codes), flags — bus-independent.
- Fully specify the bus item in
reg2bus. Every field the driver needs, including protocol-specific ones. - Confirm writes reach the DUT with a backdoor peek. It reads the real hardware, catching writes that auto-predict the mirror but never drove.
- Use a user-defined frontdoor for protocol-style access. Indirect, framed, or sequenced register access; the adapter handles the transfers within it.
- Write one custom adapter, keep the tests generic. Preserve RAL's portability even on a proprietary bus.
9. Interview / Review Questions
10. Key Takeaways
- The adapter is a universal translator: RAL runs on any memory-mapped register bus — including a proprietary one — by writing one custom adapter; hand-coding tests in bus terms needlessly discards the model and its portability.
- The recipe is the same six mappings for every bus: address → bus addressing, kind → direction/command, data → payload by kind, byte enables → lane mask, response → status (every code), and the two capability flags from the real bus and driver.
- The recipe covers any bus whose register access is a single transaction; if access is a multi-step protocol (indirect, paged, framed), use a user-defined frontdoor (4.6), with the adapter translating the transfers within it.
reg2busmust fully specify the bus item, including protocol-specific fields the standard buses fold away — a missing command/direction field makes the driver drive writes as reads.- Confirm writes reach the DUT with a backdoor peek (it reads the real hardware) — the auto-predicted mirror can make an ineffective write look correct.