Skip to main content

RAL Testbench Integration

The Register Abstraction Layer built pwm_reg_block as a standalone object model — accurate, but with nowhere for reg.write() to actually send an access. Closing that gap needs two more pieces: an adapter that translates RAL's abstract read/write request into whatever bus protocol actually drives the PWM peripheral, and — for the explicit prediction the previous page recommended — a predictor watching that same bus. This page wires both into a real environment.

The bus this example targets​

A small register-oriented bus: one transaction, write/addr/data, moving 32 bits per access — the same width pwm_reg_block's create_map() call already assumed.

class reg_bus_txn extends uvm_sequence_item;
rand bit write;
rand bit [7:0] addr;
rand bit [31:0] data;

`uvm_object_utils(reg_bus_txn)

function new(string name = "reg_bus_txn");
super.new(name);
endfunction
endclass

The driver and monitor that move reg_bus_txn on and off real pins aren't shown again here — they're built exactly as The Driver and The Monitor & Subscriber taught, just for this protocol instead of the FIFO's. What's new in this page is everything downstream of that monitor's analysis port.

The adapter: translating between uvm_reg_bus_op and the real protocol​

class pwm_reg_adapter extends uvm_reg_adapter;
`uvm_object_utils(pwm_reg_adapter)

function new(string name = "pwm_reg_adapter");
super.new(name);
endfunction

virtual function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
reg_bus_txn txn = reg_bus_txn::type_id::create("txn");
txn.write = (rw.kind == UVM_WRITE);
txn.addr = rw.addr;
txn.data = rw.data;
return txn;
endfunction

virtual function void bus2reg(uvm_sequence_item bus_item, ref uvm_reg_bus_op rw);
reg_bus_txn txn;
if (!$cast(txn, bus_item))
`uvm_fatal("ADAPTER", "Bus item is not a reg_bus_txn")
rw.kind = txn.write ? UVM_WRITE : UVM_READ;
rw.addr = txn.addr;
rw.data = txn.data;
rw.status = UVM_IS_OK;
endfunction
endclass

reg2bus() runs whenever RAL issues a frontdoor access — it turns the generic, protocol-agnostic uvm_reg_bus_op into the concrete reg_bus_txn the sequencer actually sends to the driver. bus2reg() runs in the reverse direction, and it's used in two different situations: packaging a just-completed frontdoor access's result back for RAL, and — unmodified, same function — translating raw bus traffic the predictor observes independently, whether or not that traffic originated from a RAL call at all.

Locking and connecting the map​

class pwm_env extends uvm_env;
`uvm_component_utils(pwm_env)

pwm_reg_block regmodel;
reg_bus_agent bus_agent;
pwm_reg_adapter adapter;
uvm_reg_predictor #(reg_bus_txn) predictor;

function new(string name, uvm_component parent);
super.new(name, parent);
endfunction

function void build_phase(uvm_phase phase);
super.build_phase(phase);
bus_agent = reg_bus_agent::type_id::create("bus_agent", this);

regmodel = pwm_reg_block::type_id::create("regmodel");
regmodel.build();
regmodel.lock_model();

adapter = pwm_reg_adapter::type_id::create("adapter");
predictor = uvm_reg_predictor#(reg_bus_txn)::type_id::create("predictor", this);
endfunction

function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
regmodel.default_map.set_sequencer(bus_agent.sequencer, adapter);
predictor.map = regmodel.default_map;
predictor.adapter = adapter;
bus_agent.monitor.ap.connect(predictor.bus_in);
endfunction
endclass
  • lock_model() finalizes the register map's structure — no register or field can be added after this call, and the frontdoor API refuses to run against a map that hasn't been locked yet.
  • set_sequencer() is the line that actually gives reg.write() somewhere to go: it tells the map which sequencer routes frontdoor accesses, and which adapter translates them.
  • predictor.map/predictor.adapter wire the predictor to the same map and adapter the frontdoor path uses, so both paths keep the mirror consistent the same way.
  • bus_agent.monitor.ap.connect(predictor.bus_in) reuses the exact analysis-port-to-imp connection pattern from The Scoreboard — just connecting to a predictor's built-in import instead of a scoreboard's.

Starting register access from a sequence​

class pwm_smoke_seq extends uvm_sequence;
`uvm_object_utils(pwm_smoke_seq)
pwm_reg_block regmodel;

function new(string name = "pwm_smoke_seq");
super.new(name);
endfunction

task body();
uvm_status_e status;
uvm_reg_data_t rdata;

regmodel.period.write(status, 32'd1000, .parent(this));
regmodel.duty.write(status, 32'd250, .parent(this));
regmodel.ctrl.enable.write(status, 1'b1, .parent(this));

regmodel.status.read(status, rdata, .parent(this));
if (rdata[0] !== 1'b1)
`uvm_error("SEQ", "PWM did not report running after enable")
endtask
endclass

.parent(this) on each write()/read() associates the access with the calling sequence, the same arbitration relationship The Sequencer & Sequence's start_item/finish_item pair established — without it, a register access issued mid-sequence has no sequencer arbitration context to run under.

uvm_reg_sequence is a purpose-built alternative base class

This page's example extends plain uvm_sequence and takes regmodel as an ordinary property, consistent with every sequence built earlier in this curriculum. UVM also ships uvm_reg_sequence, a base class purpose-built for register sequences — it carries a built-in model handle and skips the need to pass .parent(this) explicitly on every call. Either works; uvm_reg_sequence is worth reaching for once a testbench has enough register sequences that the boilerplate savings add up.

Two more sequence-level tools, now that the wiring exists​

Everything above exists so that a sequence's calls actually reach the DUT — two more uvm_reg methods lean directly on that same wiring and are worth knowing alongside write()/read():

  • update() — writes a register's desired value to the DUT only if desired and mirrored currently differ, skipping the bus access entirely when they already match. Useful after a batch of set() calls (which, per The Register Abstraction Layer, only touch the desired value) — update() is what actually sends whatever changed, instead of unconditionally re-writing every field regardless of whether it needs it.
  • mirror() — reads a register through the frontdoor exactly like read(), but additionally compares the value it reads against the current mirrored value, flagging an error if they disagree (when called with UVM_CHECK). This is a direct, on-demand version of the same consistency guarantee explicit prediction maintains continuously in the background — useful for a sequence that wants to assert "the model and the DUT agree right now" at one specific point, rather than trusting the predictor was never bypassed.

Both still route through the exact same default_map/adapter/sequencer wiring built above — neither is a separate access path.

One argument pwm_smoke_seq never passes: path​

Every write()/read() call in pwm_smoke_seq above only ever names status, the value, and .parent(this) — none of them mention path, a further optional argument of type uvm_path_e (UVM_FRONTDOOR, UVM_BACKDOOR, or UVM_DEFAULT_PATH, the default when the argument is omitted entirely, as it is here). Leaving it unset is exactly what routes every access in this page's example through everything just wired up — the adapter, the sequencer, the real bus. The identical call, regmodel.ctrl.enable.write(status, 1'b1, UVM_BACKDOOR, .parent(this)), would instead skip the bus (and every piece built on this page) entirely, poking the DUT's register storage directly — the subject of the next page.

What's next​

The frontdoor path — register model, adapter, predictor, sequencer — is fully wired. The next page covers the other route into the DUT's registers entirely: backdoor access, which skips the bus altogether.