Verify a SoC from block checks to software-visible scenarios
Start system verification with an action software can request and an outcome the integrated hardware must expose. Reuse block-level checks, then add address selection, completion, interrupt routing and acknowledgement checks across the integration boundary. The runnable example below exercises those relationships through original register transactions; it runs no processor instruction set or UVM environment.
Build inside-out scenarios from visible actions
An inside-out scenario begins with the initiating control path and follows its effects outward. Here, a firmware-style sequence configures one of two engines, starts it, waits for completion and acknowledges its interrupt. The second engine shares the register transaction interface but has another address region. That arrangement creates an integration question a standalone engine test cannot answer: did the intended instance receive the operation while the other instance stayed unchanged?
Use a small set of explicit requirements before writing the sequence. In this example, reset must leave both status bytes clear. Region 0x100 must select engine A and region 0x200 engine B. Completion must remain observable when interrupts are masked. Enabling an interrupt after completion must expose the pending event, and acknowledging it must clear the visible interrupt. Those requirements determine the checks; executing lines of RTL does not define success by itself.
The sequence uses write_register and wait_done tasks as a small software-visible action model. These tasks do not fetch instructions, execute a driver binary or exercise a processor bus protocol. They drive the original model’s simple address/data interface directly. That boundary is useful for debugging decode and status relationships, provided the next integration stage explicitly adds the CPU, interconnect and software behavior it needs to claim.
Keep the software action and the hardware observation separate in the test record. A write returning from a task means the modeled transfer occurred; it does not establish that the requested operation completed. The test checks completion through status, then checks the interrupt and the untouched peer instance. This separation would also help distinguish a driver that wrote the wrong address from a peripheral that accepted the command but failed to complete it.
Run a block pair with assertions and coverage
Save this complete example as example.sv. It instantiates the same original engine twice and uses a simple address-region decoder. The engine has configuration, start and acknowledgement registers. A start launches a short countdown, after which done is set. The interrupt is the conjunction of done and the configured interrupt enable, so completion and interrupt presentation can be checked independently.
// SPDX-License-Identifier: MIT
`timescale 1ns/1ps
module engine(
input logic clk, reset, selected, write_enable,
input logic [3:0] address,
input logic [7:0] write_data,
output logic [7:0] status,
output logic irq
);
logic enabled=0, irq_enable=0, busy=0, done=0;
logic [2:0] remaining=0;
assign irq=done && irq_enable;
assign status={6'b0,done,busy};
always @(posedge clk) begin
if (reset) begin
enabled<=0; irq_enable<=0; busy<=0; done<=0; remaining<=0;
end else begin
if (busy) begin
remaining<=remaining-1'b1;
if (remaining==1) begin busy<=0; done<=1; end
end
if (selected && write_enable) begin
case(address)
0: begin enabled<=write_data[0]; irq_enable<=write_data[1]; end
4: begin
assert(enabled && !busy) else $fatal(1,"illegal start");
busy<=1; done<=0; remaining<=3;
end
8: done<=0;
default: $fatal(1,"invalid register");
endcase
end
end
end
Complete: cover property (@(posedge clk) !reset && done);
Interrupt: cover property (@(posedge clk) !reset && irq);
endmodule
module tb;
logic clk=0, reset=1, write_enable=0;
logic [11:0] address=0;
logic [7:0] write_data=0, status_a, status_b;
logic irq_a, irq_b;
wire select_a=address[11:8]==4'h1;
wire select_b=address[11:8]==4'h2;
engine a(clk,reset,select_a,write_enable,address[3:0],write_data,status_a,irq_a);
engine b(clk,reset,select_b,write_enable,address[3:0],write_data,status_b,irq_b);
always #5 clk=~clk;
task tick;
@(posedge clk); #1;
endtask
task write_register(input logic [11:0] addr,input logic [7:0] data);
@(negedge clk); address=addr; write_data=data; write_enable=1;
tick(); write_enable=0;
endtask
task wait_done(input bit which);
integer cycles;
cycles=0;
while (!(which?status_b[1]:status_a[1]) && cycles<8) begin
tick(); cycles++;
end
assert(cycles<8) else $fatal(1,"software-visible completion timeout");
endtask
RoutedA: cover property (@(posedge clk) write_enable && select_a);
RoutedB: cover property (@(posedge clk) write_enable && select_b);
initial begin
tick(); reset=0;
assert(status_a==0 && status_b==0) else $fatal(1,"reset status");
write_register(12'h100,8'h03);
write_register(12'h104,8'h01);
wait_done(0);
assert(irq_a && status_b==0 && !irq_b) else $fatal(1,"routing or IRQ");
$display("TRACE A done=%b irq=%b B_status=%h",status_a[1],irq_a,status_b);
write_register(12'h108,0); tick();
assert(!irq_a) else $fatal(1,"ack did not clear IRQ");
write_register(12'h200,8'h01);
write_register(12'h204,8'h01);
wait_done(1);
assert(status_b[1] && !irq_b) else $fatal(1,"masked completion");
$display("TRACE B done=%b masked_irq=%b",status_b[1],irq_b);
write_register(12'h200,8'h03); tick();
assert(irq_b) else $fatal(1,"pending interrupt lost");
write_register(12'h208,0); tick();
assert(!irq_b) else $fatal(1,"second ack");
$display("TRACE both interrupt acknowledgements cleared");
$display("PASS register-sequence scenarios; no processor ISA or UVM run");
$finish;
end
initial begin #600; $fatal(1,"timeout"); end
endmodule
Verilator invocation:
verilator --binary --timing --assert --language 1800-2012 --x-assign 0 --x-initial 0 -Wno-fatal --quiet-build --quiet-stats --quiet-exit --top-module tb --Mdir obj example.sv --coverage-user
./obj/Vtb +verilator+quiet +verilator+coverage+file+coverage.dat
The execution returned zero and printed the following literal output. The named coverage file was generated by the same run, using Verilator’s documented binary coverage mechanism1.
TRACE A done=1 irq=1 B_status=00
TRACE B done=1 masked_irq=0
TRACE both interrupt acknowledgements cleared
PASS register-sequence scenarios; no processor ISA or UVM run
- example.sv:86: Verilog $finish
The first trace confirms engine A completed with an interrupt while engine B’s status remained zero. The second confirms engine B completed while its interrupt was masked. A later assertion checks that enabling that interrupt exposes the already-pending completion; the final trace follows successful acknowledgements for both instances. Each conclusion depends on a corresponding assertion, rather than the trace label alone.
Stimulus changes occur at the negative edge and checks occur after positive-edge updates. The countdown is deterministic and the completion task has a bounded wait. A missing completion would therefore reach a named failure instead of hanging indefinitely. The final global timeout is a second guard against a sequence that never reaches normal termination; neither timeout was reached in the measured run.
Connect coverage records to checked requirements
The coverage file contains four named records: Complete, Interrupt, RoutedA and RoutedB. Their recorded counts are four, three, three and four respectively. Verilator merged the identical engine-level coverage locations across instances into records whose hierarchy uses tb.*; this file does not provide separate per-engine completion bins. The two routing cover statements remain distinct testbench locations.
A coverage hit is not automatically a successful requirement check. Complete can increment on more than one clock while done stays high, so its count is not the number of independent jobs. Interrupt similarly records cycles satisfying its expression. The assertions establish the selected outcomes; the cover statements show that the named conditions were encountered. No coverage percentage is calculated from these four counters.
| Requirement | Stimulus | Check | Coverage context |
|---|---|---|---|
| A address selects A | Configure and start 0x100 region | A interrupt; B status remains zero | RoutedA |
| Mask preserves completion | Start B with interrupt disabled | B done; B interrupt low | Complete, merged instances |
| Pending event survives masking | Enable B interrupt after completion | B interrupt becomes high | Interrupt, merged instances |
| Acknowledgement clears interrupt | Write each acknowledgement register | Corresponding interrupt low | Checked, no dedicated cover point |
The last row is intentionally not disguised as a covered bin. Its assertions passed, but this example has no dedicated acknowledgement cover statement. Likewise, reset status is asserted without a separate reset-sequence bin. A useful coverage review names these distinctions, including requirements with checks but no explicit coverage point and coverage points without a sufficiently specific checker.
For a larger plan, assign each requirement an identifier, its initiating operation, the observation point and its failure condition. Track prerequisites such as clocks, reset release and memory initialization alongside the scenario. A report can then state which requirement had evidence in a particular run without assuming that a large global number represents every system behavior.
Retain block checks and add integration contracts
A reusable block environment can retain its transaction definitions, reference behavior and local assertions while the surrounding stimulus and connectivity change. Accellera describes UVM as supporting reuse and interoperability, and lists a versioned reference implementation separately from the standard2. Those references describe an ecosystem; they are not evidence that this example ran UVM or that an arbitrary existing environment will run unchanged in a particular simulator.
At system level, verify the assumptions the block environment previously supplied directly. In this example, address selection belongs to the integration layer. A block-only test could drive selected correctly on every transaction and still miss a top-level decoder that routes a command to both instances. The assertion that B remains untouched after A’s operation is therefore useful evidence beyond the engine’s local countdown behavior.
Interrupt routing creates another integration contract. A block may generate the expected request while the system maps it to the wrong destination or loses a pending event during masking. Here, the direct irq_a and irq_b observations test the simple model only. Adding an interrupt controller would require checking its pending, enable, priority and acknowledgement behavior at the software-visible boundary, not merely preserving these two wires in a waveform.
Keep protocol claims tied to the implemented interface. The example has no ready/valid backpressure, bus error response, alignment checking, transaction identifier or concurrent master. Calling its tasks AXI, APB or a processor memory access would imply behavior that is absent. Use it to make the requirement-to-check relationship concrete, then replace the transaction driver with the actual integration interface while retaining the meaningful end-state expectations.
Turn a passing scenario into a useful regression
Preserve source bytes, compile options, execution arguments, return code and coverage artifact as one run record. Assertions are explicitly enabled in the recorded Verilator invocation; a later build that changes assertion controls is a different verification configuration3. Do not carry a previous PASS result forward when modifying the code, changing an address map or replacing a model with another implementation.
The example’s assertions include checks for an illegal start and an invalid register, but the measured sequence does not inject those failures. That is a concrete next extension: define the expected fatal location, issue the prohibited operation and distinguish the intended failure from a compile error or timeout. Similarly, reset during active work, simultaneous activity in both engines and lost acknowledgement are useful scenarios only after their expected behavior has been specified.
Avoid interpreting repeated execution as broader coverage when the stimulus is identical. This deterministic sequence is valuable because another engineer can reproduce it and compare the same observations. Exploring additional timing relationships, ordering permutations or software paths requires additional scenarios and a record of which requirements they exercised. The small example makes those missing cases visible instead of filling them with an unsupported coverage claim.
Review observability before increasing stimulus volume. A completion flag without a data check could hide a wrong result; an interrupt without its source identity could hide incorrect routing. The example has no computed payload, so it claims only control behavior. If a later engine produces data, add the expected payload and its destination to the contract, then demonstrate that the same scenario checks both control completion and the delivered result. That change creates a new obligation instead of silently broadening the meaning of the existing done bit.
For an embedded-software stage, run the actual program or a declared processor model, preserve its image and build identity, and correlate its completion result with hardware observations. For a commercial UVM stage, identify the actual implementation, supported language features, configuration and executed tests. Neither stage has been substituted by the register tasks here. Their value is to establish a reproducible integration contract that those richer environments can carry forward and challenge.
Not covered on this page
- A processor ISA, compiled firmware, a real interconnect protocol, external devices or a commercial UVM simulator.
- Concurrent engines, reset during work, backpressure, interrupt-controller behavior and injected illegal-start failures.
- Exhaustive functional coverage, assertion proof, performance, power behavior or silicon signoff.
Sources
-
Verilator simulation and coverage collection, guide 5.052, opened 24 October 2026. ↩
-
Accellera UVM reference implementations, listing includes UVM 2020-3.2 dated August 2026, opened 24 October 2026; library not executed. ↩
-
Verilator executable arguments, guide 5.052, opened 24 October 2026. ↩
How we test: methodology. Found a mistake? Corrections.