UVM
Case Study — Register Verification with RAL
Verify a DUT's register map with the UVM register abstraction layer over the APB agent — a register model of blocks, registers, and fields, an adapter that turns register operations into APB transactions, a predictor that keeps the model's mirror in sync, frontdoor versus backdoor access, and the predictor-not-connected bug that silently desyncs the mirror.
Industry Case Studies · Module 30 · Page 30.4
The Engineering Problem
The APB, AXI, and UART agents drive and observe buses at the pin level. Register verification sits a layer above them: almost every block on a chip is configured and queried through registers, and verifying them by hand-writing bus sequences with literal addresses and bit masks does not scale or survive an address-map change. The register abstraction layer (RAL) solves this — you build a model of the register map and access registers by name, and the model turns each access into a bus transaction on the agent you already built.
The case study layers RAL onto the APB agent from earlier. Three new pieces do the work. A register model (uvm_reg_block → uvm_reg → uvm_reg_field) mirrors the DUT's map: register names, field bits, access policies, reset values. An adapter (reg2bus / bus2reg) translates an abstract register operation into an apb_seq_item and back. A predictor keeps the model's mirror — its notion of each register's current value — in sync with what actually happens on the bus, by watching the APB monitor. With these wired, a test writes regmodel.ctrl.write(...) instead of driving address 0x00, and a read can be checked against the mirror automatically.
How do you verify a register map with RAL over an existing APB agent — a register model accessed by name, an adapter that turns register operations into APB transactions, and a predictor that keeps the mirror in sync — so tests read and write registers by name and the model self-checks against the DUT?
Motivation — why RAL instead of raw register sequences
You could test registers with directed APB sequences and literal addresses. RAL exists because that does not scale:
- Readable and map-change-proof.
regmodel.ctrl.write(...)says what it means and survives an address-map change;apb.write(32'h00, 32'h3)is a magic number that breaks the day the map moves. - Self-checking for free. The model mirrors expected values, so a read can be automatically compared against the model and a
mirror(UVM_CHECK)sweeps every register — checking you would otherwise hand-write. - Frontdoor and backdoor from one model. The same register accessed frontdoor through the bus verifies the access path; accessed backdoor through hierarchical signals sets up or checks state in zero time. Both come from one model.
- Reuse across the project. The register model is built or generated once from the spec and moves with the IP; every test and every integration level uses the same model rather than re-encoding the map.
The motivation, in one line: RAL trades the one-time cost of building a model and adapter for a readable, self-checking, reusable register interface that scales across tests and integration levels and survives address-map changes — which raw bus sequences cannot.
Mental Model
Hold RAL as a live facilities dashboard that mirrors a building's real switches, kept accurate by sensors on the wiring:
Imagine a building with hundreds of physical switches and gauges scattered through it — the real registers in the hardware. Operating the building by walking to each physical switch and remembering which unlabeled one at which location does what (raw bus access by address) is slow, error-prone, and breaks the moment maintenance moves a panel. So you build a control-room dashboard: a labeled model of every switch and gauge, organized the way the building actually is, where you flip "HVAC zone 3 enable" by name and the dashboard knows that maps to the physical switch at a particular panel and sends the command down the building's wiring. That is the register model and its adapter — names on the dashboard, translated to addresses on the wire. But a dashboard is only useful if it stays accurate, and here is the subtlety: some gauges change on their own — a fault sensor trips, the building itself sets a status bit — without anyone touching the dashboard. If the dashboard only updated itself when you operated a control, it would show stale, wrong readings for everything the building changed autonomously. So you wire sensors onto the actual building wiring that watch every signal that passes — every command and every autonomous change — and feed those observations back to keep the dashboard's readings synced to reality. That sensor feed is the predictor. Without it, the dashboard tracks only your own actions and silently drifts from what the building is actually doing, and you would trust a reading that is simply wrong. A building has hundreds of physical switches and gauges — the real registers. Operating it switch by switch by location (raw address access) is slow and breaks when a panel moves. So you build a labeled control-room dashboard — a model of every switch organized like the building — where you flip a control by name and it translates to the physical address and sends it down the wiring (the model and adapter). But a dashboard is only useful if it stays accurate, and some gauges change on their own (the building sets a status bit). If the dashboard only updated on your actions, it would show stale readings for autonomous changes. So you wire sensors onto the building's wiring that watch every signal and feed it back to keep the dashboard synced — that is the predictor. Without it, the dashboard tracks only your actions and silently drifts from reality.
So building register verification is wiring the control room: the register model is the labeled dashboard, the adapter translates names to bus addresses, and the predictor is the sensor feed that watches the real APB traffic and keeps the mirror accurate — including for registers the DUT changes itself. The bug you must never ship is the disconnected predictor: a dashboard that tracks only your own writes and silently lies about everything else.
The RAL Data Path
A register access flows: the model issues an abstract operation, the adapter turns it into an APB transaction, the APB agent drives it, and the predictor — watching the monitor — updates the mirror from the observed traffic.
The figure has two flows. The command flow (top) is model → adapter → agent → bus: a register operation becomes an APB transaction via reg2bus and is driven by the agent you already built. The observation flow (bottom) is bus → monitor → predictor → mirror: the monitor sees every transfer and the predictor, via bus2reg, updates the model's mirror from what actually happened. The observation flow is the part engineers forget — and it is what makes the mirror trustworthy, because it tracks everything on the bus, not just the model's own writes. With both flows wired, the model is a faithful, self-updating replica of the DUT's registers.
The register env's component tree is the APB agent plus the predictor; the register model and the adapter are uvm_objects the env holds, not components in the tree.
The hierarchy shows the reuse: the APB agent is unchanged from the APB case study, and the env adds the predictor as a sibling component plus the model and adapter as owned objects — RAL is a layer assembled over the existing agent, not a new agent.
The Register Model and the Env
The model declares the map as classes; the env builds the model, the APB agent, the adapter, and the predictor, and wires them in connect_phase.
class ctrl_reg extends uvm_reg;
rand uvm_reg_field enable; // RW
rand uvm_reg_field mode; // RW
`uvm_object_utils(ctrl_reg)
function new(string name = "ctrl_reg"); super.new(name, 32, UVM_NO_COVERAGE); endfunction
virtual function void build();
enable = uvm_reg_field::type_id::create("enable");
enable.configure(this, 1, 0, "RW", 0, 1'h0, 1, 1, 0); // 1 bit at offset 0, reset 0
mode = uvm_reg_field::type_id::create("mode");
mode.configure(this, 2, 1, "RW", 0, 2'h0, 1, 1, 0); // 2 bits at offset 1
endfunction
endclass
class status_reg extends uvm_reg;
uvm_reg_field busy; // RO — the DUT sets this on its own
`uvm_object_utils(status_reg)
function new(string name = "status_reg"); super.new(name, 32, UVM_NO_COVERAGE); endfunction
virtual function void build();
busy = uvm_reg_field::type_id::create("busy");
busy.configure(this, 1, 0, "RO", 0, 1'h0, 1, 0, 0); // read-only status bit
endfunction
endclass
class my_reg_block extends uvm_reg_block;
rand ctrl_reg ctrl;
status_reg status;
uvm_reg_map map;
`uvm_object_utils(my_reg_block)
function new(string name = "my_reg_block"); super.new(name, UVM_NO_COVERAGE); endfunction
virtual function void build();
ctrl = ctrl_reg::type_id::create("ctrl"); ctrl.configure(this); ctrl.build();
status = status_reg::type_id::create("status"); status.configure(this); status.build();
map = create_map("map", 0, 4, UVM_LITTLE_ENDIAN); // base 0, 4-byte bus
map.add_reg(ctrl, 32'h00, "RW"); // ctrl at 0x00
map.add_reg(status, 32'h04, "RO"); // status at 0x04
lock_model();
endfunction
endclassThe model is the DUT's map as a class tree. Each uvm_reg_field is configured with its width, bit offset, access policy ("RW", "RO", …), and reset value — enable is one RW bit at offset 0, busy is a RO status bit the DUT drives. Each uvm_reg declares its fields in build(); the uvm_reg_block creates the registers and a uvm_reg_map that assigns each its address (ctrl at 0x00, status at 0x04) and locks the model. The access policy matters: modeling a RO status bit as RW would make the model expect writes to stick, and a check would wrongly fail — the model must match the spec.
class apb_reg_adapter extends uvm_reg_adapter;
`uvm_object_utils(apb_reg_adapter)
function new(string name = "apb_reg_adapter"); super.new(name); endfunction
virtual function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
apb_seq_item tr = apb_seq_item::type_id::create("tr");
tr.write = (rw.kind == UVM_WRITE);
tr.addr = rw.addr;
tr.data = rw.data;
return tr; // register op → APB transaction
endfunction
virtual function void bus2reg(uvm_sequence_item bus_item, ref uvm_reg_bus_op rw);
apb_seq_item tr;
if (!$cast(tr, bus_item)) return;
rw.kind = tr.write ? UVM_WRITE : UVM_READ;
rw.addr = tr.addr;
rw.data = tr.data;
rw.status = tr.slverr ? UVM_NOT_OK : UVM_IS_OK; // APB transaction → register op
endfunction
endclass
class reg_env extends uvm_env;
`uvm_component_utils(reg_env)
apb_agent agent;
my_reg_block regmodel;
apb_reg_adapter adapter;
uvm_reg_predictor #(apb_seq_item) predictor;
// ... new() ...
function void build_phase(uvm_phase phase);
regmodel = my_reg_block::type_id::create("regmodel");
regmodel.build(); // build + lock the model
agent = apb_agent::type_id::create("agent", this);
adapter = apb_reg_adapter::type_id::create("adapter");
predictor = uvm_reg_predictor#(apb_seq_item)::type_id::create("predictor", this);
endfunction
function void connect_phase(uvm_phase phase);
regmodel.map.set_sequencer(agent.seqr, adapter); // FRONTDOOR: ops run on the APB sequencer
predictor.map = regmodel.map; // explicit prediction setup
predictor.adapter = adapter;
agent.mon.ap.connect(predictor.bus_in); // ← the line people forget: monitor → predictor
endfunction
endclassThe adapter is the translator: reg2bus packs a register operation into an apb_seq_item, and bus2reg unpacks an observed apb_seq_item back into register terms (including mapping slverr to a status). The env's connect_phase does three wirings: map.set_sequencer(agent.seqr, adapter) makes frontdoor register ops run on the APB agent's sequencer through the adapter; predictor.map/adapter configure the predictor; and agent.mon.ap.connect(predictor.bus_in) feeds every observed transfer to the predictor for explicit prediction. That last connection is the one engineers omit — and its absence is the DebugLab.
Frontdoor, Backdoor, and the Register Test
A register sequence accesses registers by name — frontdoor through the bus, or backdoor through hierarchical signals — and mirror(UVM_CHECK) compares the model to the DUT.
class reg_smoke_seq extends uvm_reg_sequence;
`uvm_object_utils(reg_smoke_seq)
my_reg_block regmodel;
function new(string name = "reg_smoke_seq"); super.new(name); endfunction
task body();
uvm_status_e status;
uvm_reg_data_t value;
// FRONTDOOR write: runs as a real APB transfer through the agent
regmodel.ctrl.write(status, 32'h0000_0003); // enable=1, mode=1
// FRONTDOOR read: real APB read; the value is also compared against the mirror
regmodel.ctrl.read(status, value);
// BACKDOOR write: hierarchical poke, zero simulation time, no bus activity
regmodel.ctrl.write(status, 32'h0000_0001, UVM_BACKDOOR);
// Read the DUT-updated RO status; explicit prediction kept the mirror in sync
regmodel.status.read(status, value);
// Compare the whole model's mirror against the DUT (frontdoor reads + check)
regmodel.mirror(status, UVM_CHECK);
endtask
endclassThe sequence is register access by name. ctrl.write(status, 'h3) issues a frontdoor write — the model uses the adapter and the APB agent to drive a real bus transfer — and ctrl.read does a frontdoor read whose value RAL also checks against the mirror. The UVM_BACKDOOR write pokes the register through its hierarchical path in zero time with no bus activity, the fast way to set state. Reading status works because the predictor kept the mirror synced to the DUT's autonomous update of busy. Finally mirror(UVM_CHECK) reads every register frontdoor and compares it against the model — a one-line sweep of the whole map. None of this would self-check correctly without the predictor connection.
Waveform Perspective — a frontdoor register write is an APB transfer
A frontdoor regmodel.ctrl.write(status, 'h3) is not magic — RAL turns it into exactly the APB setup/access transfer the agent already knows how to drive.
Frontdoor write of ctrl = 0x3 — RAL drives it as a normal APB transfer to address 0x00
5 cyclesThe waveform makes the layering concrete: a register write by name is, on the wire, the same APB transfer from the APB case study — reg2bus produced an apb_seq_item with addr=0x00, data=0x3, and the agent drove the setup-then-access handshake exactly as before. The new part is the mirror update: the monitor observed this transfer and the predictor set ctrl's mirror to 0x3. So one bus transfer both performs the command and keeps the model honest — provided the predictor is connected.
DebugLab — the mirror that silently lied
A register check fails (or worse, passes wrongly) — the predictor was never connected
Register writes and reads worked, but mirror(UVM_CHECK) reported a mismatch on the status register: the model expected busy = 0, the DUT read back busy = 1. Stranger, a test that only wrote ctrl and never touched status also failed the mirror check on status. The APB transfers on the waveform were all correct, and the ctrl register checks passed — only the DUT-updated status was wrong in the model.
The env built the predictor but never connected it to the monitor, so prediction fell back to the model's own writes only — the mirror never saw the DUT's autonomous update of busy:
present: predictor = uvm_reg_predictor#(apb_seq_item)::type_id::create("predictor", this);
present: predictor.map = regmodel.map; predictor.adapter = adapter;
MISSING: agent.mon.ap.connect(predictor.bus_in); // monitor → predictor never wired ✗
effect: no observed traffic reaches the predictor → mirror updates only on model-issued ops
result: the DUT sets status.busy=1 on its own; the mirror still holds 0 → mirror(UVM_CHECK) fails
fix: agent.mon.ap.connect(predictor.bus_in); // explicit prediction from observed bus ✓With the predictor unconnected, the model used implicit/auto prediction — it updated the mirror only for the operations it issued itself. That is fine for a plain RW register the testbench owns, which is why ctrl checked out. But status.busy is a RO bit the DUT sets, an event the model never issued and therefore never saw — so the mirror stayed at its reset 0 while the DUT moved to 1. The mismatch is real in the sense that the model is wrong, not the DUT. The danger is the symmetric case: a register the testbench wrote and the DUT silently failed to update would pass against a stale-but-matching mirror, hiding a real bug.
The tell is a mirror mismatch on registers the DUT changes autonomously, or checks that pass despite a suspicious DUT. Localize it at the predictor wiring:
- Check that the monitor's analysis port feeds the predictor.
agent.mon.ap.connect(predictor.bus_in)must exist. Its absence means the mirror tracks only model-issued ops. - Confirm explicit prediction is intended for DUT-updated registers. Any RO/status/self-clearing register that the DUT drives needs the predictor to observe the bus; auto prediction cannot track it.
- Cross-check frontdoor versus backdoor. Read a suspect register backdoor (the true DUT value) and compare to the mirror; a divergence with no predictor connection confirms the desync.
Wire explicit prediction whenever the DUT changes registers on its own:
- Connect the predictor to the monitor in
connect_phase.agent.mon.ap.connect(predictor.bus_in), withpredictor.mapandpredictor.adapterset — the standard explicit-prediction setup. - Use explicit prediction for any DUT-driven register. RO status, self-clearing, write-one-to-clear, or anything another master can touch needs the observed-bus feed; auto prediction only tracks the model's own writes.
- Verify the mirror against backdoor in bring-up. A backdoor read of a few registers compared to the mirror confirms the predictor is actually keeping the model synced before you trust a check.
The one-sentence lesson: the predictor must be connected to the monitor's analysis port for explicit prediction; without it the mirror updates only on the model's own writes and silently desyncs from any register the DUT changes itself — so register checks fail falsely or, worse, pass against a stale mirror.
Common Mistakes
- Not connecting the predictor to the monitor. Without
mon.ap.connect(predictor.bus_in), the mirror tracks only model-issued ops and desyncs from DUT-driven registers — the DebugLab. - Wrong field access policy. Modeling a RO bit as RW (or vice versa) makes the model expect the wrong behavior, so checks fail or hide bugs. The policy must match the spec.
- Wrong address in the map.
map.add_reg(reg, addr, …)must match the DUT; a wrong address sends accesses to the wrong register and every check is meaningless. - Confusing frontdoor and backdoor. Frontdoor verifies the bus access path and takes real time; backdoor sets/checks state in zero time but bypasses and does not verify the path. Use frontdoor to verify access, backdoor to set up.
- Forgetting to lock the model.
lock_model()finalizes the map; an unlocked model behaves unexpectedly. - Trusting a green mirror check without verifying prediction. A stale mirror can match a stale DUT; confirm the predictor is actually updating it (backdoor cross-check) before trusting checks.
Senior Design Review Notes
Interview Insights
A RAL integration has the register model, the map, the adapter, and the predictor, layered over a bus agent. The register model is a class tree — a uvm_reg_block containing uvm_reg registers containing uvm_reg_field fields — that mirrors the DUT's register map: names, bit positions, access policies, and reset values. The uvm_reg_map within the block assigns each register an address and is what register accesses resolve through. The adapter, a uvm_reg_adapter, translates between the abstract register operation and the bus transaction: reg2bus packs a register read or write into the agent's sequence item, and bus2reg unpacks an observed bus item back into register terms. The predictor, a uvm_reg_predictor, keeps the model's mirror — its notion of each register's current value — in sync with reality by observing the bus monitor's analysis port and updating the mirror through the adapter, which is explicit prediction. Underneath sits the bus agent you already built, whose sequencer and driver carry frontdoor register accesses and whose monitor feeds the predictor. The flow is: a register op goes through the map and adapter to the agent, which drives the bus; the monitor observes the transfer and the predictor updates the mirror. So the model gives names and structure, the map gives addresses, the adapter translates to and from the bus, and the predictor keeps the mirror honest. The understanding to convey is the four pieces and the two flows — command out through the adapter and agent, observation back through the monitor and predictor — which is the whole RAL architecture.
Frontdoor access performs the register operation as a real bus transaction through the adapter and agent, exercising the actual access path and consuming simulation time, while backdoor access reads or writes the register directly through hierarchical signal paths, bypassing the bus in zero time. Frontdoor is the real thing: a register write becomes a bus transfer that the driver drives onto the interface, the DUT decodes the address, and the register updates — so it verifies the access path, the address decode, the protocol, and the access policy. You use it when the point is to verify the register is correctly accessible over the bus. Backdoor uses the register's hierarchical path in the DUT to poke or peek its storage directly, with no bus activity and in zero time. You use it for speed and for setup or checking that is not the object of the test: initialize many registers instantly, check a value without perturbing the bus, or verify a field changed without a bus read. A powerful idiom is to write frontdoor and check backdoor, or vice versa, confirming the bus path and the actual storage agree. The trade-off is that frontdoor verifies the path but is slow and only as good as the bus model, while backdoor is instant and direct but bypasses and therefore does not verify the path and depends on correct hierarchical paths in the model. The understanding to convey is frontdoor-verifies-the-path-in-real-time versus backdoor-is-fast-but-bypasses, and the cross-check idiom that uses both — which shows you treat RAL as a verification tool, not just a convenience.
The predictor keeps the register model's mirror in sync with the DUT, and the difference is the source of the updates: auto prediction updates the mirror from the model's own operations, while explicit prediction updates it from the actual bus traffic observed by the monitor. The mirror is the model's record of each register's current value, and it must be accurate for self-checking reads and for mirror checks to mean anything. With auto prediction, the register layer assumes each frontdoor operation it issues succeeds as modeled and updates the mirror immediately — simple, but blind to anything that changes a register outside the model's own ops. With explicit prediction, a uvm_reg_predictor is connected to the bus monitor's analysis port; it observes every transfer, including ones the model did not initiate — the DUT updating a status register, a self-clearing bit, another master writing — and updates the mirror from the observed traffic via the adapter's bus2reg. The trade-off: auto is less setup but only tracks the model's own operations and can drift from reality; explicit needs the predictor wired to a monitor but tracks all register activity, which matters whenever registers change outside your writes. The practical rule is that any DUT-driven register — read-only status, self-clearing, write-one-to-clear — requires explicit prediction, because auto prediction cannot see those changes. The common bug is building the predictor but forgetting to connect it to the monitor, which silently leaves you on auto prediction and desyncs the mirror. The understanding to convey is mirror-keeping with auto-tracks-model-ops versus explicit-tracks-observed-bus, and that DUT-driven registers need explicit, which shows you understand RAL internals where many stumble.
Because RAL gives a readable, self-checking, reusable, map-change-proof register interface, whereas literal-address sequences are unreadable magic numbers that break on map changes and have no built-in checking. Writing apb.write of address 0x00 with data 0x3 is opaque — you cannot tell what register or field it touches without the spec open — and it is fragile: the day the address map moves, every literal breaks and you hunt them down. RAL replaces this with regmodel.ctrl.enable.set and a write, which says what it means, and if the map moves you change the model in one place. RAL also gives self-checking for free: the model mirrors expected values, so a read is automatically compared against the model, and mirror with check sweeps every register, checking you would otherwise hand-write. It supports frontdoor and backdoor from the same model, so you verify the access path or set up state fast without re-encoding anything. And the model is reusable: built or generated once from the spec, it moves with the IP and serves every test and integration level, where literal sequences would be rewritten each time. The cost is the one-time effort of building the model and the adapter, and wiring the predictor — real but paid once. So the trade is setup cost now for readability, self-checking, reuse, and map-resilience across the whole project. The understanding to convey is that RAL scales and self-checks where literal sequences do not, which is why register-heavy verification standardizes on it.
If the bus transfer is correct on the waveform but the model reports the wrong value, the problem is in the model or its prediction, not the access, so I look at the predictor connection, the field access policy, the map address, and the adapter's bus2reg. First, the predictor: if it is not connected to the monitor's analysis port, the mirror only tracks the model's own writes, so any DUT-driven change — a status bit, a self-clearing field — never reaches the mirror and a check against it fails even though the bus and DUT are correct; this is the most common cause and I would confirm mon.ap.connect(predictor.bus_in) exists and explicit prediction is set. Second, the access policy: if a field is modeled RO that is actually RW, or a write-one-to-clear modeled as plain RW, the model's expectation diverges from the DUT's real behavior, so the value mismatches; I would check the policy against the spec. Third, the map address: if the register is mapped to the wrong address, the access goes to a different register than intended, so the value is someone else's; I would verify the address. Fourth, the adapter's bus2reg: if it mis-extracts data, address, or status from the bus item, the predictor updates the mirror with wrong values; I would check the round-trip. A quick triangulation is to read the register backdoor to get the true DUT value and compare against both the mirror and the frontdoor read — that isolates whether the DUT is right and the model is wrong, which points at the model side. The understanding to convey is that a correct bus transfer with a wrong model value localizes to the model and prediction — predictor connection first, then policy, address, and adapter — which shows you debug RAL systematically rather than blaming the bus.
Exercises
- Add a register. Add a write-one-to-clear interrupt-status register to the model with the correct field policy, and state why its prediction needs the connected predictor.
- Wire the predictor. Given an env that builds but does not connect the predictor, write the
connect_phaselines that enable explicit prediction. - Frontdoor vs backdoor. For "initialize 64 registers before the real test" and "verify the bus correctly writes ctrl," choose frontdoor or backdoor for each and justify.
- Trace the path. Walk a
regmodel.ctrl.write(status, 'h3)from the model call through the adapter to the APB pins and back through the monitor to the mirror. - Catch the desync. Describe how a backdoor-versus-mirror cross-check would reveal a missing predictor connection.
Summary
- RAL verifies registers by name over the bus agent you already built: a register model (block → reg → field with access policy and reset), a map assigning addresses, an adapter (
reg2bus/bus2reg), and a predictor. - The command flow is model → adapter → agent → bus (a register op becomes a real bus transfer); the observation flow is bus → monitor → predictor → mirror (explicit prediction keeps the model synced to reality).
- Frontdoor access verifies the real path in simulation time; backdoor sets or checks state in zero time but bypasses the path — use frontdoor to verify, backdoor to set up, and cross-check with both.
- The predictor must be connected to the monitor for explicit prediction; without it the mirror tracks only the model's own writes and silently desyncs from any DUT-driven register — the DebugLab, which fails checks falsely or passes them against a stale mirror.
- RAL trades one-time model/adapter setup for a readable, self-checking, reusable, map-change-proof register interface that raw literal-address sequences cannot provide.
- The durable rule of thumb: build register verification as a model + map + adapter + predictor over your bus agent, access registers by name frontdoor or backdoor, and always connect the predictor to the monitor for explicit prediction — because the model is only trustworthy if its mirror tracks everything on the bus, not just the writes you issued.
Next — Complete SoC Environment: with APB, AXI, and UART agents and a register layer built, the final case study composes them into a system-level environment — reusing each agent passive or active, coordinating them with a virtual sequencer and virtual sequences, and checking end-to-end with a system scoreboard — the payoff of building every piece as reusable verification IP.