PID

PID closes a control loop: it takes an error and produces a control output from a proportional, an integral and a derivative term.
Current release

PID closes a control loop: it takes an error and produces a control output from a proportional, an integral and a derivative term. Use it wherever a loop is closed outside the actuator control loop — a pressure loop, a temperature loop, a force loop.

Three things distinguish it from a textbook PID. Its integral only accumulates while the error is large enough and the machine is slow enough. It has three independent protections against integral wind-up. And its derivative is filtered, so sensor noise is not amplified without bound. It is single channel: one instance closes one loop.

flowchart LR
    i1(["error — reference minus actual"]) --> B["PID"]
    i2(["actual — feeds the velocity gate only"]) --> B
    i3(["iReset, integratorFreeze, controllerReset"]) --> B
    i4(["disable"]) --> B
    B --> o1(["output — the control signal"])
    B --> o2(["iTerm — integral contribution"])
    B --> o3(["dTerm — derivative contribution"])
    B --> o4(["isEnabled"])

$u = k_p e + k_i !\int! e,dt + k_d \frac{de}{dt}$. nd is the derivative filter frequency in rad/s: the D action rolls off above it, and its gain at high frequency is capped at $k_d \times$ nd. Keep nd below $2/$ task period [s] — 2000 on a 1 ms task — and above 0. The integral term reaches output one cycle after it is accumulated.

enable defaults to true, so the block runs unless you switch it off — the default is deliberate, so that adding the parameter did not silently mute every existing PID. While it is off, or the disable input is on, the output is zero and the whole state is held cleared, so re-enabling starts from rest rather than dumping a stale integral into the plant.

iMax and iMin default to ±infinity, so there is no integrator clamp unless you configure one. Wind-up is held off by the output saturation, the directional conditional integration and backCalculationGain; the clamp is a hard backstop on top of those. This changed: they used to default to ±0.1, which on any real machine looked like a broken ki rather than a backstop. If you were relying on that ±0.1, set it explicitly.

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, from the same cycle you raise it. 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.
disable - - While true the output is zero and the integral term, derivative term and error history are held cleared, so re-enabling starts from rest and cannot dump a stale integral into the plant. The error and actual history keep tracking the live inputs while disabled, so re-enabling produces no derivative spike and no spurious velocity-gate trip. Use this for a runtime override from a supervisor; use enable for the configured intent. It is a level, not a pulse.
integratorFreeze - - While true the integral term holds its value — it keeps contributing to the output but stops accumulating. This is the difference from iReset, which discharges it to zero. A level, not a pulse.
controllerReset - - While true the whole controller state is cleared — integral term, derivative term and error history — at the start of every cycle. Broader than iReset, which touches the integral term only. It does not suppress the output: the controller runs on from the zeroed history, so output is the proportional term plus a derivative formed against a zero previous error. That is the difference from disable, which holds the output at zero. A level, not a pulse.

Outputs

Path Unit Description
output output unit The control signal, after the output limits are applied. A single value. Starts from zero after every controller start, but the integral state is not cleared by a stop — see Limits and errors.
iTerm output unit The integral term’s accumulated value. It is published one cycle ahead of its effect on output, so the two will not reconcile sample-for-sample on a trace. 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. Is cleared by disable, enable = false and controllerReset.
isEnabled - True when enable is true and the disable input is false. While it is false the output is zero and the state is held cleared.

Parameters

Path Unit Default Range Effect
enable - true - Runs the controller. While false the output is zero and the state is held cleared, exactly as for the disable input. Defaults to true, so a PID that never writes this parameter behaves as it did before the parameter existed — note the actuator loop’s inline PID controller defaults the other way and relies on its configuration to switch it on.
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 — see below.
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 clamped to [0, 2/task period [s]] — stay strictly below the ceiling Derivative filter frequency. Higher gives a sharper, noisier D action; lower gives a softer, slower one. Clamped every cycle to the filter’s stability bound, so a value that would make the derivative diverge is refused rather than accepted — 2000 is the ceiling on a 1 ms task. The clamp is silent; read the leaf back to see the applied value, and expect a warning in the log at startup if your configured value was above the bound. At exactly the ceiling the filter pole sits at −1, so dTerm alternates sign without decaying — treat the bound as a limit to stay under, not a setting. 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 +infinity any Hard upper bound on the integral term. At the default there is no bound. If written below iMin, the two are swapped.
iMin output unit −infinity any Hard lower bound. At the default there is no bound. Note the C++ setter forces the sign (setIMin(5) stores −5), so through the API the window always brackets zero; a direct parameter write does not.
controlOutputMin output unit −infinity must be less than controlOutputMax Lower output limit. Infinity means no limit, which is the legacy behaviour. Set this and its pair before any gain.
controlOutputMax output unit +infinity must be greater than controlOutputMin Upper output limit.
backCalculationGain 1/s 0.0 0 upward Second anti-windup layer. Above 0 it actively unwinds the integral while the output is saturated. 0 is off. Too large a value makes the integral oscillate.
controlErrorDeadBand signal unit 0.0 0 upward Errors smaller than this do not accumulate in the integral. It applies to the integral path only — the proportional and derivative terms still see the full error.

All fifteen parameters are persistent and survive a controller restart. The inputs and outputs do not. No parameters exist below this block.

