A block-level testbench randomizes the field values of one transaction type, inside constraints that describe one protocol. An SoC has dozens of blocks, a real address map, real software running on real cores, and no single protocol to constrain against - so "randomize the stimulus" stops meaning what it meant one level down. This chapter looks at how stimulus gets generated once the DUT is an entire chip instead of one block, starting with the most basic question: at this level, what is actually being randomized?

This series has already covered how a testbench built from reused block-level UVCs, virtual sequences, and a shared register model comes together. This article looks at what those pieces are for from a stimulus point of view - not the infrastructure itself, but the specific things that get varied, seeded, and constrained once that infrastructure is in place.

10 min read | Intermediate Level

What You'll Learn

  • Why SoC-level randomization targets scenarios and configuration, not raw field values
  • The specific things that actually get randomized at this level - test selection, IP configuration, background traffic, and timing
  • How randomization stays inside a legal address map and register set instead of producing garbage the DUT was never meant to handle
  • How software-driven tests randomize through control registers instead of through a testbench object

Why This Isn't Block-Level Randomization Anymore

A block-level agent randomizes a transaction: an address, a burst length, a set of data bytes, each constrained by that block's own protocol rules. That model works because the agent owns the entire legal input space for one interface. At the SoC level, no single component owns the input space anymore - a write from a CPU core has to land inside the real, currently-configured address map, respect whatever the register model says is legal for the target register, and often needs to make sense as an instruction a real piece of software would actually issue. Randomizing a raw address the way a block-level agent would risks generating a transaction the design was never meant to receive, which tests nothing useful and can trip false failures that have nothing to do with a real bug.

So SoC-level randomization moves up a level of abstraction. Instead of randomizing what a transaction looks like, it randomizes which scenarios run, in what order, on which cores, against which configuration, and with what background load happening at the same time. The transaction-level randomization from the block-level UVCs still happens underneath all of this - it just gets driven by decisions made at a higher level instead of directly by the test.

What Actually Gets Randomized

Four categories cover most of what a mature SoC-level test actually varies from run to run:

  • Scenario and test selection. A virtual sequence typically wraps a library of smaller scenarios - one that exercises a DMA transfer, one that triggers an interrupt storm, one that runs a cache-coherency litmus test - and randomizes which of them run, in what order, and how many times, often with weights that favor scenarios the team wants more coverage on. This is randomization of the test plan itself, not of any single transaction inside it.
  • IP and system configuration. Peripherals are rarely tested at one fixed configuration. A UART's baud rate, stop-bit count, and parity setting; a clock generator's enabled outputs and frequency ratios; which power domains are active - all of these are legitimate randomization targets, because real software runs against many different configurations of the same hardware, and a bug that only shows up at one particular baud rate or clock ratio is exactly the kind of thing directed testing at a single fixed configuration would never find.
  • Background traffic. A directed scenario - say, a DMA transfer being checked for correctness - rarely runs alone on real hardware. SoC-level tests randomize what else is happening on the interconnect at the same time: other masters issuing unrelated traffic, a different core running its own instruction stream, a peripheral generating interrupts on an unrelated schedule. This is what actually exercises arbitration, contention, and the timing-sensitive bugs that a quiet, uncontended bus never triggers.
  • Timing and delays. Response latencies, ready/valid backpressure, and inter-transaction gaps are randomized within legal protocol bounds to vary exactly when things happen relative to each other, without changing what happens. A scenario that's functionally correct at every one of a dozen random delay profiles is a much stronger result than the same scenario passing once at whatever delay the RTL happened to produce.

Multi-core assignment belongs on this list too: which core runs which piece of a scenario, and with what randomized timing offset between them, is exactly the kind of randomization the multi-core stress tests from earlier in this series depend on to hit real interleavings instead of one predictable one.

Staying Inside the Legal Space

None of this is useful if it's unconstrained. Randomizing an address at the SoC level has to respect the address map and decode rules covered earlier in this series, and randomizing a register field has to respect what the register model knows is legal for that specific register - reserved bits, read-only fields, and value ranges the hardware actually implements all have to be excluded, or the test spends its time chasing self-inflicted illegal-access errors instead of real ones.

