Speed and position loops
Almost nobody wants a current. They want a speed, or a position. The current loop is the innermost of a cascade — three nested loops, each closing around the one inside it.
Each loop commands the one inside it and treats it as an ideal actuator. That assumption is what makes the design tractable, and it is only true if the inner loop is substantially faster than the outer one. Everything on this page follows from that.
Why cascade at all
A single controller mapping position error to voltage is possible and nobody builds one. Three reasons:
Limits live in the right place. A current limit is a physical constraint — it protects the bridge and the magnets. In a cascade it is simply the output clamp of the speed loop, expressed in the units it is specified in. In a monolithic controller there is no signal that is the current, so the limit has nowhere natural to go.
Each loop is commissioned separately. Tune the current loop and verify it. Then close the speed loop around a known-good actuator. Then position. A problem at any stage has one plausible cause. Tuning three interacting gains simultaneously against a machine that may be about to destroy itself is a different experience.
Disturbances are rejected where they enter. A load torque disturbs speed, and the speed loop sees it directly. The current loop rejects back-EMF variation without the speed loop ever knowing.
The speed loop
The plant from q-axis current to mechanical speed is first-order, exactly like the electrical plant:
For the reference machine N·m/A. The same pole-zero cancellation argument gives and .
Except that this time you should not use it.
On the bench The mechanical pole is tiny — a fraction of a hertz for almost any real machine, and 0.0004 Hz for the reference machine with a representative load. Cancelling it exactly gives an integral gain so small that the loop takes many seconds to reject a load step.
So the zero is placed deliberately instead, typically a factor of five below crossover, and the design stops being a cancellation. This is the usual state of affairs in practice: the tidy method from the current loop does not survive contact with a plant whose pole is at DC.
Separation is the whole design rule
The outer loop assumes the inner one is instantaneous. Measured on the real cascade — a 50 Hz speed loop around current loops of varying bandwidth, small step so nothing saturates:
| Current BW | Separation | Speed overshoot |
|---|---|---|
| 1500 Hz | 30× | 12.4% |
| 500 Hz | 10× | 12.4% |
| 250 Hz | 5× | 13.5% |
| 150 Hz | 3× | 16.5% |
| 100 Hz | 2× | 22.9% |
| 75 Hz | 1.5× | 29.8% |
Ten times and above costs nothing at all — the inner loop really is invisible. At five times you start paying. Below three the inner loop’s own dynamics are inside the outer loop’s crossover, and the overshoot more than doubles.
Five to ten times per stage is the rule, and this is where it comes from. With a 1 kHz current loop that allows perhaps 100–200 Hz of speed loop, and a position loop below 20 Hz.
The speed loop saturates on every startup
This is the difference between the speed loop and the current loop that catches people out.
The current loop saturates occasionally — on a large step, or near base speed. The speed loop saturates on every significant speed change, because the current limit is precisely what makes acceleration finite. Command 1500 rpm and the loop asks for infinite current, gets the limit, and stays there for the whole acceleration.
So anti-windup is not a refinement here. It is the difference between working and not:
| Overshoot | Settling | |
|---|---|---|
| With anti-windup | 6.9% | 140 ms |
| Without | 57.8% | 228 ms |
Fifty-eight percent overshoot on a speed command is not a tuning problem to be solved with lower gains — it is an integrator that spent 74 ms accumulating error it could do nothing about. The fix is the same back-calculation the current loop uses.
On the bench There is a second, subtler version of the same trap. When the inner loop saturates — the current loop hits its voltage limit — the speed loop’s output is no longer being delivered, but nothing tells the speed loop that. Its own output is not clamped, so its own anti-windup never fires, and it winds up regardless.
Robust cascades feed the inner loop’s saturation flag outward, so each loop stops integrating when the loop beneath it has run out of authority. This is what the bottom panel of the figure is showing: while “asked” and “delivered” coincide, the assumption holds; when they separate, the speed loop is regulating something that is not happening.
Feedforward costs nothing and is usually skipped
Feedback acts only on error, so a loop tracking a ramp always lags — that is what it means to be error-driven. If the acceleration is known in advance, which it is whenever a motion profile generated the command, the current it needs can be added directly:
This adds nothing to the loop transfer function, so it cannot destabilise anything. It simply means the feedback path has less work to do. It is the cheapest performance improvement available in a cascade and the most frequently left out.
The position loop wants proportional control only
With a working speed loop inside, the plant from speed command to position is a pure integrator. Proportional control alone therefore gives a first-order closed loop with no overshoot, and .
Integral action here is usually a mistake. The plant already integrates, so there is no steady-state position error for an integrator to remove — and adding a second integrator costs 90° of phase and invites exactly the sustained hunting that servo drives are notorious for.
On the bench The exception is a constant load torque — gravity on a vertical axis, say — which does produce a standing position error. Even then the better answer is usually to feed the load forward rather than integrate it away, because feedforward costs no phase margin.
Where speed comes from
The speed loop needs a speed, and encoders measure position. Differentiating a quantised signal is the problem it sounds like.
Count edges in a fixed window and the speed comes out quantised in steps of
which does not depend on speed at all. So as a fraction of the measurement it gets worse and worse as the machine slows:
| Encoder | Step (1 ms window) | 1% accuracy above |
|---|---|---|
| 500 CPR | 120 rpm | 12000 rpm |
| 1000 CPR | 60 rpm | 6000 rpm |
| 4000 CPR | 15 rpm | 1500 rpm |
A 1000-count encoder sampled every millisecond resolves speed to 60 rpm. At 100 rpm the estimate is useless, and no filtering recovers information that was never captured.
The standard answer is to switch methods: count edges at high speed, and time the interval between edges at low speed, where the roles of the quantised and continuous variables swap. Managing the transition between the two — and the discontinuity in noise characteristics at the changeover — is a real piece of engineering rather than a footnote.
On the bench Filtering the speed estimate is the obvious response and it is not free: any filter you add to the feedback path is inside the loop, and its phase lag comes straight off the speed loop’s margin. A drive that needs heavy speed filtering to be quiet has already given up most of its bandwidth. Buy encoder counts instead.
What to take away
- Cascade because it puts limits where they belong, lets you commission one loop at a time, and rejects disturbances where they enter.
- The speed plant is first order, but its pole is at DC. Place the controller zero deliberately rather than cancelling.
- Separate the bandwidths by five to ten times per stage. Ten costs nothing; below three the overshoot doubles.
- The speed loop saturates on every startup, so anti-windup is structural, not a refinement — 6.9% overshoot with, 57.8% without.
- Propagate the inner loop’s saturation outward, or the outer loop winds up while the inner one is limiting.
- Feed acceleration and load forward. It costs no stability and removes the lag feedback cannot.
- Position loops want P, not PI. The plant already integrates.
- Counting encoders quantise speed by a fixed step, so low speed is where they fail. Filtering does not recover it; counts do.