Every block-level testbench you've built so far has one job: generate legal stimulus and check the response, entirely from SystemVerilog. At SoC level, that assumption breaks the moment you add a CPU. The chip now boots real firmware, and that firmware - not your sequences - ends up driving most of the traffic that matters. Software stops being something the chip merely runs after tapeout and becomes a verification tool in its own right, present from the first bring-up test to the last system-level scenario.
13 min read | Intermediate Level
What You'll Learn
- Why a CPU in the DUT turns software into a stimulus generator, not just a payload
- The two ways compiled software actually executes in simulation, and why the difference matters
- The three jobs software does across a chip's lifetime, and why that's a bigger payoff than just "more stimulus"
- How a C test and an SV virtual sequence hand off control during a test
- Where this fits with the block-level UVCs and virtual sequencer from the previous article
Why Software Enters the Picture at SoC Level
An IP-level testbench has no CPU, so there's nothing to disagree with your sequences about what "legal" traffic looks like - you decide. An SoC has at least one CPU core, and that core fetches instructions from reset the moment simulation starts. Those instructions came from a real compiler, targeting the real memory map, and they will keep running whether or not your SV environment tells them to. From that point on, a large share of the transactions hitting your interconnect, your peripherals, and your DMA engine are produced by that fetch-decode-execute loop, not by a uvm_sequence.
This isn't a limitation to work around. Software is the only stimulus source that naturally exercises the chip the way it will actually be used: through its own register interface, respecting its own programming model, in the order a real driver would issue it. A DMA descriptor built by hand in an SV sequence can be legal but unrealistic; the same descriptor built by the production driver running on the CPU is guaranteed to look like silicon will actually see it.
This is why the block-level agents in the previous article get switched to passive at SoC level - the CPU is now the active driver on those interfaces, and the agent's job shifts to observing what software does rather than generating traffic itself.
"Software" in this article means bare-metal, cross-compiled diagnostic code written specifically to exercise the hardware - not a full operating system or RTOS boot. A real product's software stack may eventually run Linux or an RTOS on top of drivers, but booting that full stack is its own, much larger verification effort, usually reserved for emulation or FPGA prototypes rather than RTL simulation. The bare-metal C tests this article describes are what runs directly in the SoC testbench.
Two Ways Software Actually Executes in Simulation
"The CPU runs the C test" hides a real implementation choice, and the choice affects how faithfully the test represents real silicon:
- Full-functional model. The C test is cross-compiled for the target instruction set and actually executed - fetch, decode, execute, one instruction at a time - on an RTL model of the core or on an instruction set simulator (ISS) wired into the testbench. Every bus transaction the peripherals see is a genuine side effect of that execution, exactly as it would happen on real silicon.
- Bus-functional shortcut. Instead of executing real instructions, a bus-functional model (BFM) directly plays back the sequence of bus cycles (address, data, read/write) that the software would have produced. This skips instruction-level execution entirely, which is much faster to simulate, but only as accurate as whoever hand-built (or captured) that transaction sequence.
Bus-functional replay isn't a lesser technique - it's a deliberate simulation-speed trade-off. A common flow captures the real bus transactions from one full-functional run of a C test and saves them; later regressions replay that captured sequence through a BFM instead of re-running the whole CPU model, since the outcome is identical but far cheaper to simulate. What matters is knowing which mode a given test is running in, since only the full-functional model can catch a bug in the software's own instruction sequence.
Which mode you're in, and how the software image actually gets into memory before either can run, is covered in depth in Chapter 2 (loading software into simulation) and Chapter 5 (ISS and lockstep co-simulation). For this article, what matters is that "software" doesn't mean one fixed technique - and a testbench should be explicit about which mode a given test needs.
The Three Jobs Software Does Across a Chip's Life
Once software is in the loop, it typically does two distinct jobs within a single test, and a third job that pays off long after this testbench is retired:
- Bring-up: does the chip boot at all? The first software that runs on any new SoC bench is usually the simplest possible bring-up code - reset, fetch the first instruction, write one known value to one known register. This has almost no coverage value on its own, but it's the test that proves the CPU model, the memory map, and the software-loading path all actually work before anything more ambitious is attempted.
- Diagnostics: software as stimulus and checker. A C test programs a peripheral, triggers a DMA transfer, or walks through a realistic driver sequence - generating traffic that's difficult to reconstruct by hand in SV - and then reads back status registers to independently decide pass or fail, using the same visibility a real driver has, no more and no less.
- Reuse beyond simulation. A self-checking C test written for the UVM testbench doesn't have to stay there. The same source, recompiled, typically runs unmodified on emulation, on an FPGA prototype, and eventually on the real chip during post-silicon bring-up - so the investment in writing it well pays out multiple times across the project, not just once in simulation.
A self-checking C test is valuable precisely because its view is limited to what real software can see. If it needs internal RTL visibility to pass, it's testing something a real driver could never verify in the field - and it also can't be reused post-silicon, which erases the reuse benefit that makes software-driven tests worth writing in the first place. That check probably belongs in an SV monitor instead.
None of this happens outside the plan the rest of the testbench follows. A C test still needs to trace back to a specific scenario in the verification plan and still needs to close specific functional coverage bins, the same as any SV sequence would - "software covers it" isn't a special case that skips planning, it's one more way a planned scenario gets exercised. Article 4 in this chapter covers how software-driven scenarios fit into planning at the SoC level, and Chapter 8 covers how that coverage gets measured.
Coordinating a C Test with the SV Environment
Software running on the CPU and the SV testbench running the simulation are two independent control flows, so a test typically needs a handshake between them. A common pattern: the SV side starts the simulation and waits, the C test runs to completion and writes a result code to a known memory-mapped location, and an SV sequence polls that location to learn when the C test is done and whether it passed.
class wait_for_sw_done_seq extends uvm_sequence;
`uvm_object_utils(wait_for_sw_done_seq)
virtual task body();
bit [31:0] status;
// Poll the shared status register the C test writes to on exit
// (see TEST_STATUS_ADDR in soc_test.h in the C test sources)
do begin
#100ns;
mem_read(TEST_STATUS_ADDR, status);
end while (status == TEST_RUNNING);
if (status == TEST_FAIL)
`uvm_error("SW_TEST", "C test reported a failure")
endtask
endclass
The address and encoding of TEST_STATUS_ADDR are shared between the C header and the SV testbench, usually generated from the same address map source so the two sides can't drift out of sync. Chapter 2 covers this handoff in depth, including how the software image is built and loaded into simulation.
Polling a status register is the simplest handshake to write, but it isn't the only one, and it isn't what production software actually does. A closer-to-real alternative has the C test trigger a hardware interrupt on completion instead - the same mechanism a real driver would use - with an SV sequence or monitor waiting on that interrupt rather than spinning on memory reads. Some teams reserve a small range of memory-mapped addresses purely for this kind of synchronization, so that a write from the C test to a specific address is treated by the testbench as an event to react to, independent of any real hardware register at that location. Polling is fine for a first test; an interrupt-driven handshake exercises more of the real path and is worth moving to once the basic coordination works.
Software and SV Sequences Run Side by Side, Not Instead of Each Other
Bringing software in doesn't retire the virtual sequencer from the previous article. A realistic SoC scenario often needs both at once: the C test programs and starts a peripheral through its driver, while an SV virtual sequence independently injects traffic on an interface with no software visibility, such as an external stimulus agent or a fault-injection path. The two stimulus sources run concurrently and are checked by the same scoreboard, each covering what the other cannot reach.
Decide per interface, not per test, whether software or an SV sequence owns it. An interface with a real driver in the field should be exercised by software wherever possible; an interface with no software-visible register (an external DDR model, a debug-only path) stays SV-driven.
Common Mistakes
Mistake: Writing C tests with RTL-only checks.
Peeking at internal signals via backdoor access from C code (or relying on simulation-only hooks) produces a test that can never run on real silicon, defeating the reason to write it in C in the first place.
Mistake: No agreed handshake protocol.
If the C test and the SV environment don't share a single, versioned definition of the status register and its encodings, they drift apart silently - the SV side may report a false pass while the C test is still mid-failure recovery.
Mistake: Never testing beyond the bus-functional shortcut.
If every regression run replays captured bus transactions instead of executing the real instruction stream, a bug in the compiled software itself - a wrong branch, a bad pointer, a missed register field - can hide indefinitely, since the replayed traffic was captured from a run that already worked.
Summary
In this article, you learned:
- A CPU in the DUT makes embedded software an unavoidable stimulus source, not just a post-tapeout concern
- Software executes either as a full-functional model (real instructions, full fidelity) or a bus-functional replay (faster, but only as accurate as the capture it replays)
- Software does three jobs across a chip's life: bring-up, self-checking diagnostics, and reuse on emulation, FPGA, and silicon
- A shared status register - or an interrupt-based handshake closer to what real software does - coordinates a C test with an SV sequence
- Software-driven and SV-driven stimulus run concurrently, each covering interfaces the other can't reach