From 55cf03c0157e8b14976f371f8b045400c988918b Mon Sep 17 00:00:00 2001 From: alex Date: Mon, 6 Jul 2026 20:24:14 +0200 Subject: [PATCH] pi without the d --- README.md | 23 ++- include/config.h | 87 ++++------ include/pid_autotuner.h | 45 +---- include/thermal_controller.h | 324 ++++++++++++++--------------------- pi-instructions.md | 2 +- src/main.cpp | 48 +++++- 6 files changed, 231 insertions(+), 298 deletions(-) diff --git a/README.md b/README.md index 0c9aa44..65f8c59 100644 --- a/README.md +++ b/README.md @@ -82,20 +82,35 @@ Verify access: `test -w /dev/ttyUSB0 && echo ok` 1. Flash firmware and open the serial monitor at 115200 baud. 2. Confirm `TCA9548A detected` and four valid sensor channels (`ch2`–`ch5`). -3. Run PID autotune once per physical unit (values are stored in EEPROM): +3. Tune **heat PI** (stored in EEPROM on autotune complete): ``` target 0 - autotune 45 + pid default + autotune 50 ``` -4. Start drying: + Emergency cutoff is fixed at **70°C** — you can autotune at 50–55°C with ABS in the chamber while hot corners stay below that. + +4. Tune **mix PI** (spread → fan) while drying: ``` target 55 + log on ``` -Send `help` over serial for all commands (`target`, `fan on/off`, `log on/off`, `status`, `pid`, etc.). + Watch `spread` in status. Adjust mix gains without reflash: + + ``` + mixpi 40 2 + pid save + ``` + + Defaults: heat Kp/Ki from autotune; mix Kp=40 Ki=2 in firmware until tuned. + +5. Optional: `stepresp 45 35` logs open-loop fan steps if you want to estimate mix gain before live tuning. + +Send `help` over serial for all commands (`target`, `mixpi`, `fan test`, `log on/off`, `status`, `pid`, etc.). ## Raspberry Pi control diff --git a/include/config.h b/include/config.h index 598050b..f84a658 100644 --- a/include/config.h +++ b/include/config.h @@ -21,73 +21,58 @@ static const uint8_t HEATER_PIN = A2; // heater via solid-state relay // --------------------------------------------------------------------------- // Temperature control // --------------------------------------------------------------------------- -static const float TARGET_TEMP_C = 0.0f; // power-on default: idle (heater off) -static const float AUTOTUNE_DEFAULT_TEMP_C = 40.0f; // autotune when no temp given and idle -static const float TARGET_MIN_C = 0.0f; // 0 = idle (heater off, fan at idle speed) +static const float TARGET_TEMP_C = 0.0f; +static const float AUTOTUNE_DEFAULT_TEMP_C = 40.0f; +static const float TARGET_MIN_C = 0.0f; static const float TARGET_MAX_C = 80.0f; -// Absolute max corner temp — heater off + full fan. Decoupled from PID target so you can -// run target 50–55 while tuning with headroom for hot corners (ABS in chamber). +// Absolute max corner — heater off + full fan (decoupled from PID target). static const float EMERGENCY_MAX_TEMP_C = 70.0f; -static const float CORNER_STOP_MARGIN_C = 3.0f; // taper heater when max within this of emergency +static const float CORNER_STOP_MARGIN_C = 3.0f; -// PID on chamber average -static const float PID_KP = 4.0f; -static const float PID_KI = 0.05f; -static const float PID_KD = 6.0f; +// Dual PI — heat on avg, mix on spread (no D term) +static const float HEAT_PI_KP = 4.0f; +static const float HEAT_PI_KI = 0.05f; +static const float MIX_PI_KP = 40.0f; +static const float MIX_PI_KI = 2.0f; +static const float SPREAD_TARGET_C = 0.5f; +static const float HEAT_UP_BAND_C = 8.0f; +static const uint8_t FAN_COLD_CAP_PWM = 0; -// Tiered heater cap — more power when cold, gentle near setpoint -static const float HEATER_MAX_DUTY_COLD = 85.0f; // avg >=10 °C below target -static const float HEATER_MAX_DUTY_MID = 65.0f; // avg 3–10 °C below target -static const float HEATER_MAX_DUTY_NEAR = 45.0f; // avg <3 °C below target +// Legacy aliases for autotuner relay math only +static const float PID_KP = HEAT_PI_KP; +static const float PID_KI = HEAT_PI_KI; +static const float PID_KD = 0.0f; +static const float GOOD_SPREAD_C = 5.0f; + +// Tiered heater cap during heat-up +static const float HEATER_MAX_DUTY_COLD = 85.0f; +static const float HEATER_MAX_DUTY_MID = 65.0f; +static const float HEATER_MAX_DUTY_NEAR = 45.0f; static const float HEATER_COLD_BELOW_C = 10.0f; static const float HEATER_WARM_BELOW_C = 3.0f; -// Below this band from target, only the hard cutoff limits max-corner (full heat-up) static const float CORNER_LIMIT_BAND_C = 10.0f; - -// Ramp-up limit (% per second) — still caps sudden jumps static const float HEATER_SLEW_UP_PER_S = 18.0f; - -// Hot-corner limiter: taper heater as max corner approaches stop temperature static const float MAX_TEMP_HEADROOM_C = 15.0f; -// Average-temp approach: taper only in the last few °C before setpoint -static const float APPROACH_BAND_C = 4.0f; - -// When spread is good, allow hottest corner slightly above target so avg can reach setpoint -static const float GOOD_SPREAD_C = 5.0f; -static const float BALANCED_MAX_ABOVE_TARGET_C = 2.0f; -static const float SPREAD_HEADROOM_FACTOR = 0.5f; // extra max-corner °C per °C of spread - static const uint16_t HEATER_CYCLE_MS = 3000; -// Fan PWM (0–255) -static const uint8_t FAN_IDLE_PWM = 77; // ~30 % — optional override via "fan on" -static const float IDLE_AUTO_FAN_OFF_TEMP_C = 40.0f; // idle: fans off when max corner below this -static const uint8_t FAN_MIX_MIN_PWM = 70; // ~27 % — light mixing when spread rises -static const uint8_t FAN_HEAT_MIN_PWM = 100; // ~39 % — floor while heating (near setpoint) -static const uint8_t FAN_HEAT_MAX_PWM = 140; // ~55 % — cap during heat-up -static const float FAN_OFF_BELOW_TARGET_C = 8.0f; // no heat-up fan when avg this far below target -static const float FAN_RAMP_BELOW_TARGET_C = 15.0f; // fan ramps in between this and FAN_OFF_BELOW -static const uint8_t FAN_MIX_MAX_PWM = 200; // ~78 % — cap for spread-driven mixing -static const uint8_t FAN_MAX_PWM = 255; // failsafe / over-temp only -// Most 24 V MOSFET modules are active-low (pin LOW = fan on). If off/speed seem wrong, -// try flipping this and reflash. Test: `fan test 0` (off) vs `fan test 200` vs `fan test 255`. +// Fan PWM +static const uint8_t FAN_IDLE_PWM = 77; +static const float IDLE_AUTO_FAN_OFF_TEMP_C = 40.0f; +static const uint8_t FAN_MAX_PWM = 255; static const bool FAN_PWM_INVERT = true; -// Corner mixing — moderate airflow; full speed reserved for safety -static const float SPREAD_DEADBAND_C = 0.5f; -static const float SPREAD_FULL_MIX_C = 8.0f; static const float SPREAD_EMA_ALPHA = 0.45f; -// PID auto-tune (relay method) — run with: autotune 45 +// PID auto-tune (relay method) — heat PI only static const float AUTOTUNE_HYSTERESIS_C = 0.4f; static const float AUTOTUNE_PREHEAT_BAND_C = 3.0f; static const float AUTOTUNE_PREHEAT_DUTY = 100.0f; -static const uint8_t AUTOTUNE_PREHEAT_FAN_PWM = 0; // fan off — maximize heat-up +static const uint8_t AUTOTUNE_PREHEAT_FAN_PWM = 0; static const uint8_t AUTOTUNE_CYCLES_REQUIRED = 5; -static const uint32_t AUTOTUNE_PREHEAT_TIMEOUT_MS = 1200000UL; // 20 min -static const uint32_t AUTOTUNE_RELAY_STALL_MS = 1500000UL; // 25 min in relay, 0 cycles -static const uint32_t AUTOTUNE_SESSION_TIMEOUT_MS = 3600000UL; // 60 min total +static const uint32_t AUTOTUNE_PREHEAT_TIMEOUT_MS = 1200000UL; +static const uint32_t AUTOTUNE_RELAY_STALL_MS = 1500000UL; +static const uint32_t AUTOTUNE_SESSION_TIMEOUT_MS = 3600000UL; static const uint32_t AUTOTUNE_RELAY_PERIOD_MAX_MS = 2400000UL; // Fan step-response — open-loop heater, fan PWM steps (command: stepresp) @@ -96,9 +81,9 @@ static const float STEPRESP_DEFAULT_HEATER_PCT = 35.0f; static const float STEPRESP_MIN_HEATER_PCT = 10.0f; static const float STEPRESP_MAX_HEATER_PCT = 70.0f; static const float STEPRESP_PREHEAT_BAND_C = 2.0f; -static const uint32_t STEPRESP_PREHEAT_TIMEOUT_MS = 1200000UL; // 20 min -static const uint32_t STEPRESP_BASELINE_MS = 120000UL; // 2 min fan-off baseline -static const uint32_t STEPRESP_STEP_HOLD_MS = 300000UL; // 5 min per fan level +static const uint32_t STEPRESP_PREHEAT_TIMEOUT_MS = 1200000UL; +static const uint32_t STEPRESP_BASELINE_MS = 120000UL; +static const uint32_t STEPRESP_STEP_HOLD_MS = 300000UL; static const uint32_t STEPRESP_LOG_INTERVAL_MS = 1000UL; static const uint8_t STEPRESP_FAN_STEPS[] = {0, 77, 140, 200, 255}; static const uint8_t STEPRESP_FAN_STEP_COUNT = @@ -110,4 +95,4 @@ static const uint8_t STEPRESP_FAN_STEP_COUNT = static const uint32_t SENSOR_READ_INTERVAL_MS = 1000; static const uint32_t CONTROL_INTERVAL_MS = 500; static const uint32_t SERIAL_REPORT_INTERVAL_MS = 2000; -static const bool LOG_CSV_DEFAULT = false; // enable with serial command: log on +static const bool LOG_CSV_DEFAULT = false; diff --git a/include/pid_autotuner.h b/include/pid_autotuner.h index 46c1262..5ab2207 100644 --- a/include/pid_autotuner.h +++ b/include/pid_autotuner.h @@ -27,10 +27,8 @@ public: useMaxSensorPv_(false), sessionStartMs_(0), phaseStartMs_(0), - resultKp_(PID_KP), - resultKi_(PID_KI), - resultKd_(PID_KD), - resultFanMixMax_(FAN_MIX_MAX_PWM) {} + resultKp_(HEAT_PI_KP), + resultKi_(HEAT_PI_KI) {} Phase phase() const { return phase_; } @@ -77,7 +75,7 @@ public: Serial.print(preheatTargetC(), 1); Serial.print(F("-")); Serial.print(setpointC_, 1); - Serial.println(F("C avg")); + Serial.println(F("C avg (heat PI only)")); return true; } @@ -98,20 +96,18 @@ public: float resultKp() const { return resultKp_; } float resultKi() const { return resultKi_; } - float resultKd() const { return resultKd_; } - uint8_t resultFanMixMax() const { return resultFanMixMax_; } Phase update(float avgTempC, float maxTempC, float spreadC, uint32_t nowMs, float &heaterDutyOut, uint8_t &fanPwmOut) { heaterDutyOut = 0.0f; - fanPwmOut = FAN_HEAT_MIN_PWM; + fanPwmOut = AUTOTUNE_PREHEAT_FAN_PWM; if (phase_ == Phase::Idle || phase_ == Phase::Done || phase_ == Phase::Failed) { return phase_; } - if (maxTempC >= TARGET_MAX_C - 1.0f) { - fail(F("autotune: abort — max sensor at safety limit")); + if (maxTempC >= EMERGENCY_MAX_TEMP_C) { + fail(F("autotune: abort — max sensor at emergency limit")); return phase_; } @@ -140,11 +136,6 @@ public: return phase_; } - if (nowMs - sessionStartMs_ > AUTOTUNE_SESSION_TIMEOUT_MS) { - fail(F("autotune: abort — session timeout")); - return phase_; - } - if (cycleCount_ == 0 && nowMs - phaseStartMs_ > AUTOTUNE_RELAY_STALL_MS) { Serial.print(F("autotune: relay stalled — avg ")); Serial.print(avgTempC, 1); @@ -264,7 +255,6 @@ private: const float ku = (4.0f * 100.0f) / (PI * avgAmplitude); resultKp_ = 0.45f * ku; resultKi_ = resultKp_ / (2.2f * avgPeriodSec); - resultKd_ = resultKp_ * avgPeriodSec / 6.3f; if (resultKp_ < 0.5f) { resultKp_ = 0.5f; @@ -273,30 +263,15 @@ private: resultKi_ = resultKp_ / 3.0f; } - resultFanMixMax_ = FAN_MIX_MAX_PWM; - const float spreadAvg = - spreadSamples_ > 0 ? spreadSum_ / static_cast(spreadSamples_) : GOOD_SPREAD_C; - if (spreadAvg > GOOD_SPREAD_C) { - const float boost = 1.0f + ((spreadAvg - GOOD_SPREAD_C) / 10.0f); - int boosted = static_cast(static_cast(FAN_MIX_MAX_PWM) * boost); - if (boosted > static_cast(FAN_MAX_PWM) - 20) { - boosted = FAN_MAX_PWM - 20; - } - resultFanMixMax_ = static_cast(boosted); - } - phase_ = Phase::Done; Serial.print(F("autotune: done in ")); Serial.print((nowMs - sessionStartMs_) / 1000UL); Serial.println(F("s")); - Serial.print(F(" Kp=")); + Serial.print(F(" heat Kp=")); Serial.print(resultKp_, 3); Serial.print(F(" Ki=")); - Serial.print(resultKi_, 4); - Serial.print(F(" Kd=")); - Serial.print(resultKd_, 3); - Serial.print(F(" fanMixMax=")); - Serial.println(resultFanMixMax_); + Serial.println(resultKi_, 4); + Serial.println(F(" tune mix PI separately: mixpi ")); } void fail(const __FlashStringHelper *reason) { @@ -325,6 +300,4 @@ private: uint32_t phaseStartMs_; float resultKp_; float resultKi_; - float resultKd_; - uint8_t resultFanMixMax_; }; diff --git a/include/thermal_controller.h b/include/thermal_controller.h index 7bbdcf2..9d658b0 100644 --- a/include/thermal_controller.h +++ b/include/thermal_controller.h @@ -11,10 +11,11 @@ class ThermalController { public: - enum class HeaterBlock : uint8_t { None, Cutoff, Corner, Allow, Autotune, StepResp }; + enum class HeaterBlock : uint8_t { None, Cutoff, Corner, Autotune, StepResp }; ThermalController() - : pid_(PID_KP, PID_KI, PID_KD, 0.0f, 100.0f), + : heatPi_(HEAT_PI_KP, HEAT_PI_KI, 0.0f, 0.0f, 100.0f), + mixPi_(MIX_PI_KP, MIX_PI_KI, 0.0f, 0.0f, 255.0f), autotuner_(), stepresp_(), targetTempC_(TARGET_TEMP_C), @@ -22,9 +23,9 @@ public: heaterAllowancePercent_(100.0f), cornerSpreadC_(0.0f), lastMaxTempC_(0.0f), - fanPwm_(FAN_MAX_PWM), - fanMixMax_(FAN_MIX_MAX_PWM), - adaptiveEnabled_(false), + regulatingFanPwm_(0), + fanPwm_(0), + tuningLoaded_(false), fanIdleOverride_(false), sensorWarmValid_(false), cutoffActive_(false), @@ -36,7 +37,9 @@ public: heaterBlock_(HeaterBlock::None), fanTestActive_(false), fanTestPwm_(0), - fanTestEndMs_(0) {} + fanTestEndMs_(0) { + mixPi_.setSetpoint(0.0f); + } void begin() { pinMode(FAN_PIN, OUTPUT); @@ -44,12 +47,12 @@ public: digitalWrite(HEATER_PIN, LOW); writeFan(0); - pid_.setSetpoint(targetTempC_); - pid_.reset(); + heatPi_.setSetpoint(targetTempC_); + heatPi_.reset(); + mixPi_.reset(); heaterCycleStartMs_ = millis(); lastHeaterUpdateMs_ = 0; failSafeActive_ = true; - fanPwm_ = FAN_MAX_PWM; cornerSpreadC_ = 0.0f; lastMaxTempC_ = 0.0f; sensorWarmValid_ = false; @@ -62,7 +65,7 @@ public: TuningData stored; if (tuningLoad(stored)) { applyTuning(stored); - Serial.println(F("Loaded learned PID from EEPROM")); + Serial.println(F("Loaded learned PI from EEPROM")); printTuning(); } @@ -74,46 +77,75 @@ public: } void applyTuning(const TuningData &data) { - pid_.setTunings(data.kp, data.ki, data.kd); - fanMixMax_ = data.fanMixMax; - adaptiveEnabled_ = true; + heatPi_.setTunings(data.heatKp, data.heatKi, 0.0f); + mixPi_.setTunings(data.mixKp, data.mixKi, 0.0f); + tuningLoaded_ = true; } void clearTuning() { - adaptiveEnabled_ = false; - fanMixMax_ = FAN_MIX_MAX_PWM; - pid_.setTunings(PID_KP, PID_KI, PID_KD); + tuningLoaded_ = false; + heatPi_.setTunings(HEAT_PI_KP, HEAT_PI_KI, 0.0f); + mixPi_.setTunings(MIX_PI_KP, MIX_PI_KI, 0.0f); tuningClear(); - pid_.reset(); - Serial.println(F("PID reset to defaults")); + heatPi_.reset(); + mixPi_.reset(); + Serial.println(F("PI reset to defaults")); + } + + void setMixTunings(float kp, float ki) { + mixPi_.setTunings(kp, ki, 0.0f); + mixPi_.reset(); + Serial.print(F("Mix PI Kp=")); + Serial.print(kp, 3); + Serial.print(F(" Ki=")); + Serial.println(ki, 4); + } + + void resetMixTunings() { + mixPi_.setTunings(MIX_PI_KP, MIX_PI_KI, 0.0f); + mixPi_.reset(); + Serial.println(F("Mix PI reset to defaults")); } void printTuning() const { - Serial.print(F("PID Kp=")); - Serial.print(pidKp(), 3); + Serial.print(F("Heat PI Kp=")); + Serial.print(heatPi_.kp(), 3); Serial.print(F(" Ki=")); - Serial.print(pidKi(), 4); - Serial.print(F(" Kd=")); - Serial.print(pidKd(), 3); - Serial.print(F(" fanMixMax=")); - Serial.print(fanMixMax_); - Serial.print(F(" adaptive=")); - Serial.println(adaptiveEnabled_ ? F("yes") : F("no")); + Serial.print(heatPi_.ki(), 4); + Serial.print(F(" Mix PI Kp=")); + Serial.print(mixPi_.kp(), 3); + Serial.print(F(" Ki=")); + Serial.print(mixPi_.ki(), 4); + Serial.print(F(" tuned=")); + Serial.println(tuningLoaded_ ? F("yes") : F("no")); } - float pidKp() const { return pid_.kp(); } - float pidKi() const { return pid_.ki(); } - float pidKd() const { return pid_.kd(); } + void saveTuningToEeprom() { + TuningData data; + data.magic = TUNING_MAGIC; + data.heatKp = heatPi_.kp(); + data.heatKi = heatPi_.ki(); + data.mixKp = mixPi_.kp(); + data.mixKi = mixPi_.ki(); + tuningSave(data); + tuningLoaded_ = true; + Serial.println(F("Saved PI to EEPROM")); + } - bool isAdaptive() const { return adaptiveEnabled_; } + float heatKp() const { return heatPi_.kp(); } + float heatKi() const { return heatPi_.ki(); } + float mixKp() const { return mixPi_.kp(); } + float mixKi() const { return mixPi_.ki(); } + + bool isTuningLoaded() const { return tuningLoaded_; } bool startAutotune(float setpointC) { if (autotuner_.isActive() || stepresp_.isActive()) { return false; } - adaptiveEnabled_ = false; cutoffActive_ = false; - pid_.reset(); + heatPi_.reset(); + mixPi_.reset(); return autotuner_.start(setpointC); } @@ -136,9 +168,9 @@ public: return false; } stopFanTest(); - adaptiveEnabled_ = false; cutoffActive_ = false; - pid_.reset(); + heatPi_.reset(); + mixPi_.reset(); setTarget(targetC, false); if (!stepresp_.start(targetC, heaterPct)) { return false; @@ -176,23 +208,27 @@ public: return false; } + heatPi_.setTunings(autotuner_.resultKp(), autotuner_.resultKi(), 0.0f); + heatPi_.reset(); + TuningData data; data.magic = TUNING_MAGIC; - data.kp = autotuner_.resultKp(); - data.ki = autotuner_.resultKi(); - data.kd = autotuner_.resultKd(); - data.fanMixMax = autotuner_.resultFanMixMax(); + data.heatKp = autotuner_.resultKp(); + data.heatKi = autotuner_.resultKi(); + data.mixKp = mixPi_.kp(); + data.mixKi = mixPi_.ki(); tuningSave(data); - applyTuning(data); + tuningLoaded_ = true; autotuner_.reset(); - Serial.println(F("Saved learned PID to EEPROM")); + Serial.println(F("Saved heat PI to EEPROM (mix PI unchanged)")); return true; } void setTarget(float targetC, bool persist = true) { targetTempC_ = targetC; - pid_.setSetpoint(targetC); - pid_.reset(); + heatPi_.setSetpoint(targetC); + heatPi_.reset(); + mixPi_.reset(); cutoffActive_ = false; if (targetC > 0.0f) { fanIdleOverride_ = false; @@ -280,7 +316,12 @@ public: void stopFanTest() { fanTestActive_ = false; } - float maxHeatStopAt(float avgTempC) const { return maxHeatStopTemp(avgTempC); } + float maxHeatStopAt(float avgTempC) const { + if (isIdle()) { + return INFINITY; + } + return maxHeatStopTemp(avgTempC); + } const char *heaterBlockReason() const { switch (heaterBlock_) { @@ -288,8 +329,6 @@ public: return "cutoff"; case HeaterBlock::Corner: return "corner"; - case HeaterBlock::Allow: - return "allow"; case HeaterBlock::Autotune: return "autotune"; case HeaterBlock::StepResp: @@ -299,6 +338,10 @@ public: } } + const char *regulatingModeName() const { + return tuningLoaded_ ? "regulating" : "manual"; + } + void update(float avgTempC, float maxTempC, float cornerSpreadC, uint32_t nowMs) { failSafeActive_ = false; noteSensorMax(maxTempC); @@ -306,8 +349,8 @@ public: heaterBlock_ = HeaterBlock::None; cornerSpreadC_ = - SPREAD_EMA_ALPHA * cornerSpreadC + - (1.0f - SPREAD_EMA_ALPHA) * cornerSpreadC_; + SPREAD_EMA_ALPHA * cornerSpreadC_ + + (1.0f - SPREAD_EMA_ALPHA) * cornerSpreadC; if (stepresp_.isActive()) { updateStepResponse(avgTempC, maxTempC, nowMs); @@ -322,21 +365,17 @@ public: if (isIdle()) { forceHeaterOff(); cutoffActive_ = false; - pid_.reset(); + heatPi_.reset(); + mixPi_.reset(); lastHeaterUpdateMs_ = nowMs; applyFan(nowMs); return; } - if (adaptiveEnabled_) { - updateAdaptive(avgTempC, maxTempC, nowMs); - } else { - updateLegacy(avgTempC, maxTempC, nowMs); - } - + updateRegulating(avgTempC, maxTempC, nowMs); lastHeaterUpdateMs_ = nowMs; applyHeaterBurst(nowMs); - applyFan(nowMs); + writeFan(regulatingFanPwm_); } void enterFailSafe() { @@ -344,7 +383,8 @@ public: cutoffActive_ = false; forceHeaterOff(); applyFan(millis()); - pid_.reset(); + heatPi_.reset(); + mixPi_.reset(); autotuner_.abort(); stepresp_.abort(); } @@ -405,7 +445,7 @@ private: void updateAutotune(float avgTempC, float maxTempC, uint32_t nowMs) { float duty = 0.0f; - uint8_t fan = FAN_HEAT_MIN_PWM; + uint8_t fan = AUTOTUNE_PREHEAT_FAN_PWM; autotuner_.update(avgTempC, maxTempC, cornerSpreadC_, nowMs, duty, fan); heaterDutyPercent_ = duty; @@ -418,38 +458,33 @@ private: commitAutotuneIfDone(); } - void updateAdaptive(float avgTempC, float maxTempC, uint32_t nowMs) { - const float cutoff = cutoffThreshold(); - - if (maxTempC >= cutoff) { + void updateRegulating(float avgTempC, float maxTempC, uint32_t nowMs) { + if (maxTempC >= EMERGENCY_MAX_TEMP_C) { cutoffActive_ = true; heaterDutyPercent_ = 0.0f; heaterAllowancePercent_ = 0.0f; - heaterOn_ = false; + regulatingFanPwm_ = FAN_MAX_PWM; heaterBlock_ = HeaterBlock::Cutoff; - pid_.reset(); + heatPi_.reset(); + mixPi_.reset(); return; } - if (cutoffActive_ && maxTempC <= targetTempC_) { + if (cutoffActive_ && maxTempC < EMERGENCY_MAX_TEMP_C - 5.0f) { cutoffActive_ = false; - pid_.reset(); + heatPi_.reset(); + mixPi_.reset(); } if (cutoffActive_) { heaterBlock_ = HeaterBlock::Cutoff; + regulatingFanPwm_ = FAN_MAX_PWM; return; } - if (shouldLimitMaxCorner(avgTempC) && maxTempC >= maxHeatStopTemp(avgTempC)) { - heaterDutyPercent_ = 0.0f; - heaterAllowancePercent_ = 0.0f; - heaterBlock_ = HeaterBlock::Corner; - pid_.reset(); - return; - } + heaterAllowancePercent_ = allowanceFromMaxCorner(maxTempC, avgTempC); - float duty = pid_.compute(avgTempC, nowMs); + float duty = heatPi_.compute(avgTempC, nowMs); const float below = targetTempC_ - avgTempC; if (below > 8.0f) { const float floor = below > 15.0f ? 75.0f : 60.0f; @@ -461,51 +496,21 @@ private: if (duty > maxDuty) { duty = maxDuty; } - heaterAllowancePercent_ = maxDuty; - heaterDutyPercent_ = duty; - } - - void updateLegacy(float avgTempC, float maxTempC, uint32_t nowMs) { - const float cutoff = cutoffThreshold(); - - if (maxTempC >= cutoff) { - cutoffActive_ = true; - heaterDutyPercent_ = 0.0f; - heaterAllowancePercent_ = 0.0f; - heaterOn_ = false; - heaterBlock_ = HeaterBlock::Cutoff; - pid_.reset(); - return; - } - - if (cutoffActive_ && maxTempC <= targetTempC_) { - cutoffActive_ = false; - pid_.reset(); - } - - if (cutoffActive_) { - heaterBlock_ = HeaterBlock::Cutoff; - return; - } - - if (shouldLimitMaxCorner(avgTempC) && maxTempC >= maxHeatStopTemp(avgTempC)) { - heaterDutyPercent_ = 0.0f; - heaterAllowancePercent_ = 0.0f; - heaterBlock_ = HeaterBlock::Corner; - pid_.reset(); - return; - } - - const float pidOut = pid_.compute(avgTempC, nowMs); - heaterAllowancePercent_ = heaterAllowancePercent(avgTempC, maxTempC); - float duty = pidOut; if (duty > heaterAllowancePercent_) { duty = heaterAllowancePercent_; - } - if (duty <= 0.0f && heaterAllowancePercent_ <= 0.0f) { - heaterBlock_ = HeaterBlock::Allow; + if (heaterAllowancePercent_ < 100.0f) { + heaterBlock_ = HeaterBlock::Corner; + } } heaterDutyPercent_ = applyHeaterRamp(duty, avgTempC, nowMs); + + const float mixInput = SPREAD_TARGET_C - cornerSpreadC_; + float fanOut = mixPi_.compute(mixInput, nowMs); + uint8_t fanPwm = static_cast(fanOut + 0.5f); + if (avgTempC < targetTempC_ - HEAT_UP_BAND_C && fanPwm > FAN_COLD_CAP_PWM) { + fanPwm = FAN_COLD_CAP_PWM; + } + regulatingFanPwm_ = fanPwm; } static float clampPercent(float value) { @@ -518,8 +523,6 @@ private: return value; } - bool isBalancedChamber() const { return cornerSpreadC_ <= GOOD_SPREAD_C; } - bool shouldLimitMaxCorner(float avgTempC) const { return avgTempC >= targetTempC_ - CORNER_LIMIT_BAND_C; } @@ -536,10 +539,6 @@ private: return 100.0f; } - if (isBalancedChamber() && avgTempC < targetTempC_) { - return 100.0f; - } - const float stopAt = maxHeatStopTemp(avgTempC); if (maxTempC >= stopAt) { return 0.0f; @@ -553,19 +552,6 @@ private: return clampPercent((headroom / MAX_TEMP_HEADROOM_C) * 100.0f); } - float allowanceFromAverage(float avgTempC) const { - if (avgTempC >= targetTempC_) { - return 0.0f; - } - - const float below = targetTempC_ - avgTempC; - if (below >= APPROACH_BAND_C) { - return 100.0f; - } - - return clampPercent((below / APPROACH_BAND_C) * 100.0f); - } - float heaterMaxDuty(float avgTempC) const { if (avgTempC >= targetTempC_) { return HEATER_MAX_DUTY_NEAR; @@ -581,17 +567,6 @@ private: return HEATER_MAX_DUTY_NEAR; } - float heaterAllowancePercent(float avgTempC, float maxTempC) const { - const float fromMax = allowanceFromMaxCorner(maxTempC, avgTempC); - const float fromAvg = allowanceFromAverage(avgTempC); - float allowance = fromMax < fromAvg ? fromMax : fromAvg; - const float maxDuty = heaterMaxDuty(avgTempC); - if (allowance > maxDuty) { - allowance = maxDuty; - } - return allowance; - } - float applyHeaterRamp(float requestedDuty, float avgTempC, uint32_t nowMs) { const float maxDuty = heaterMaxDuty(avgTempC); if (requestedDuty > maxDuty) { @@ -609,55 +584,6 @@ private: return requestedDuty; } - uint8_t fanPwmForHeatUp() const { - if (heaterDutyPercent_ <= 0.0f) { - return 0; - } - - const float below = targetTempC_ - lastAvgTempC_; - uint8_t heatFan = 0; - if (below <= FAN_OFF_BELOW_TARGET_C) { - const uint8_t span = FAN_HEAT_MAX_PWM - FAN_HEAT_MIN_PWM; - heatFan = FAN_HEAT_MIN_PWM + - static_cast((heaterDutyPercent_ / 100.0f) * static_cast(span)); - } else if (below < FAN_RAMP_BELOW_TARGET_C) { - const float spanC = FAN_RAMP_BELOW_TARGET_C - FAN_OFF_BELOW_TARGET_C; - const float t = (FAN_RAMP_BELOW_TARGET_C - below) / spanC; - heatFan = static_cast(t * static_cast(FAN_HEAT_MIN_PWM)); - } - - uint8_t mixFan = 0; - if (below <= FAN_OFF_BELOW_TARGET_C || cornerSpreadC_ > GOOD_SPREAD_C) { - mixFan = fanPwmForCornerSpread(); - } - - uint8_t duty = heatFan > mixFan ? heatFan : mixFan; - - if (lastAvgTempC_ >= targetTempC_ - 2.0f && lastMaxTempC_ > targetTempC_ && - duty < FAN_MAX_PWM) { - duty = FAN_MAX_PWM; - } - return duty; - } - - uint8_t fanPwmForCornerSpread() const { - if (cornerSpreadC_ <= SPREAD_DEADBAND_C) { - return 0; - } - - float spread = cornerSpreadC_; - if (spread > SPREAD_FULL_MIX_C) { - spread = SPREAD_FULL_MIX_C; - } - - const float t = - (spread - SPREAD_DEADBAND_C) / (SPREAD_FULL_MIX_C - SPREAD_DEADBAND_C); - const uint8_t mixMax = fanMixMax_; - const uint8_t mixMin = FAN_MIX_MIN_PWM; - const uint8_t span = mixMax > mixMin ? mixMax - mixMin : 0; - return mixMin + static_cast(t * static_cast(span)); - } - void applyHeaterBurst(uint32_t nowMs) { if (heaterDutyPercent_ <= 0.0f) { forceHeaterOff(); @@ -702,12 +628,10 @@ private: writeFan(FAN_MAX_PWM); return; } - - uint8_t duty = fanPwmForHeatUp(); - writeFan(duty); } - PidController pid_; + PidController heatPi_; + PidController mixPi_; PidAutotuner autotuner_; FanStepResponse stepresp_; float targetTempC_; @@ -715,9 +639,9 @@ private: float heaterAllowancePercent_; float cornerSpreadC_; float lastMaxTempC_; + uint8_t regulatingFanPwm_; uint8_t fanPwm_; - uint8_t fanMixMax_; - bool adaptiveEnabled_; + bool tuningLoaded_; bool fanIdleOverride_; bool sensorWarmValid_; bool cutoffActive_; diff --git a/pi-instructions.md b/pi-instructions.md index 51d8fa5..9bc0e03 100644 --- a/pi-instructions.md +++ b/pi-instructions.md @@ -26,7 +26,7 @@ python3 scripts/capture_csv.py log # logs/dryer_YYYYMMDD_HHMMSS.csv ``` -**TUI keys:** `0` idle · `t` target · `p` presets · `f` fan on · `F` fan off · `l` toggle CSV log · `a` autotune · `:` raw command · `q` quit +**TUI keys:** `0` idle · `t` target · `p` presets · `f` fan on · `F` fan off · `l` CSV log · `a` autotune · `r` stepresp · `:` command · `q` quit **Always-on logging** diff --git a/src/main.cpp b/src/main.cpp index 80a19fd..8112e5c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -178,10 +178,8 @@ void printStatus(float avgTemp, float minTemp, float maxTemp) { Serial.print(F(" heat=")); Serial.print(thermal.stepResponseHeaterPct(), 0); Serial.print(F("%")); - } else if (thermal.isAdaptive()) { - Serial.print(F("learned")); } else { - Serial.print(F("manual")); + Serial.print(thermal.regulatingModeName()); } Serial.print(F(" sensors=[")); @@ -211,12 +209,16 @@ void printHelp() { Serial.println(F(" fan off cancel idle fan override (auto-off below 40C)")); Serial.println(F(" fan on idle fan 30% (optional, auto-off below 40C)")); Serial.println(F(" fan test N set fan PWM 0-255 for 15s (verify wiring)")); - Serial.println(F(" autotune [C] learn PID (default: 40C when idle)")); + Serial.println(F(" autotune [C] learn heat PI (default: 40C when idle)")); Serial.println(F(" autotune stop")); Serial.println(F(" stepresp [C] [heater%] fan step response (default: 45C 35%)")); Serial.println(F(" stepresp stop")); - Serial.println(F(" pid show PID / adaptive status")); - Serial.println(F(" pid default reset to factory PID")); + Serial.println(F(" pid show heat + mix PI gains")); + Serial.println(F(" pid default reset all PI to factory")); + Serial.println(F(" pid save write current PI to EEPROM")); + Serial.println(F(" mixpi show mix PI gains")); + Serial.println(F(" mixpi set mix PI (spread -> fan)")); + Serial.println(F(" mixpi default reset mix PI to factory")); Serial.println(F(" status print current readings")); Serial.println(F(" log on|off CSV data stream")); Serial.println(F(" help show this message")); @@ -393,6 +395,40 @@ void processSerialLine(const char *line) { return; } + if (strcmp(line, "pid save") == 0) { + thermal.saveTuningToEeprom(); + return; + } + + if (strcmp(line, "mixpi") == 0 || strcmp(line, "mixpi show") == 0) { + Serial.print(F("Mix PI Kp=")); + Serial.print(thermal.mixKp(), 3); + Serial.print(F(" Ki=")); + Serial.println(thermal.mixKi(), 4); + return; + } + + if (strcmp(line, "mixpi default") == 0) { + thermal.resetMixTunings(); + return; + } + + if (strncmp(line, "mixpi ", 6) == 0) { + const float kp = atof(line + 6); + const char *space = strchr(line + 6, ' '); + if (space == nullptr) { + Serial.println(F("ERR mixpi requires: mixpi ")); + return; + } + const float ki = atof(space + 1); + if (kp <= 0.0f || ki < 0.0f) { + Serial.println(F("ERR mixpi Kp must be > 0, Ki >= 0")); + return; + } + thermal.setMixTunings(kp, ki); + return; + } + if (strcmp(line, "help") == 0) { printHelp(); return;