diff --git a/components/pid/temp_controller_base.py b/components/pid/temp_controller_base.py index f58d68d..2a380b5 100644 --- a/components/pid/temp_controller_base.py +++ b/components/pid/temp_controller_base.py @@ -132,11 +132,15 @@ class TempControllerBase(TempControllerFsm, APid): def process_pid(self, theta_err, hold_scale=1.0): self.pid_hold.process(theta_err, -self.theta_ist, hold_scale) - # Compute heatrate_soll HERE, after pid_hold ticks, so it uses - # this tick's output rather than last tick's — eliminates the - # one-tick cascade delay that caused p_set to dip near zero on - # the first tick of a new ramp (HOLD→HEAT transition). - self.heatrate_soll = self.heatrate_soll_set * self.pid_hold.get_y() + # In HOLD state, clamp pid_hold's output to [0, 1]: a small temperature + # overshoot makes pid_hold.y go negative, which would invert heatrate_soll + # and drive pid_heat's power to 0, causing a limit cycle (power on → + # overshoot → power off → coast down → repeat). Clamping to 0 lets the + # outer loop reduce the inner setpoint to zero but no further. + pid_hold_y = self.pid_hold.get_y() + if self.state == States.HOLD: + pid_hold_y = max(0.0, pid_hold_y) + self.heatrate_soll = self.heatrate_soll_set * pid_hold_y heatrate_err = self.heatrate_soll - self.heatrate_ist # Only the PID actually driving y is advanced - otherwise the