Per-unit and scaling
Two pieces of bookkeeping that every shipped drive does and most tutorials skip. They are separate ideas that end up used together, because they solve the two halves of the same problem: SI units make a controller hard to reason about, and they make it hard to compute.
Per-unit: divide everything by its rating
Pick three base quantities — a current, a voltage and a speed — and express everything as a fraction of them. Everything else follows:
For the reference machine, taking 40 A, the inverter’s linear voltage limit V, and 3000 rpm:
| Base | Value |
|---|---|
| Current | 40 A |
| Voltage | 27.71 V |
| Speed | 1257 rad/s electrical (3000 rpm) |
| Impedance | 0.693 Ω |
| Inductance | 551 µH |
| Flux | 0.0221 Wb |
| Torque | 5.29 N·m |
| Power | 1663 W |
And the machine itself becomes a set of dimensionless numbers:
Every parameter is now within an order of magnitude of 1. That is not cosmetic — it is what makes a wrong value look wrong. A per-unit resistance of 0.505 is plainly a normal machine; one of 8.0 is plainly not. In SI, 0.35 Ω tells you nothing until you know the machine.
On the bench The bases are a convention, not a fact, and a per-unit number is meaningless without knowing which base it used. Write them down next to the constants. The commonest per-unit bug is not arithmetic — it is two engineers using peak and RMS current bases and neither saying so.
The payoff: tune once, deploy twice
Take a second machine with four times the resistance and inductance and twice the flux, and give it bases with half the current and twice the voltage. Its per-unit description is identical — the site’s tests assert this to 1e-12.
So the same controller constants work on both. Physically different machines, one set of gains, because they are the same machine in per-unit. That is the whole argument for the extra bookkeeping, and it is why motor control libraries are written this way.
The time constants survive too:
which converts back to 600 µs — exactly in SI, as it must.
On the bench Choose the voltage base as the inverter’s linear limit rather than . Then a modulation index of 1.0 per-unit means exactly “the edge of the linear region”, and a voltage request greater than 1.0 is immediately recognisable as overmodulation without any further arithmetic.
Fixed point: representing those numbers
Per-unit gets every quantity near 1. Fixed point is how you store a number near 1 on a core with no FPU — as an integer, with an implied binary point.
Qn means n bits after the point. The two properties that matter:
for a b-bit word. Resolution and range trade directly against each other, and the choice is exactly where the fractional bits go.
Base current is the drive's rated peak phase current — 5 A a small industrial servo, 200 A an e-bike or forklift drive. Moving it changes which drive you are scaling for, not the machine on this page: all it sets is what one LSB is worth in amps.
Q15 has no headroom, and that is the trap
The obvious choice for a 16-bit word is Q15: all fifteen non-sign bits fractional. It gives excellent resolution — 1.22 mA on a 40 A base — and a range of exactly .
Note the bracket. 1.0 is not representable. Any per-unit quantity that can legitimately reach or exceed rated overflows:
- a current transient during a step,
- a voltage request in overmodulation,
- an integrator that has wound up,
- a torque demand at 1.0 pu.
The fix is to spend one bit on integer range. Q14 in a 16-bit word gives and 2.44 mA resolution — half the precision, twice the headroom, and it no longer explodes at rated. On a 32-bit core the question mostly evaporates: Q24 gives pu and 0.002 mA.
| Format | Range (pu) | Resolution at 40 A | SNR at 0.8 pu |
|---|---|---|---|
| Q15 in 16 bits | ±1.0 (exclusive) | 1.22 mA | 96 dB |
| Q14 in 16 bits | ±2.0 | 2.44 mA | 90 dB |
| Q24 in 32 bits | ±128 | 0.002 mA | 150 dB |
Saturate, never wrap
This is the one that destroys hardware.
Overflow a saturating format and a 1.2 pu current reads as 1.0 — wrong by 17%, but the right sign and the right order of magnitude. The controller sees too little current and pushes slightly harder. Recoverable.
Overflow a wrapping format and 1.2 pu reads as −0.8. The sign has flipped. The controller now believes the current is flowing the other way, and responds by driving it harder in the direction it was already going. That is not a degraded reading; it is a runaway, and it happens in one control period.
The figure measures the difference. A 1.3 pu signal on a 40 A base — 52 A, a plausible transient — stored in Q15:
| Overflow behaviour | Worst error |
|---|---|
| Saturating | 12 A |
| Wrapping | 80 A |
Nearly seven times worse, and pointing the wrong way. Tick Wrap on overflow and push the amplitude past the limit to watch the trace invert. Then set Integer bits to 1 — Q14, range ±2.0 — and the error drops to 1.2 mA, because the value fits.
On the bench
C’s built-in integer arithmetic wraps. Saturation is something you have to ask
for — either with saturating intrinsics (__SSAT on Cortex-M, or the DSP
extension’s saturating multiply-accumulates) or with an explicit clamp before
every store. Every place a per-unit quantity is narrowed is a place to check.
Scaling is not free precision
A format only delivers its rated resolution to a signal that fills its range. At a tenth of full scale you lose more than three bits — the SNR drops by 20 dB and the effective word is three bits shorter than the one you paid for.
The practical consequence: pick the current base close to the largest current you actually intend to measure, not to some distant absolute maximum. A base sized for a fault current you hope never to see spends most of its resolution representing numbers you never encounter.
Where the conversions belong
The pattern that works is to normalise at the edges and work in per-unit everywhere inside:
/* Edge: ADC counts -> per-unit, once, on the way in. */
const q15_t i_a = mul_q15(adc_a - offset_a, ADC_TO_PU);
/* Interior: everything is per-unit and dimensionless. */
foc_step(i_a, i_b);
/* Edge: per-unit -> compare register counts, once, on the way out. */
timer->CCR1 = mul_q15(duty_a, PWM_PERIOD_COUNTS);
Two conversions, both at the boundary, and no unit ever appears in the control maths. The alternative — converting back and forth inside the loop — is where scaling bugs live, because each conversion is a chance to use the wrong base and none of them are visible in the equations.
On the bench Do the per-unit design on paper before writing any code, and keep the base table in a comment beside the constants. Almost every scaling bug is really a bookkeeping failure: someone applied a base that was right for a different quantity, and the result was plausible enough to ship.
What to take away
- Choose three bases — current, voltage, speed — and every other base follows. Write them down.
- In per-unit, every machine parameter sits near 1, so wrong values look wrong.
- Two physically different machines with the same per-unit description take the same controller constants. That is the reason to bother.
- Base the voltage on so that 1.0 pu means “edge of the linear region”.
- Q15 in 16 bits cannot represent 1.0. Spend a bit on headroom, or use a 32-bit format where the question does not arise.
- Saturate on overflow. Wrapping flips the sign, and a sign-flipped current is a runaway rather than an error.
- A format only gives its rated resolution to a signal that fills it. Size the base to what you measure, not to what you fear.
Next: space vectors if you have not read it, or straight to Clarke and Park to start controlling something.