Setup

  1. Set controlOutputMin and controlOutputMax to what your actuator can accept. Do this first, before any gain.

    Step 1 is what makes the anti-windup work at all. With the limits left at infinity the block never knows it is saturated, so the directional anti-windup and backCalculationGain both have nothing to act on.

  2. Set iMax and iMin to the largest integral contribution you will accept — often a fraction of the output range. Leave them at ±0.1 and the integral will saturate at once.

  3. Set kp, ki and kd to 0, and nd to something sensible for your task rate — 50 to 200 rad/s is a normal starting range on a 1 ms task.

  4. Link error from your comparison. Link actual only if you intend to use the velocity window; otherwise leave it at 0.

  5. Raise kp from 0 until the loop responds usefully. output should track error × kp exactly while ki and kd are still 0.

  6. Raise ki from 0 until steady-state error disappears. Watch iTerm — if it sits pinned at iMax or iMin, go back to step 2.

  7. Raise kd from 0 only if you need to damp overshoot. Watch dTerm on a trace; if it is noisy, lower nd.

  8. Drive the loop into saturation deliberately and confirm it comes back out promptly when the error reverses. If it hangs, see the symptom table.

Tuning

  1. Set the limits before the gains: controlOutputMin and controlOutputMax first, then iMin and iMax. Every anti-windup behaviour depends on them.
  2. Tune kp alone. Raise it until the response is fast enough, then back off until any oscillation is gone. Leave ki and kd at 0 throughout.
  3. Add ki next. Raise it until steady-state error is removed within an acceptable time. Too much shows as a slow oscillation the proportional gain cannot explain.
  4. Add kd last, and only for overshoot. Read dTerm on a trace before you trust it — if the trace is noise, kd is amplifying your sensor and you should lower nd or leave kd at 0.
  5. Set nd against your noise, not against your response. Start at 100 rad/s on a 1 ms task and lower it until dTerm is smooth. Never go above $2/$ task period and never to 0.
  6. Use controlErrorDeadBand only to stop the integral hunting around a small residual error. It does not quiet the proportional or derivative terms.
  7. Use the error thresholds and the velocity window only when the integral must be suppressed during motion — they exist to stop wind-up while the machine is moving fast. Leave them at their defaults otherwise.
  8. Turn on backCalculationGain only if the directional anti-windup alone is not getting you out of saturation fast enough. Start small; a large value drives the integral hard the other way.
  9. Re-check nd after any task-rate change. Its upper limit scales with the task period.

Controller output for a unit step error at three gain sets: proportional onlyholds at 1.0, adding integral gain ramps it up to 3.5 over half a second, andadding derivative gain puts a spike of 1.0 on the first cycle that decays inabout 20 ms.

Read each gain’s contribution off the gap between the curves.

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
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
output or dTerm grew without bound nd above 2/task period, or negative Set nd within range, then restart the controller
The loop stays saturated long after the error reversed Wind-up: the output limits are still at infinity, so the anti-windup has nothing to work against Set controlOutputMin and controlOutputMax
Still slow to leave saturation with the limits set Directional anti-windup alone is not enough for this loop Raise backCalculationGain from 0, a little at a time
The integral oscillates after saturation backCalculationGain too high Lower it, or return it to 0
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, so a loop that was saturated resumes saturated Pulse iReset before releasing the loop
iTerm and output do not add up on a trace Expected: iTerm is published one cycle ahead of its effect Compare iTerm against the next sample of output
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
The integral does not accumulate near the setpoint controlErrorDeadBand or the error thresholds are set Reduce the dead band; remember it affects the integral only
Small errors still drive the proportional term after setting a dead band Expected: the dead band applies to the integral path only Use a dead-zone block upstream if you need to quiet the whole controller
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 limits in the actuator’s own units:

controlOutputMin     = -10.0
controlOutputMax     =  10.0
iMin                 =  -5.0
iMax                 =   5.0
nd                   = 100.0
kp                   =   1.0
ki                   =   0.0
kd                   =   0.0
backCalculationGain  =   0.0
controlErrorDeadBand =   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
iMin < iMax The block Swapped if you invert them, rather than refused — the same thing Limiter does with an inverted window. The C++ setters also force the signs, so only a direct parameter write can invert it Not reported; read both leaves back
controlOutputMin < controlOutputMax Nothing Not checked. The same hazard, with no sign forcing at all Not reported
nd within 0 and 2/task period [s] — the filter’s stability bound The block Clamped, silently, every cycle, at both ends: negative values become 0, values above the bound become the bound. The derivative can no longer be configured into divergence. At exactly the bound the pole is −1, so dTerm alternates without decaying — stay strictly below it. 2000 is the ceiling on a 1 ms task Silent in the RT path; read nd back. A warning is logged at startup when the configured value is above the bound
output controlOutputMin, controlOutputMax Clamped to the range. At the infinite defaults it is unbounded, and the anti-windup is inactive Not reported; compare output against your limits
Integral term iMin, iMax, plus directional anti-windup and backCalculationGain Clamped where a clamp is configured — there is none at the default — and actively prevented from winding further into an active output limit Not reported; watch iTerm
Integral state across a stop Nothing Not cleared by a stop or a start. A loop that was saturated when it stopped resumes saturated. Pulse iReset before releasing the loop, or hold disable, which does clear it Not reported
Derivative state Nothing Not cleared by iReset, a stop, or a start. disable, enable = false and controllerReset all clear it, and they seed the error history from the live error so re-enabling does not kick the derivative Not reported
Channel count Fixed One loop per instance, always Not reported

The only thing the block logs is the startup warning about nd exceeding its stability bound. Every other failure above shows as a value on a trace, not as a message.


Verified against motorcortex-control3 3.30.0 (bc348fd).