PID_VariableGainControl
PID_VariableGainControl closes a control loop with a proportional, integral and derivative term.11 minute read
PID_VariableGainControl closes a control loop with a proportional, integral
and derivative term. Its integral only accumulates while the error is large
enough and the machine is slow enough, and its derivative is filtered so sensor
noise is not amplified without bound. It is single channel: one instance
closes one loop.
The block is named for a second, variable-gain integrator intended to reject
low-frequency disturbance without the usual phase-lag penalty. That branch is
not exposed — no parameter enables it and it contributes nothing. For new
work use PID, which is the same controller with output limits,
stronger anti-windup and more conventional parameter naming.
Deprecated — scheduled for removal.
PIDis the PID to use for new work. With the variable-gain branch dead, this block isPIDwithout the anti-windup rework — no output saturation, no back-calculation, no directional conditional integration, no integrator deadband — and with the derivative filter spelledNdrather thannd. Its only consumer,FeedbackController, is deprecated with it. Nothing else in the library instantiates either. This block is kept only until the last machine is off it, and will then be deleted.
Swapping to PID is close to a rename. kp, ki, kd, iMin, iMax,
iPositionErrorThreshold* and iActualVelocityWindow* all carry over
unchanged, and the proportional, integral and derivative arithmetic is the
same. Two things to watch: the derivative filter leaf is nd there, not Nd;
and PID integrates the deadband residual of the error rather than the raw
error, so a non-zero controlErrorDeadBand changes the integral — it defaults
to 0, which reproduces this block exactly.
Do not try to revive the variable-gain branch as it stands. iTermVGC has
no clamp of any kind — iMax/iMin bound the ordinary integral term only — so
a one-sided error outside the deadband charges it without bound, and iReset
does not discharge it. Its only discharge path is a sign change of the error,
and that test is asymmetric about zero. The technique itself is sound and
published (Heertjes and van de Wouw 2007; Heertjes et al. 2019, 2020); the
implementation is not finished.
flowchart LR
i1(["error — reference minus actual"]) --> B["PID_VariableGainControl"]
i2(["actual — feeds the velocity gate only"]) --> B
i3(["iReset"]) --> B
B --> o1(["output — the control signal"])
B --> o2(["iTerm — integral contribution"])
B --> o3(["dTerm — derivative contribution"])
$u = k_p e + k_i !\int! e,dt + k_d \frac{de}{dt}$.
Ndis the derivative filter frequency in rad/s: the D action rolls off above it and its high-frequency gain is capped at $k_d \times$Nd. KeepNdbelow $1/$ task period [s] for a clean response — 1000 on a 1 ms task — and never above $2/$ task period, where it grows without bound.
The derivative filter is Nd here, with a capital N. PID
registers the same quantity as nd. Configuration files are not
interchangeable between the two blocks on this point.
This block has no enable, no disable and no output limit. iMax and iMin
default to ±0.1 and are the only protection against integral wind-up — set
them before you set ki.
Signals
Inputs
| Path | Unit | Range | Description |
|---|---|---|---|
error |
signal unit | unbounded | The control error: reference minus actual. A single value, not an array. This is the only input the control law acts on. |
actual |
signal unit | unbounded | The measured value. It feeds the velocity gate only and plays no part in computing the output. Leave it at 0 if you are not using the velocity window. |
iReset |
- | - | While true, the integral term is held at zero and stops accumulating. The block becomes a proportional-derivative controller for as long as you hold it. It is a level, not a pulse. It does not clear dTerm. |
Outputs
| Path | Unit | Description |
|---|---|---|
output |
output unit | The control signal — the sum of all terms, with no limit applied. Starts from zero after every controller start, but the integral state is not cleared by a stop. |
iTerm |
output unit | The integral term’s accumulated value, after this cycle’s update, so it and output reconcile sample for sample. Watch it to see wind-up as it happens. |
dTerm |
output unit | The filtered derivative term. Not cleared by iReset, and not cleared by a stop. |
Parameters
| Path | Unit | Default | Range | Effect |
|---|---|---|---|---|
kp |
output unit per signal unit | 0.0 | any | Proportional gain. Raising it makes the loop stiffer and faster, and eventually makes it oscillate. |
ki |
output unit per signal unit per second | 0.0 | any | Integral gain. It removes steady-state error. It does nothing useful until iMax and iMin are set wide enough. |
kd |
output unit per signal unit per second | 0.0 | any | Derivative gain. It damps overshoot and amplifies noise. Set Nd before you use it. |
Nd |
rad/s | 1.0 | above 0, below 1/task period [s] in practice | Derivative filter frequency, capital N. Higher gives a sharper, noisier D action. Not checked — see Limits and errors. At the default of 1.0 the D action is filtered so heavily that kd barely acts. |
iPositionErrorThresholdPos |
signal unit | 0.0 | any | The integral accumulates only while the error is above this or below the negative threshold. At the default of 0 it accumulates whenever the error is not exactly zero. |
iPositionErrorThresholdNeg |
signal unit | 0.0 | any | The lower half of the same gate. |
iActualVelocityWindowPos |
signal unit per second | +infinity | any | The integral accumulates only while the rate of change of actual is inside this window. The default admits everything. |
iActualVelocityWindowNeg |
signal unit per second | −infinity | any | The lower half of the same window. Writing 0 to both closes the gate permanently and the integral never accumulates. |
iMax |
output unit | 0.1 | above iMin |
Upper bound on the integral term, and the block’s only wind-up protection. Raise it before tuning ki. |
iMin |
output unit | −0.1 | below iMax |
Lower bound. Setting it above iMax pins the integral at iMin — see Limits and errors. |
All ten parameters are persistent and survive a controller restart. The inputs and outputs do not. No parameters exist below this block.
Setup
-
Set
iMaxandiMinto the largest integral contribution you will accept. Do this first. There is no output limit in this block, so these two are all that stands between a wound integrator and the actuator.Step 1 is the only wind-up protection you get. Unlike
PID, this block has no output clamp and no back-calculation. If a wound integrator reaching the actuator would be unsafe, usePIDinstead. -
Set
kp,kiandkdto 0, andNdto something sensible for your task rate — 50 to 200 rad/s is a normal starting range on a 1 ms task. -
Link
errorfrom your comparison. Linkactualonly if you intend to use the velocity window; otherwise leave it at 0. -
Raise
kpfrom 0 until the loop responds usefully.outputshould trackerror×kpexactly whilekiandkdare still 0. -
Raise
kifrom 0 until steady-state error disappears. WatchiTerm— if it sits pinned atiMaxoriMin, go back to step 1. -
Raise
kdfrom 0 only if you need to damp overshoot. WatchdTermon a trace; if it is noisy, lowerNd. -
Bound
outputin a limiter downstream. This block will not do it for you.
Tuning
- Set
iMinandiMaxbefore any gain, per Setup step 1. - Tune
kpalone. Raise it until the response is fast enough, then back off until any oscillation is gone. Leavekiandkdat 0 throughout. - Add
kinext. Raise it until steady-state error is removed within an acceptable time. Too much shows as a slow oscillation the proportional gain cannot explain. - Add
kdlast, and only for overshoot. ReaddTermon a trace before you trust it — if the trace is noise,kdis amplifying your sensor and you should lowerNdor leavekdat 0. - Set
Ndagainst your noise, not against your response. Start at 100 rad/s on a 1 ms task and lower it untildTermis smooth. - Use the error thresholds and the velocity window only when the integral must be suppressed during motion. Leave them at their defaults otherwise.
- Re-check
Ndafter any task-rate change. Its usable range scales with the task period.
Read where each curve flattens as the point its integral term hit iMax.
| Symptom | Cause | Action |
|---|---|---|
| Steady-state error never goes away | ki is 0, or iTerm is pinned at iMax or iMin |
Raise ki; if iTerm is pinned, raise iMax and lower iMin first |
Raising ki seems to do nothing |
Expected with the ±0.1 default limits: the integral saturates almost immediately | Set iMax and iMin to real values for your output unit |
output went far beyond what the actuator can take |
Expected: this block has no output limit | Add a limiter downstream, or use PID, which clamps its own output |
| The loop stays saturated long after the error reversed | Wind-up, and this block has no anti-windup beyond the integral clamp | Tighten iMax and iMin, or move to PID |
| The loop overshoots and rings | kp too high, or ki too high |
Lower kp first; if the oscillation is slow, lower ki |
| The output buzzes or is audibly rough | kd amplifying sensor noise |
Lower Nd, or set kd to 0 |
dTerm is pure noise on a trace |
Nd too high for the noise on error |
Lower Nd |
dTerm froze at a value and never moves |
Nd is 0 |
Set Nd above 0 |
dTerm oscillates cycle to cycle |
Nd above 1/task period |
Lower Nd below 1000 on a 1 ms task |
output or dTerm grew without bound |
Nd above 2/task period, or negative |
Set Nd within range, then restart the controller |
A configuration copied from a PID block did not apply the derivative filter |
Expected: this block spells it Nd, PID spells it nd |
Write Nd explicitly; the two are not interchangeable |
iTerm sits at iMin no matter what the error does |
iMin was set above iMax |
Keep iMin below iMax |
A large kick on output on the first cycle after a start |
Expected with a non-zero kd: the derivative sees the initial error as a step |
Ramp the reference in, or start with the error near zero |
A full-scale output immediately after a restart |
Expected: the integral state is not cleared by a stop | Pulse iReset before releasing the loop |
| The integral never accumulates at all | The velocity window is closed — writing 0 to both ends admits nothing | Set iActualVelocityWindowPos to +infinity and the negative one to −infinity |
| The integral stops accumulating whenever the machine moves | Expected if the velocity window is set: that is what it is for | Widen the window, or leave it at its defaults |
| You cannot find the variable-gain parameters | Expected: that branch is not exposed and contributes nothing | Use the ordinary ki integral, or PID |
output and iTerm went to a non-numeric value and iReset did not clear it |
A non-numeric value reached the derivative path, which iReset does not clear |
Restart the controller, then fix the source |
| You need to close several loops | Not possible — this block is single channel | Use one instance per loop |
A conservative starting point for a loop on a 1 ms task, with the integral bounds in the actuator’s own units:
iMin = -5.0
iMax = 5.0
Nd = 100.0
kp = 1.0
ki = 0.0
kd = 0.0
Work steps 2 to 4 of Tuning from there. This is a starting point, not a final tuning.
Limits and errors
| Limit | Set by | What happens | Reported |
|---|---|---|---|
output |
Nothing | Unbounded. This block has no output limit, no back-calculation and no directional anti-windup. Bound it in a limiter downstream, or use PID |
Not reported |
| Integral term | iMax, iMin |
Clamped to the range. This is the block’s only wind-up protection | Not reported; watch iTerm |
iMin < iMax |
Nothing | Not checked. With iMin above iMax the integral is pinned at iMin and the gain has no effect |
Not reported |
Nd within 0 and 2/task period [s] |
Nothing | Not checked. Above 1/task period the derivative oscillates, above 2/task period it grows without bound, at 0 it freezes, below 0 it diverges at once | Not reported |
| Variable-gain integrator | Not exposed | The branch named in the block’s title has no parameters and no way to enable it. It contributes nothing to output |
Not reported |
| Integral state across a stop | Nothing | Not cleared by a stop or a start. A loop that was wound when it stopped resumes wound. Pulse iReset before releasing the loop |
Not reported |
| Derivative state | Nothing | Not cleared by iReset, a stop, or a start. Only a controller restart clears it |
Not reported |
| Channel count | Fixed | One loop per instance, always | Not reported |
The block raises no errors or warnings and logs nothing. Every failure above shows as a value on a trace, not as a message.
Verified against motorcortex-control3 3.30.0 (bc348fd).