A phone SoC passes every functional test. In the first power-management regression, the always-on interrupt controller starts reporting an interrupt from the DMA block while the DMA is switched off. Nobody wrote that bug into the RTL: the RTL has no idea power can be switched. The bug lives in the power intent, a separate file that said where isolation cells go and missed one output. How does a simulator know a block is off, and what does the testbench have to check while it is?
Low-power chips switch off blocks they are not using, keep some state alive while they are off, and run blocks at different voltages. None of that is written in RTL. It is described in a power intent file in UPF (the Unified Power Format, IEEE 1801), a Tcl-based format that every implementation and verification tool reads alongside the RTL. Think of the RTL as a building's floor plan and the UPF as its electrical plan: which rooms are on which circuit breaker, which rooms keep their lights on in a blackout, and which doors lock when a room goes dark.
Concept | What it is | What goes wrong without it |
|---|---|---|
Power domain | A group of instances sharing one primary supply that can be switched or scaled together | Nothing to switch |
Supply set / supply net | The power and ground nets that feed a domain, with their states (on, off) | Simulator cannot tell a domain is off |
Power switch | The header or footer that connects a domain to its supply under a control signal | Domain can never turn off |
Isolation | A clamp on a signal leaving an off domain, holding it at 0, 1 or its last value | Floating or X values reach the always-on logic |
Retention | Flops with a shadow element on an always-on supply: save before power off, restore after power on | State lost; software must reprogram everything |
Level shifter | A cell that translates a signal between two domains at different voltages | Wrong logic levels or leakage between domains |
A power-aware simulator reads the UPF with the RTL (or the netlist) and adds this behaviour automatically. When a domain's supply goes off, it corrupts every flop and every output of that domain to X. When the isolation signal is active, it applies the clamp on the domain boundary. On the save signal it copies retained registers into their shadow, and on restore it copies them back. When the supply returns, non-retained flops stay X until reset. You change no RTL to get this; you add the UPF file and a power-aware option at compile time.
The listing is illustrative: standard IEEE 1801 commands for a DMA domain that can switch off, with an isolation clamp on its outputs and retention on its configuration register. The syntax follows IEEE 1801-2013 and later (earlier UPF 2.0 used different option names for the isolation and retention supplies). A real file also declares supply ports and connects them to the chip's pads; check which UPF version your tools accept before copying any line.
# Illustrative IEEE 1801 (UPF 2.1 or later) power intent for a switchable DMA
create_supply_net VDD
create_supply_net VDD_DMA
create_supply_net VSS
create_supply_set SS_AON -function {power VDD} -function {ground VSS}
create_supply_set SS_DMA -function {power VDD_DMA} -function {ground VSS}
create_power_domain PD_TOP -include_scope -supply {primary SS_AON}
create_power_domain PD_DMA -elements {u_dma} -supply {primary SS_DMA}
create_power_switch SW_DMA -domain PD_DMA \
-input_supply_port {in VDD} \
-output_supply_port {out VDD_DMA} \
-control_port {ctrl dma_pwr_en} \
-on_state {on_s in {ctrl}}
set_isolation ISO_DMA -domain PD_DMA -applies_to outputs \
-clamp_value 0 -isolation_signal dma_iso_en -isolation_sense high \
-isolation_supply SS_AON -location parent
set_retention RET_DMA -domain PD_DMA -elements {u_dma/cfg_q} \
-retention_supply SS_AON \
-save_signal {dma_save high} -restore_signal {dma_restore high}
add_power_state PD_DMA.primary \
-state {ON -supply_expr {power == FULL_ON && ground == FULL_ON} -simstate NORMAL} \
-state {OFF -supply_expr {power == OFF} -simstate CORRUPT}Read it top to bottom: two supplies, two domains (the top scope on the always-on supply, the DMA on its switched supply), a switch controlled by dma_pwr_en, an isolation strategy that clamps every DMA output to 0 while dma_iso_en is high, using cells placed in the parent and powered by the always-on supply, a retention strategy for one register, and the two power states of the DMA's supply. -simstate CORRUPT is what tells the simulator to corrupt the domain in the OFF state.
This course has no power-aware simulator, so the four-state model below writes out by hand the three things one would do from the UPF: corrupt the domain to X when the supply is off, clamp its output when isolation is on, and save and restore one register. It runs two power cycles: one correct, one with the isolation step missing. The always-on logic latches the interrupt with an OR, as a gate would.
// not-run: needs a four-state simulator (run with Icarus)
// What a power-aware simulator does to a switchable domain, written out by hand.
// In a real flow none of this is in the RTL: the simulator applies it from the UPF.
module pd_dma ( // the switchable domain
input logic clk, rst_n, pwr_on, save, restore,
input logic [7:0] cfg_in,
input logic cfg_we,
output logic [7:0] cfg_q, // retained register
output logic [3:0] cnt_q, // ordinary register
output logic irq_raw
);
logic [7:0] cfg_ret; // retention shadow, on the always-on supply
always_ff @(posedge clk or negedge rst_n or negedge pwr_on)
if (!pwr_on) begin cfg_q <= 'x; cnt_q <= 'x; end // supply off: corrupt
else if (!rst_n) begin cfg_q <= '0; cnt_q <= '0; end
else if (restore) cfg_q <= cfg_ret;
else begin
if (cfg_we) cfg_q <= cfg_in;
cnt_q <= cnt_q + 4'd1;
end
always_ff @(posedge clk) if (save && pwr_on) cfg_ret <= cfg_q;
assign irq_raw = pwr_on ? (cnt_q == 4'd15) : 1'bx; // outputs of an off domain are X
endmodule
module top;
logic clk = 0, rst_n = 0, pd_rst_n = 0, pwr_on = 1, save = 0, restore = 0, iso_en = 0, cfg_we = 0;
logic [7:0] cfg_in = 0, cfg_q;
logic [3:0] cnt_q;
logic irq_raw, irq_iso, irq_seen;
pd_dma u_dma (.rst_n(pd_rst_n), .*); // the domain has its own reset
assign irq_iso = iso_en ? 1'b0 : irq_raw; // isolation cell, clamp 0
always_ff @(posedge clk or negedge rst_n) // always-on logic
if (!rst_n) irq_seen <= 1'b0; else irq_seen <= irq_seen | irq_iso;
always #5 clk = ~clk;
task automatic show(string what);
$display("%-26s pwr_on=%b iso_en=%b cfg_q=%h cnt_q=%h irq_iso=%b irq_seen=%b",
what, pwr_on, iso_en, cfg_q, cnt_q, irq_iso, irq_seen);
endtask
task automatic power_cycle(bit use_iso);
@(negedge clk); if (use_iso) iso_en = 1; show("isolate");
@(negedge clk); save = 1; @(negedge clk); save = 0; show("save");
pwr_on = 0; @(negedge clk); show("power off");
repeat (2) @(negedge clk);
pwr_on = 1; pd_rst_n = 0; @(negedge clk); pd_rst_n = 1; show("power on, domain reset");
restore = 1; @(negedge clk); restore = 0; show("restore");
iso_en = 0; @(negedge clk); show("release isolation");
endtask
initial begin
repeat (2) @(negedge clk); rst_n = 1; pd_rst_n = 1;
cfg_in = 8'hC3; cfg_we = 1; @(negedge clk); cfg_we = 0;
show("configured");
$display("-- run 1: isolation and retention used correctly");
power_cycle(1);
$display("-- run 2: isolation missing");
power_cycle(0);
$finish;
end
endmoduleconfigured pwr_on=1 iso_en=0 cfg_q=c3 cnt_q=1 irq_iso=0 irq_seen=0
-- run 1: isolation and retention used correctly
isolate pwr_on=1 iso_en=1 cfg_q=c3 cnt_q=2 irq_iso=0 irq_seen=0
save pwr_on=1 iso_en=1 cfg_q=c3 cnt_q=4 irq_iso=0 irq_seen=0
power off pwr_on=0 iso_en=1 cfg_q=xx cnt_q=x irq_iso=0 irq_seen=0
power on, domain reset pwr_on=1 iso_en=1 cfg_q=00 cnt_q=0 irq_iso=0 irq_seen=0
restore pwr_on=1 iso_en=1 cfg_q=c3 cnt_q=0 irq_iso=0 irq_seen=0
release isolation pwr_on=1 iso_en=0 cfg_q=c3 cnt_q=1 irq_iso=0 irq_seen=0
-- run 2: isolation missing
isolate pwr_on=1 iso_en=0 cfg_q=c3 cnt_q=2 irq_iso=0 irq_seen=0
save pwr_on=1 iso_en=0 cfg_q=c3 cnt_q=4 irq_iso=0 irq_seen=0
power off pwr_on=0 iso_en=0 cfg_q=xx cnt_q=x irq_iso=x irq_seen=x
power on, domain reset pwr_on=1 iso_en=0 cfg_q=00 cnt_q=0 irq_iso=0 irq_seen=x
restore pwr_on=1 iso_en=0 cfg_q=c3 cnt_q=0 irq_iso=0 irq_seen=x
release isolation pwr_on=1 iso_en=0 cfg_q=c3 cnt_q=1 irq_iso=0 irq_seen=xIn run 1 the domain goes to X when off, the clamp holds irq_iso at 0, reset brings cnt_q back to 0, and restore brings cfg_q back to C3, the value saved before power off. In run 2 the X leaks through the missing clamp the moment the supply drops, and the always-on irq_seen flop captures it. Look at the last three lines: the DMA domain is back on and clean, but irq_seen stays X. An X that leaks into always-on state survives the power cycle that caused it, which is why missing isolation is a high-severity bug even when it seems to last only a few cycles.
A power-aware bench is the normal bench plus four pieces:
A power-control agent. If a power-management unit in the design drives the switch, isolation, save and restore signals, the agent is passive and monitors them. At block level, where nothing drives them, it is active and drives them from sequence items. Either way it publishes power-state transitions on an analysis port.
Power-state sequences. A virtual sequence that runs traffic, quiesces the block, runs the power-down steps in order, waits, powers up, and runs traffic again. Negative variants deliberately break the order (switch off before isolating, release isolation before restore) to prove the checks fire.
Checks. While a domain is off, every isolated output equals its clamp value. After restore, every retained register equals the value it had at save time (sample it at save through the monitor or a front-door read, compare after restore). After power-up, non-retained registers read their reset values. The scoreboard flushes the domain's expected traffic at power-off, exactly like the reset-aware scoreboard flushes on reset.
Coverage. Covergroups on the power-state transitions (ON to OFF, OFF to ON, and the illegal ones the design must refuse), crossed with what the block was doing when the request came (idle, mid-job, interrupt pending).
// snippet: a power cycle as a sequence on the power-control agent's sequencer
class dma_power_cycle_vseq extends uvm_sequence;
`uvm_object_utils(dma_power_cycle_vseq)
uvm_sequencer #(pwr_item) pwr_sqr; // set by the test before start()
rand int unsigned off_cycles;
constraint c_off { off_cycles inside {[4:64]}; }
function new(string name = "dma_power_cycle_vseq");
super.new(name);
endfunction
task body();
pwr_step(PWR_ISO, 1); // 1. clamp outputs
pwr_step(PWR_SAVE, 1); // 2. save retained state (pulse)
pwr_step(PWR_SWITCH, 0); // 3. supply off: the simulator corrupts the domain
pwr_step(PWR_IDLE, 0, off_cycles); // stay off
pwr_step(PWR_SWITCH, 1); // 4. supply on
pwr_step(PWR_RESET, 1); // 5. reset non-retained state (pulse)
pwr_step(PWR_RESTORE, 1); // 6. restore retained state (pulse)
pwr_step(PWR_ISO, 0); // 7. release the clamp
endtask
// one item per control step; the driver holds each item's level for n clocks
task pwr_step(pwr_ctl_e ctl, bit value, int unsigned n = 1);
pwr_item it = pwr_item::type_id::create("it");
start_item(it, -1, pwr_sqr);
if (!it.randomize() with { kind == ctl; level == value; cycles == n; })
`uvm_fatal("PWR", "randomize failed")
finish_item(it, -1);
endtask
endclassThe isolation check is the easiest to get wrong: compare the isolated output against its clamp while the domain is off, and sample the output on the always-on side of the isolation cell. A monitor sitting inside the switched domain sees X during off, which is correct and useless. In a real power-aware simulator the bench can also ask for supply states: IEEE 1801 defines a small SystemVerilog package for driving and querying supplies from a testbench, and each simulator adds its own power-aware debug commands. Check your tool's documentation for what it supports.
The first version of the model above had a single reset shared by the domain and the always-on logic, so the power-up step had no domain reset. The run looked fine until the clamp was released:
power on pwr_on=1 iso_en=1 cfg_q=xx cnt_q=x irq_iso=0 irq_seen=0
restore pwr_on=1 iso_en=1 cfg_q=c3 cnt_q=x irq_iso=0 irq_seen=0
release isolation pwr_on=1 iso_en=0 cfg_q=c3 cnt_q=x irq_iso=x irq_seen=0The retained register came back (C3), but the ordinary counter cnt_q stayed X after power returned, and releasing isolation let that X out on irq_iso. It is the third classic low-power bug, next to missing isolation and a missing restore: non-retained state used before it is reset. Notice also that irq_seen stayed 0. In that first version the always-on flop was written if (irq_iso) irq_seen <= 1, and the if turned the X into the else branch, the X-optimism from the previous lesson, hiding the leak in RTL. The fixes were a separate domain reset pulsed at power-up, and an always-on capture written so that an X shows up instead of disappearing.
Not necessarily. Outputs of an off domain are X by design; the question is whether they are isolated. Check the signal on the always-on side of the isolation cell: if it holds the clamp value, the design is fine and the scoreboard must stop expecting traffic (flush at power-off, as on reset). If the always-on side is also X, the isolation is missing or enabled too late.
Restore happened before the domain reset, so the reset overwrote the restored value (the order must be reset, then restore); or save never captured the value, for example because it was pulsed after the supply had already dropped, or the register is not in the retention strategy's element list.
Because the X does not stay on the wire. Any always-on flop that samples it (an interrupt latch, a status bit, a state machine) captures an unknown value and keeps it after power returns, as irq_seen did in run 2. On silicon that becomes a random value: a spurious interrupt or a stuck state that depends on the power-up.
Next: simulation is one way to get evidence. The Safety, Acceleration and Models topic covers what changes when the chip must be safe, when the simulation is too slow, and when the design includes analog.