In practice this constraint information comes from two places. Static constraints live in the testbench itself: a specific test can use the UVM factory to substitute a more restrictive randomized object for one particular scenario, narrowing the legal space without touching the base sequence at all. Dynamic constraints come from the register model and address map directly, since both are generated from the same source of truth the RTL was built against, so a randomized register write is automatically bounded by what that register's fields actually allow.


class uart_config_seq extends uvm_sequence #(reg_transaction);
  `uvm_object_utils(uart_config_seq)

  // Randomized fields constrained to values the UART IP actually supports -
  // not every value that fits in the register's bit width is legal
  rand int unsigned baud_rate;
  rand bit [1:0]     stop_bits;
  rand bit           parity_en;

  constraint c_legal_baud {
    baud_rate inside {9600, 19200, 38400, 57600, 115200};
  }
  constraint c_legal_stop_bits {
    stop_bits inside {0, 1};   // 2'b10, 2'b11 are reserved encodings
  }

  virtual task body();
    // Rest of the code: writes baud_rate/stop_bits/parity_en through
    // the register model, which applies its own field-level constraints
    // (read-only bits, reserved fields) on top of these
  endtask
endclass

Software-Driven Randomization: A Different Loop

Where the test is a C program running on an embedded core rather than a SystemVerilog sequence, the object being randomized often isn't visible to the C code at all. A common pattern is for the SystemVerilog side to randomize a configuration object, then write the resulting values into memory-mapped control registers the C test reads at startup - the C side never calls randomize() itself, it just reacts to whatever legal configuration the SV side already settled on and prepared before the test began.

The reverse direction also shows up: a C test writes a constraint value - a range, a mode select, a seed - into a control register that a SystemVerilog monitor or sequence reads back and applies as an additional bound on its own randomization from that point forward. This keeps the SW test in control of what gets exercised without giving it direct access to the UVM class hierarchy generating the actual stimulus underneath it.

This is the same virtual-sequence and register-model infrastructure introduced earlier in this series - randomization at the SoC level is a matter of what these components choose to vary, not new machinery on top of them.

Scenario Selection as Its Own Modeling Problem

Once a testbench has enough scenarios and enough legal configurations, picking which combination to run stops being a simple weighted dice roll - some scenarios depend on others having happened first, some resources can't be used by two scenarios at once, and some configurations only make sense together. The industry's answer to this specific problem is the Portable Test and Stimulus Standard (PSS), which models available actions and their dependencies as a graph rather than a flat list, so a tool can generate a legal, randomized sequence of actions - and even resolve some of those choices statically, at test-generation time, rather than leaving every decision to be made randomly during simulation. The virtual-sequence-based scenario selection described above is effectively a hand-built, UVM-native version of the same idea: constraining which combinations of scenario, configuration, and resource are legal before randomization is allowed to pick among them.

Common Mistakes

Randomizing addresses without constraining them to the current address map.

An address map isn't static across every test - some regions may be disabled or remapped by configuration. A randomized address has to be constrained against the address map as it's currently configured, not a hardcoded list from the spec.

Randomizing configuration once at the start of a test and never revisiting it.

A single random configuration per test run means many legal configurations across the design's actual operating range never get exercised. Configuration needs to be part of what varies across the regression, not a one-time setup step.

Leaving background traffic out of directed scenarios to keep results easy to debug.

A directed scenario run in isolation, with no other bus activity, avoids exactly the arbitration and contention conditions most likely to expose a real SoC-level bug. Background traffic should be the default, not an occasional add-on.

Summary

In this article, you learned:

  • SoC-level randomization targets scenarios, configuration, and timing rather than raw transaction field values, because no single component owns the legal input space the way a block-level agent does
  • Scenario and test selection, IP/system configuration, background traffic, and timing/delays are the four main things that actually get varied from run to run
  • Multi-core assignment - which core runs what, with what timing offset - is part of this same randomization space
  • Randomization stays legal by constraining against the address map and register model directly, using static factory-based constraints or dynamic constraints layered on top
  • Software-driven tests randomize indirectly - reading a configuration the SV side already prepared, or writing constraint values into control registers the SV side applies going forward
Did you find what you were looking for?

Quiz

No quiz questions available for this article.