Fix COOL->HOLD undershoot caused by stale pid_rate windup

process_pid() was advancing pid_rate and pid_cool unconditionally on
every tick regardless of which one actually drove y. While COOL had
pid_cool driving, pid_rate kept silently integrating against the same
(very negative) heatrate_err with no way to act on it, building a
large stale windup. The moment the FSM returned to HOLD (which only
reset pid_hold), y read straight from that saturated pid_rate instead
of a fresh value, so the heater stayed off well past the target
before the windup finally unwound - an undershoot of about 1 degree
in scripts/demos/pid/demo_temp_controller.py's last (cooling) step.

Now only the PID currently driving y is process()'d each tick, and
pid_rate is also reset on IDLE->HOLD and COOL->HOLD (it was already
reset on the transitions into HEAT/COOL). Undershoot drops to ~0.06
degrees, in line with normal PID settling.
This commit is contained in:
2026-06-21 13:55:57 +02:00
parent 929b8758c4
commit 55020d9f38
+16 -3
View File
@@ -105,9 +105,12 @@ class TempControllerBase(APid):
elif self.state == States.IDLE:
# Just (re-)enabled - land in HOLD; the very next tick's
# threshold check (below) moves it on to HEAT/COOL if the gap
# actually warrants it.
# actually warrants it. pid_rate was frozen (see process_pid())
# and possibly stale for as long as we were disabled - start it
# clean rather than resuming wherever it last left off.
state_next = States.HOLD
self.pid_hold.reset()
self.pid_rate.reset()
elif self.state == States.HOLD:
if diff >= self.thresholds['HoldHeat']:
state_next = States.HEAT
@@ -129,6 +132,11 @@ class TempControllerBase(APid):
elif diff >= -self.thresholds['CoolHold']:
state_next = States.HOLD
self.pid_hold.reset()
# pid_rate was frozen during COOL (see process_pid()) -
# resume it clean rather than from whatever it last held
# before COOL took over, which by now may be a stale fit
# for a completely different part of the curve.
self.pid_rate.reset()
if state_next != self.state:
self.state = state_next
@@ -136,14 +144,19 @@ class TempControllerBase(APid):
def process_pid(self, theta_err, heatrate_err, hold_scale=1.0):
self.pid_hold.process(theta_err, -self.theta_ist, hold_scale)
self.pid_rate.process(heatrate_err, -self.heatrate_ist)
self.pid_cool.process(heatrate_err, -self.heatrate_ist)
# Only the PID actually driving y is advanced - otherwise the
# inactive one (e.g. pid_rate while COOL has pid_cool driving)
# would keep silently integrating against a heatrate_err that
# isn't actually under its control, building a stale windup that
# causes a discontinuity in y the moment it takes back over.
if self.state == States.IDLE:
self.y = 0
elif self.state == States.COOL:
self.pid_cool.process(heatrate_err, -self.heatrate_ist)
self.y = self.pid_cool.get_y()
else:
self.pid_rate.process(heatrate_err, -self.heatrate_ist)
self.y = self.pid_rate.get_y()
self.post_pid()