The DMA moves into a subsystem. APB is now driven by a real CPU model, and the input stream comes from an upstream block, so the subsystem bench reuses dma_env with the APB agent and in_agt passive. The first run dies at time 0 with a null-object access in dma_base_test::reg_write. The second, after a quick fix, runs to the end with SB_EMPTY: nothing was compared. Two failures, two different boundaries. How do you find which one is broken without guessing?
A reusable env is assembled differently in each bench. Which agents are active is a policy, and it changes the shape of the tree: passive agents have no sequencer or driver, so anything that expects them (tests, virtual sequences, handle copies) has to cope. When something goes wrong in a composed bench, the failure almost always sits on one of five boundaries: something was not created, not configured, not connected, has the wrong activity, or a checker is not ingesting traffic.
Debugging those in a fixed order is like a mechanic's checklist for a car that will not start: fuel, spark, air, in that order, because each step is cheap and rules out a whole class of causes. Random poking ("let's look at waves") skips the cheap checks and usually starts at the most expensive one.
Agent | Block bench | Subsystem bench | What changes for tests |
|---|---|---|---|
| Active: the test programs the DMA | Passive: the CPU model programs it | No |
| Active: sequences make packets | Passive: the upstream block sends them | No packet sequences on this agent |
| Passive | Passive | Nothing: the scoreboard still checks every word |
| Checks from three monitors | Checks from the same three monitors | Nothing: this is the payoff |
The last two rows are the point of the whole architecture. The scoreboard never depended on stimulus, only on monitors, so it keeps checking the DMA at subsystem level without a line of change. Everything that does change is on the stimulus side.
Put the cheap checks into the base test once, and every failing run carries its own evidence. This report_phase prints the activity of each agent, the scoreboard's ingest counters and the analysis connections; the topology is already printed at end_of_elaboration.
// snippet: added to a base test
function void report_phase(uvm_phase phase);
`uvm_info("DBG", $sformatf("apb=%s in=%s out=%s",
env.apb.get_is_active().name(), env.in_agt.get_is_active().name(),
env.out_agt.get_is_active().name()), UVM_LOW)
`uvm_info("DBG", $sformatf("sb: %0d matched, %0d mismatched, %0d expected left",
env.sb.n_match, env.sb.n_mismatch, env.sb.expected.size()), UVM_LOW)
env.apb.ap.debug_connected_to(); // prints what each port reaches
env.in_agt.ap.debug_connected_to();
env.out_agt.ap.debug_connected_to();
endfunctionQuestion | UVM's answer | Typical finding |
|---|---|---|
Was it created, and as which type? |
| An override that never applied; a passive agent with a driver |
Did configuration reach it? |
| A |
Is every port connected? |
| A monitor port with no subscribers |
Is it active as intended? |
|
|
Is the checker hearing traffic? | The scoreboard's own counters | Zero compares: stimulus never ran, or the test ended early |
Step 1 costs nothing, because the bench prints its topology at time 0. In the block bench, the stream agents print as in The Agent as the Reuse Unit: in_agt has drv, mon and sqr, while out_agt has only mon.
In the subsystem bench, in_agt would look like out_agt: a monitor and nothing else. Read that against the test code and the hook's first failure explains itself before you open a single source file.
The null at time 0 is an activity problem surfacing in a test. dma_base_test::reg_write starts apb_rw_seq on env.apb.sqr. With the APB agent passive, there is no sequencer: the handle is null, and starting a sequence on it is a null-object access. The test was written for a block bench, where the APB agent is always active. The fix is not to make the agent active again (it would fight the CPU model on the APB wires); it is a subsystem test that does not program registers itself, and a base test that checks get_is_active() before using a sequencer, failing with a clear message instead of a null access.
The `SB_EMPTY` after the quick fix is an ingestion problem with a cause on the stimulus side. With the test no longer programming the DMA, nobody started a job: the CPU model's software had not been loaded in this configuration. The monitors were connected and the scoreboard was listening, but there was no traffic, and the scoreboard's "nothing was compared" check turned a silent false pass into a visible failure. That check is the cheapest piece of insurance in the bench; without it, this run would have been green.
SB_EMPTY. Which triage step comes first?Connectivity (step 3). The monitors see traffic, so creation, configuration and activity on the monitor side are fine; the transactions are not reaching the scoreboard. Check debug_connected_to() on each agent's ap and the env's connect_phase before anything else.
Activity decides what the agent builds. uvm_agent::build_phase reads is_active once, and the driver and sequencer are created or skipped then. A later change rebuilds nothing, and the tree is frozen after build anyway, so creating a driver later would be an ILLCRT fatal.
Every row whose check uses monitors (data XOR, early TLAST via STATUS reads observed on APB, W1C behaviour if software exercises it) can still be checked passively. Rows that need deliberate stimulus, such as KEY written mid-job or a long packet, need the CPU software or the upstream block to create them; the passive env can only check what real traffic happens to do.
Next: the bench's structure is in place. Phases explains the schedule that builds, connects and runs it, and how a test decides it is finished.