Skip to main content

Hierarchical UPF

Every example so far described one flat set of domains in one file. A real chip is built from blocks written by different teams, sometimes different companies, integrated into a hierarchy — and UPF has a real mechanism for composing power intent the same way RTL itself gets composed.

load_upf: including subsidiary files at a scope​

# top.upf
set_scope u_pwm_block
load_upf pwm_block.upf

set_scope u_uart_block
load_upf uart_block.upf

A top-level UPF file doesn't have to describe every domain in the entire chip directly — load_upf, run after set_scope positions the load at the right point in the hierarchy, pulls in a block's own UPF file exactly where that block sits. Each block's power intent is written once, by whoever owns that block, and composed into the full chip's power architecture without the top-level author needing to know that block's internal domain structure at all.

Why supply set handles matter here specifically​

Power Domains and Supplies introduced supply set handles — primary, default_isolation, and the rest — as an indirection layer, without fully explaining why the indirection exists. Here's the payoff directly: a block's UPF, written before it's ever integrated anywhere, can declare "my primary supply" as a handle without knowing what real net will eventually feed it. The block author never has to specify physical nets or ports at all — that gets refined later, at integration time, when the top-level UPF actually connects the block's abstract handle to a concrete supply. This is successive refinement, the design philosophy UPF 2.0 introduced back in 2009 (per Introduction's version history): power intent that starts abstract and gets progressively filled in with physical detail, rather than needing every detail specified up front.

UPF 4.0's Refinable Macros — also from that same version history — formalize this further, letting an IP provider ship "soft IP" with power intent the end user explicitly refines, rather than relying on supply-set indirection alone to carry that flexibility.

A shortcut: load_upf's own -scope argument​

The set_scope-then-load_upf pair above is the general pattern, but load_upf also accepts its own -scope argument, positioning the load inline without a preceding set_scope call at all:

# top.upf — three identical PWM macro instances
load_upf pwm_macro.upf -scope u_soc/u_pwm0
load_upf pwm_macro.upf -scope u_soc/u_pwm1
load_upf pwm_macro.upf -scope u_soc/u_pwm2

This matters specifically when the same macro is instantiated more than once — three identical PWM blocks here, each needing pwm_macro.upf's domains created fresh at its own hierarchical path. set_scope changes the current scope for every command that follows until it's changed again, which would leave stray state to reset between each load; load_upf -scope is self-contained per call — one line positions and loads a single instance, with no shared scope state to manage across the three calls. The source UPF file itself (pwm_macro.upf) is written once and never duplicated per instance, the same reuse payoff the caution box below extends to legal power states.

Two directions to build the hierarchy in​

load_upf says how a block's file gets pulled in, but not which direction the overall hierarchy gets built from. Bottom-up starts with each block's own UPF written and verified standalone, then merges those already-verified files upward into the top-level file — each subsystem can be linted for power intent independently, without needing a full-chip UPF to check against yet. Top-down goes the other way: the full-chip power intent is written and verified first, and any block-level UPF derived from it inherits that already-verified status — block-level intent is only as trustworthy as the known-good full-chip description it was derived from. Neither direction is UPF syntax; it's a methodology choice about verification order layered on top of the same load_upf/set_scope mechanics either way.

connect_supply_net: actually wiring a block's supply ports in​

The integration checklist below asks what a block's supply ports are — connect_supply_net is the command that actually answers that question in UPF, wiring a named supply net at the current scope to a port or pin that needs it:

# top.upf, after loading pwm_block.upf at u_pwm_block
connect_supply_net VDD_top -ports u_pwm_block/VDD
connect_supply_net VSS_top -ports u_pwm_block/VSS

This is the concrete step that closes the loop successive refinement opened: pwm_block.upf declared its own supply ports (or handles) without knowing what real net would eventually feed them; connect_supply_net, written at the top level once the block is actually integrated, is where that abstract port finally gets tied to a specific, real supply net in the parent scope. A block supply port left unconnected — no connect_supply_net call ever targeting it — is exactly the silent, no-error-until-later gap the integration checklist below warns about: the port exists in the block's own UPF, but nothing in the full-chip power network actually feeds it.

Scope visibility only ever looks downward​

set_scope/load_upf -scope position where commands apply, but they also impose a real constraint worth stating explicitly: UPF commands can only reference netlist objects at the current scope and below it — never anything above. A block's own UPF, written and verified standalone before it's ever integrated, has no way to name a signal, domain, or supply that lives above its own root — which is exactly why connect_supply_net above has to be written at the parent's scope, not the block's own, to wire an already-loaded block's port to something outside it. This is the same one-way, block-can't-see-its-integrator relationship that makes standalone bottom-up verification of a block possible in the first place: nothing the block author writes can accidentally depend on integration-time context that doesn't exist yet.

A real integration checklist​

Before loading a block's UPF into a larger design, two questions have real, concrete answers worth checking rather than assuming: what are this block's supply ports, and what are its legal power states. Any supply the block itself doesn't generate internally has to be brought in from the parent scope explicitly — miss one, and the integrated power network is silently incomplete, with no error until something downstream depends on a connection that was never actually made. The block's own power state table (per Power States) is the authoritative source for both questions — reading it, rather than assuming from the block's RTL interface alone, is what a real integration pass actually does.

What's next​

Every construct across all three sections of this topic now exists — domains, supplies, states, all four strategies, sequencing, and hierarchy. The capstone applies all of it to a single real example: writing complete UPF for the PWM register block already built in UVM's RAL section.