A power design is a KDL v2 document. powertree schema prints the full list of nodes and properties generated from the validator, so it is always current; this document explains the meaning.
All electrical values are quoted strings with units: "3.3V", "250mA", "45mΩ" (also ohm, R), "1.2W". SI prefixes p n u µ m k M G are accepted. A KDL type annotation is equivalent: imax=(mA)250. Bare numbers are rejected for quantities, so a missing unit is always an error.
| Form | Example | Meaning |
|---|---|---|
| voltage spec | "5V", "5V ±2%", "5V +-50mV", "4.75V..5.25V" |
nominal and min/max (range form: nominal = midpoint) |
| voltage range | "3V..5.5V" |
accepted input range, adjustable setpoint range |
| ratio | 0.9, "90%" |
efficiency, rule thresholds |
| boolean | #true, #false |
KDL v2 keywords |
design <name> [desc=] // exactly one per design file
use "<path>" | "<lib>:<path>" // load parts and designs; path relative to this file or a library root
net <NAME> [desc=] [count=] // named bus; every net= must refer to a declared net
port <PORT> net=<NET> [dir=in|out|bidir] // this design's interface when used as a board
part <PART> { functions... } // reusable definition (any file)
chip <REF> [part=] [desc=] [count=] { functions... }
board <REF> design=<name> [count=] [desc=] { port <PORT> net=<NET> ... }
scenario <name> [base="a, b"] [desc=] { loads nom|min|max; set <target> prop=value... }
rules { converter-load max=; provider-load max=; switch-load max=; input-headroom min= }
waive <code> <target> reason="..."References (CHIP, BOARD) may not contain ., :, { or }; those characters are reserved for paths and templates.
A chip groups all power functions of one physical component. A PMIC is one chip with several converter functions and possibly consumer functions for its own supply pins. Function names are optional when a chip has one function of that kind; otherwise name them (consumer vdd, consumer vdda). Functions are addressed as CHIP.function, or just CHIP if the chip has a single function.
| Function | Ports | Parameters |
|---|---|---|
provider |
out (v, imax, share) |
r internal resistance |
converter kind=... |
in (range), out (v, range, imax, share) |
iq, eff, dropout (linear) |
switch [enable=SIG] |
in (range), out (imax) |
rdson |
series |
in, out (imax) |
r — fuse, ferrite, sense resistor, connector, cable |
oring |
in × N (priority), out (imax) |
vf, r |
consumer |
in (v, range) |
load |
Converter kinds: buck boost buck-boost flyback isolated charge-pump charger linear. Kind linear uses Iin = Iout + Iq and ignores efficiency. Every other kind requires eff. The solver sanity-checks buck (vin > vout) and boost (vin < vout).
Implicit ports. A function with exactly one input or output gets that port implicitly when it is not written. So series { in net=VIN; r "10mΩ" } still has an output, which another input can link to with from=RSENSE.series.out without naming the net between them. OR-ing inputs are never implicit. An implicit port that nothing links to is reported by the topology rules (unconnected-input error, unconnected-output warning).
on=#false on any function turns it off. This is usually set per scenario.
range="a..b" on an input, optionally overridden by vmin=/vmax=. On a consumer, v="3.3V ±5%" also defines the accepted range. A v= with no tolerance and no range= is documentation only and is not checked.
vlim= on a converter output gives the highest voltage the converter can produce, as an expression of vin and iout. Use it for datasheet limits such as "Output Voltage: 0.8V to 0.85 × VIN", a maximum duty cycle, or a pass-element drop:
out net=V12 v="12V ±2%" vlim="0.85*vin"
out net=V5 v="5V" vlim="vin - 0.2Ω*iout"The output is the lower of the setpoint and the limit, both for the nominal value and for the min/max range. It is usually a part property, so it belongs in the library definition.
Two findings report the limit:
output-limited(error): the converter cannot reach its setpoint at the nominal input.output-limit-margin(warning): it regulates at nominal input but not at the minimum input.
The result must be a voltage, so every constant needs a unit: max(0.8V, 0.85*vin), not max(0.8, 0.85*vin).
eff 0.9 // constant
eff "92%"
eff expr="0.94 - 0.03 * (iout / 750mA)" // variables vin, vout, iout
eff vin="12V" vout="5V" { // datasheet curves; several allowed
point iout="10mA" eff=0.62
point iout="1A" eff=0.91
}Expressions are restricted and unit-aware. They allow + - * / **, unit literals, and the functions min max abs sqrt exp log log10 clamp. The result must be dimensionless. t and temp are reserved for future time and temperature support.
Tables are interpolated linearly in log(iout), and linearly in vin between curves. The curve set with the nearest vout is used. Outside the data the value is clamped and a finding is reported: info below the lowest current, warning above the highest current or more than 10% outside the vin/vout covered.
Input power is P_in = P_out / η + V_in · I_q.
load i="35mA" imin="15uA" imax="90mA" // constant current
load p="50mW" pmin="1mW" pmax="80mW" // constant power (current rises as voltage falls)
load r="100Ω" // resistive
load i="100mA" offboard=#true // power leaves the board: not counted as heatA missing min or max falls back to the nominal value.
Several load lines, and lines that scale with enabled elements. A consumer may have any number of load lines; its draw is their sum, and each line has its own model, so a static current can sit beside a per-lane power. A line with per= is scaled by how many of those elements are enabled:
consumer vddpll {
in net=V1P8 range="1.71V..1.89V"
load i="1.2mA" imax="2mA" // static, always drawn
load i="4mA" imax="7mA" per=PLL active=2 of=4 // per enabled PLL
load p="10mW" pmax="15mW" per=lane of=8 // per enabled lane (active defaults to 1)
}How a scaled line works:
- Draw. A line draws
active ×its value at the scenario's load level:active × i,active × p, oractiveresistors ofrin parallel. - Bounds.
activedefaults to 1 and may be 0.of=is how many elements the chip has; a largeractiveis anactive-rangeerror. - Naming.
per=is required whenactive=orof=is given, must be unique within the consumer, and cannot bei,p,roron. - Shared settings. All lines must agree on
offboard.
Labels. desc= names any load line, scaled or not, e.g. load i="32mA" imax="36mA" desc="TX".
Output. By default only the summed draw is shown, and the report's State column and the graph show the counts as 2/4 PLL, 1/8 lane.
With --load-lines (on report and dot), each line is listed with its current and power:
- Report: a Load line table is added.
- Graph: a consumer with several lines gets a small table in its box (load, current, power; "each" for stacks). A consumer with a single line shows no numbers for it, since they equal the summary. It shows only a label that adds something: a scaled line's name and count (
SPE 3/6), orscenario override. - Line names: a line is named by its
desc, else itspername, elseload N. With both, the name isdesc (per). A consumer whose only line has no name gets a blank name. - JSON: the per-line values are always included.
These are counts of elements within one consumer on one net, unlike count=, which creates separate chips.
in net=NAME connects a port to a named net.
in from=CHIP[.function[.out]] links directly to an output:
- If that output is on a named net, the input joins that net.
- Otherwise an anonymous net
~CHIP.function.outis created. - Giving both
net=andfrom=is allowed only if they agree.
chip U1 part=TPS54331 { converter buck { in net=A; out net=B v="5V" } } merges the instance onto the part definition with the same function name:
- Instance properties override library properties.
in/outports merge by position.- Any other child (
eff,iq, …) given on the instance replaces the library's.
If part= names no loaded part, it is metadata only, and the chip body must be complete. Defining the same part name twice is an error.
count=N on a chip, board or net creates N instances. Chips and boards are named REF:1 … REF:N. Inside a counted node, {n} in net=, from= and desc= is replaced by the instance number. A counted net needs {n} in its name. Values containing {n} must be quoted, because braces are not allowed in bare KDL strings:
net "IO{n}_24V" count=8
chip F count=8 desc="Slot {n} fuse" { series { in net=BUS; out net="IO{n}_24V"; r "40mΩ" } }
chip D count=12 { consumer led { in net=V5; load i="5mA" } } // D:1 .. D:12, all on V5Any design file can be instantiated as a board. It declares its interface with port nodes, each naming one of its own nets:
// io-card.kdl
design io-card
port VIN net=VIN_24V dir=in
net VIN_24V
...
scenario run
scenario idle { loads min; set K:* on=#false }The parent uses the file and instantiates it, binding every port to one of its own nets. An unbound port is an error.
use "io-card.kdl"
board IO count=8 design=io-card { port VIN net="IO{n}_24V" }How a board is flattened into the parent:
- Chips are prefixed with the instance path, so U3 on the second card becomes
IO:2.U3and its function becomesIO:2.U3.buck. - Internal nets are prefixed too (
IO:2.V5), except port nets, which become the parent's net. - Boards nest to any depth. A design that instantiates itself, directly or indirectly, is an error.
- The board's
waivenodes come along, prefixed with the instance path. - The board's
rulesare ignored; the top-level design's rules apply. - The board's scenarios are available to the parent through
set <board> scenario=<name>.
With no source on its input ports, a board analysed on its own reports the port nets as undriven. A standalone block fixes this. Its contents are used only when the file is the top-level design, and are dropped entirely when the file is instantiated as a board, so they never add a second driver to the parent's net:
standalone {
chip TP1 desc="Bench supply on VIN" { provider { out net=VIN_24V v="24V ±3%" imax="1A" } }
scenario low-line { set TP1 v="21.6V ±3%" }
}A standalone block may contain net, chip, board, scenario, rules and waive. Scenarios that refer to standalone chips belong inside it; otherwise they would be unresolved references when the file is used as a board. The report lists what the block added (info standalone).
dir= is informational in this version.
A path is dot-separated: BOARD.CHIP.function, for example SHELF.IO:3.U10.vdd. A segment of a path can select several instances:
| Selector | Matches |
|---|---|
REF:3 |
instance 3 |
REF:* |
all instances |
REF:2..5 |
instances 2 to 5 |
Selectors work in set and waive targets, e.g. set IO:*.D:* on=#false or waive overload IO:1..4.U3 reason=....
Named roots let design files refer to shared libraries without machine-specific paths. They are set in powertree-project.kdl, found by searching upward from the design file:
library corp path="../corp-power-lib" // relative to the project fileEach later source overrides earlier ones:
- the project file
$POWERTREE_LIBS(name=pathentries separated by:on Linux/macOS,;on Windows)--lib name=pathon the command line
Using a root:
use "corp:regulators.kdl"loads a file from the root.- Relative
usepaths inside a library file stay in that library. - Parts and designs are namespaced by library.
part=corp:TPS54331is always exact. - An unqualified
part=TPS54331must be unique across all loaded files; otherwise it is anambiguous-parterror. The same rule applies todesign=.
Traceability: every report records each root used, with its path, its origin, a SHA-256 content hash of the files actually loaded, and the git commit (marked +dirty if there are uncommitted changes).
scenario active
scenario peak base=active { loads max }
scenario night base="active, low-power" {
set U4.sw on=#false
set U10.vdd i="15uA"
set J1 v="10.8V"
}Resolution order: defaults → each base in listed order, recursively → the scenario's own loads and set lines. Later settings win.
Consumer load models: setting one of i/p/r replaces any load model set earlier.
What set can change:
- any function:
on - consumer:
i,p,r, which replace all of the consumer's load lines with that one load; or<per name>=N, which sets the enabled count of a scaled line, e.g.set U5.vddpll PLL=4 lane=0 - provider:
v,imax - converter:
v - chip with several functions:
ononly, applied to all its functions - board:
scenario=<name>, which applies the board's own scenario scoped to that board, andon, which turns off everything on the board
Load level and counts are independent. loads min|nom|max picks each line's per-element value; it never changes active. Within one scenario the order of loads and set lines does not matter for that reason. The order does matter only when two set lines change the same property, where the later one wins.
Load levels are scoped. A board scenario's loads applies only inside that board. Otherwise loads lines apply in order, and the last one covering a function wins. So in scenario s { loads max; set IO:2 scenario=idle }, board IO:2 runs at its idle level and everything else at max.
If no scenario is defined, a single implicit nominal scenario runs.
Composing on the command line. -s a+b runs one scenario made of a then b, as if you had written scenario a+b base="a, b". Repeating -s runs separate scenarios: -s peak -s low-line produces two reports. A scenario actually named a+b takes precedence over composition.
| Rule | Default | Finding |
|---|---|---|
converter-load max= |
80% | warning when Iout/imax exceeds it; above 100% is always an overload error |
provider-load max= |
80% | same, for providers |
switch-load max= |
80% | same, for switch, series and oring |
input-headroom min= |
0 (off) | warning when the supplied range is within this fraction of an input's accepted limits |
waive <code> <target> reason= accepts matching findings. The target may be a board, chip, function or net, or a selector; it covers everything beneath it. Waived findings are still listed, marked as waived. A waiver that matches nothing produces an unused-waiver warning.
Each layer runs only if the previous layers produced no errors, and reports all errors in its own layer.
- Syntax:
syntax - Structure:
unknown-node,unknown-property,bad-value,argument-count,missing-property,unexpected-children,duplicate-property - Model and references:
- Loading:
file-not-found,library,duplicate-part,duplicate-design,ambiguous-part,ambiguous-design,unknown-design,kind-mismatch, and the warningunknown-partfor a near-miss part name - Hierarchy:
bad-name,template,unknown-port,unbound-port,duplicate-port,duplicate-board,board-cycle, and infoignored-rules,standalone - Function definitions:
port-count,missing-voltage,missing-efficiency,missing-load,load-model,active-range,efficiency,setpoint-range - Duplicates:
duplicate-net,duplicate-chip,duplicate-function,duplicate-scenario - References:
undefined-net,bad-reference,conflicting-link,undefined-scenario,scenario-cycle
- Loading:
- Topology:
unconnected-input,undriven-net,multiple-drivers,cycle, and warningsunused-net,unloaded-net,unconnected-output - Electrical, per scenario:
- Loading:
overload,converter-load,provider-load,switch-load - Input voltage:
input-range,input-headroom - Regulation:
dropout,dropout-margin,output-limited,output-limit-margin,converter-kind,eff-extrapolated,efficiency,vlim - Solver:
collapse,no-convergence - State:
unpowered(info)
- Loading:
The solver computes a steady-state DC solution per scenario:
- Forward pass in topological order. This computes each net's nominal voltage and its min/max range:
- Providers use their spec minus I·r.
- Switching converters use their setpoint, capped by
vlimwhen given. - Linear regulators use min(setpoint, V_in − dropout(I)), with dropout scaled linearly from
at=. - Switches and series elements subtract I·R.
- OR-ing picks the lowest
priorityamong powered inputs, otherwise the highest voltage, then subtracts vf + I·r.
- Backward pass in reverse order. This computes currents from loads up to sources. A net with several
share=#truedrivers splits current equally. Otherwise, the highest-voltage driver supplies the net. - Iterate both passes until voltages and currents stop changing. This handles constant-power loads behind resistive drops.
Currents are nominal. Voltage ranges are propagated using those currents.
| Row | Definition |
|---|---|
| Source output | Σ provider output power: V_out · I_out, after the provider's internal resistance |
| Converter / Switch / Series / OR-ing loss | Σ (P_in − P_out) over functions of that kind |
| On-board consumers | Σ P_in of consumers without offboard |
| External consumers | Σ P_in of consumers with offboard=#true |
| Source internal loss | Σ I²·r inside providers; heat, but not part of source output |
| Heat on board | all losses + on-board consumers + source internal loss |
| System efficiency | (on-board + external consumers) / source output |
The rows from Converter loss through External consumers sum to Source output.
Heat per chip is the sum of its functions' heat:
- consumer: input power, unless
offboard - converter, switch, series, oring: P_in − P_out
- provider: I²·r
powertree dot draws one scenario. Chips are drawn as clusters, boards as dashed clusters, and nets as ellipses. Edges carry currents, and findings colour the affected nodes: red for errors, orange for warnings.
Stacking. Repeated instances are drawn once, as a stack. Instances of the same count= group stack when the scenario configures them the same way: same on/off state, same set overrides, same load level and same board scenario. The partition is coherent across the graph: if card IO:1 runs a different scenario from IO:2..4, its fuse, harness net and contents split off with it. Computed values do not decide stacking, so the graph keeps the same shape across scenarios unless you configure instances differently.
A stacked node shows:
- per-instance values, marked "each", as a range when they differ by more than 1%
- the total over every instance it represents (
total ×24: …) - how many instances are off or unpowered
- which instances have errors
Stacked functions are drawn as 3-D boxes, stacked nets with a double outline, and stacked chip and board clusters with a bold border. Every label carries ×N.
Edge labels. A net's voltage and current are in its bubble, so an edge only carries a current label where the bubble can't tell you: on the load edges of a net with several loads, and on the source edges of a net with several sources. A one-source, one-load net has unlabelled edges. Edges that stand for several stacked instances are labelled per-instance ×count = total. Edges carrying no current are dashed.
Unnamed nets (created by from=) are not drawn. The source connects straight to its load(s), and the net's voltage and current can be read from those two nodes.
| Option | Effect |
|---|---|
--expand SEL |
draw matching instances separately, e.g. --expand IO:2; repeatable, selectors allowed |
--no-stack |
draw every instance |
--collapse-boards |
draw each top-level board (or stack of boards) as one box with its ports, heat and input power |
--wrap N |
fold linear chains longer than N functions into rows of N (default 0: off) |
--rankdir LR|TB |
flow left-to-right (default) or top-to-bottom |
Chain folding. A chain is a run of function → net → function links where each net has one source and one load, and each function in the chain has one input and one output, e.g. fuse → sense resistor → reverse-protection FET → eFuse → buck. With --wrap N, each run of N functions becomes a row. The next row starts level with the first function of the chain and is placed beside the previous row, so the chain reads like lines of text instead of one long line.
In a folded chain, each chip is drawn as a single node that looks like the chip box with its function inside, so the rows can be lined up column by column. The edge from the end of one row to the start of the next runs through the gap between the rows.
Graphviz decides the final vertical order itself. So when Graphviz is available, powertree dot lays out a few equivalent variants and keeps the first whose rows come out in reading order and next to each other. If none does, it keeps rows together and turns the row-to-row edge round so it still runs through the gap. Without Graphviz (writing .dot only), the first variant is written unchecked.
A chain ends at:
- a branch (a net with several sources or loads, or a function with several inputs or outputs)
- a chip that also holds other functions, because clusters must nest
- a board boundary
Fan-outs are laid out as before.
- KiCad netlist mode
offboardfor series elements, so cable losses are not counted as board heat- worst-case current solve
- battery state-of-charge and runtime
- time-based behavior