add fanchars

This commit is contained in:
2026-07-06 22:19:43 +02:00
parent 55cf03c015
commit 8182b9efd2
9 changed files with 296 additions and 439 deletions

View File

@@ -3,7 +3,7 @@
#include <Arduino.h>
#include "config.h"
#include "fan_step_response.h"
#include "fan_characterize.h"
#include "pid_autotuner.h"
#include "pid_controller.h"
#include "settings_store.h"
@@ -11,19 +11,20 @@
class ThermalController {
public:
enum class HeaterBlock : uint8_t { None, Cutoff, Corner, Autotune, StepResp };
enum class HeaterBlock : uint8_t { None, Cutoff, Corner, Autotune, FanChars };
ThermalController()
: 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_(),
fanchars_(),
targetTempC_(TARGET_TEMP_C),
heaterDutyPercent_(0.0f),
heaterAllowancePercent_(100.0f),
cornerSpreadC_(0.0f),
lastMaxTempC_(0.0f),
regulatingFanPwm_(0),
stirFanPwm_(FAN_IDLE_PWM),
fanPwm_(0),
tuningLoaded_(false),
fanIdleOverride_(false),
@@ -70,9 +71,13 @@ public:
}
SettingsData settings;
if (settingsLoad(settings) && settings.targetC >= TARGET_MIN_C &&
settings.targetC <= TARGET_MAX_C) {
setTarget(settings.targetC, false);
if (settingsLoad(settings)) {
if (settings.stirFanPwm > 0) {
stirFanPwm_ = settings.stirFanPwm;
}
if (settings.targetC >= TARGET_MIN_C && settings.targetC <= TARGET_MAX_C) {
setTarget(settings.targetC, false);
}
}
}
@@ -140,7 +145,7 @@ public:
bool isTuningLoaded() const { return tuningLoaded_; }
bool startAutotune(float setpointC) {
if (autotuner_.isActive() || stepresp_.isActive()) {
if (autotuner_.isActive() || fanchars_.isActive()) {
return false;
}
cutoffActive_ = false;
@@ -163,46 +168,65 @@ public:
float autotunePreheatTargetC() const { return autotuner_.preheatTargetC(); }
bool startStepResponse(float targetC, float heaterPct) {
if (autotuner_.isActive() || stepresp_.isActive()) {
bool startFanCharacterize(float maxCornerC, float avgTempC) {
if (autotuner_.isActive() || fanchars_.isActive()) {
return false;
}
stopFanTest();
cutoffActive_ = false;
heatPi_.reset();
mixPi_.reset();
setTarget(targetC, false);
if (!stepresp_.start(targetC, heaterPct)) {
setTarget(0.0f, false);
return fanchars_.start(maxCornerC, avgTempC);
}
void stopFanCharacterize() {
fanchars_.abort();
forceHeaterOff();
writeFan(0);
}
bool isFanCharacterizeActive() const { return fanchars_.isActive(); }
uint32_t fanCharacterizeElapsedMs(uint32_t nowMs) const { return fanchars_.elapsedMs(nowMs); }
const char *fanCharacterizePhaseName() const { return fanchars_.phaseName(); }
uint8_t fanCharacterizeProfileIndex() const { return fanchars_.profileIndex(); }
uint8_t fanCharacterizeProfileCount() const { return fanchars_.profileCount(); }
uint8_t fanCharacterizeFanPwm() const { return fanchars_.currentFanPwm(); }
bool isFanCharacterizeRefineRun() const { return fanchars_.isRefineRun(); }
float fanCharacterizeHeaterPct() const { return fanchars_.heaterPct(); }
uint8_t stirFanPwm() const { return stirFanPwm_; }
void logFanCharacterizeIfDue(const float *sensorTemps, const bool *sensorValid, uint8_t sensorCount,
float avgTempC, float minTempC, float maxTempC, float spreadC,
uint32_t nowMs) {
fanchars_.logIfDue(sensorTemps, sensorValid, sensorCount, avgTempC, minTempC, maxTempC,
spreadC, nowMs);
}
bool saveStirFanFromCharacterize() {
if (fanchars_.phase() != FanCharacterize::Phase::Done) {
return false;
}
writeFan(0);
const uint8_t winner = fanchars_.winnerFanPwm();
if (winner == 0) {
return false;
}
stirFanPwm_ = winner;
settingsSaveStirFan(winner);
Serial.print(F("stir fan "));
Serial.println(winner);
fanchars_.reset();
return true;
}
void stopStepResponse() {
stepresp_.abort();
writeFan(0);
}
bool isStepResponseActive() const { return stepresp_.isActive(); }
uint32_t stepResponseElapsedMs(uint32_t nowMs) const { return stepresp_.elapsedMs(nowMs); }
const char *stepResponsePhaseName() const { return stepresp_.phaseName(); }
uint8_t stepResponseStepIndex() const { return stepresp_.stepIndex(); }
uint8_t stepResponseStepCount() const { return stepresp_.stepCount(); }
float stepResponseHeaterPct() const { return stepresp_.heaterPct(); }
void logStepResponseIfDue(const float *sensorTemps, const bool *sensorValid, uint8_t sensorCount,
float avgTempC, float minTempC, float maxTempC, float spreadC,
uint32_t nowMs) {
stepresp_.logIfDue(sensorTemps, sensorValid, sensorCount, avgTempC, minTempC, maxTempC,
spreadC, nowMs);
}
bool commitAutotuneIfDone() {
if (autotuner_.phase() != PidAutotuner::Phase::Done) {
return false;
@@ -331,8 +355,8 @@ public:
return "corner";
case HeaterBlock::Autotune:
return "autotune";
case HeaterBlock::StepResp:
return "stepresp";
case HeaterBlock::FanChars:
return "fanchars";
default:
return "none";
}
@@ -352,8 +376,8 @@ public:
SPREAD_EMA_ALPHA * cornerSpreadC_ +
(1.0f - SPREAD_EMA_ALPHA) * cornerSpreadC;
if (stepresp_.isActive()) {
updateStepResponse(avgTempC, maxTempC, nowMs);
if (fanchars_.isActive()) {
updateFanCharacterize(avgTempC, maxTempC, nowMs);
return;
}
@@ -386,7 +410,7 @@ public:
heatPi_.reset();
mixPi_.reset();
autotuner_.abort();
stepresp_.abort();
fanchars_.abort();
}
void forceHeaterOff() {
@@ -425,22 +449,17 @@ public:
}
private:
void updateStepResponse(float avgTempC, float maxTempC, uint32_t nowMs) {
void updateFanCharacterize(float avgTempC, float maxTempC, uint32_t nowMs) {
float duty = 0.0f;
uint8_t fan = 0;
stepresp_.update(avgTempC, maxTempC, cornerSpreadC_, nowMs, duty, fan);
fanchars_.update(avgTempC, maxTempC, cornerSpreadC_, nowMs, duty, fan);
heaterDutyPercent_ = duty;
heaterAllowancePercent_ = duty;
heaterBlock_ = duty > 0.0f ? HeaterBlock::StepResp : HeaterBlock::None;
heaterBlock_ = duty > 0.0f ? HeaterBlock::FanChars : HeaterBlock::None;
applyHeaterBurst(nowMs);
writeFan(fan);
lastHeaterUpdateMs_ = nowMs;
if (stepresp_.phase() == FanStepResponse::Phase::Done ||
stepresp_.phase() == FanStepResponse::Phase::Failed) {
stepresp_.reset();
}
}
void updateAutotune(float avgTempC, float maxTempC, uint32_t nowMs) {
@@ -504,13 +523,28 @@ private:
}
heaterDutyPercent_ = applyHeaterRamp(duty, avgTempC, nowMs);
const float mixInput = SPREAD_TARGET_C - cornerSpreadC_;
float fanOut = mixPi_.compute(mixInput, nowMs);
uint8_t fanPwm = static_cast<uint8_t>(fanOut + 0.5f);
if (avgTempC < targetTempC_ - HEAT_UP_BAND_C && fanPwm > FAN_COLD_CAP_PWM) {
fanPwm = FAN_COLD_CAP_PWM;
uint8_t fanPwm = stirFanPwm_;
if (avgTempC < targetTempC_) {
mixPi_.reset();
} else {
const float mixInput = SPREAD_TARGET_C - cornerSpreadC_;
float fanOut = mixPi_.compute(mixInput, nowMs);
fanPwm = static_cast<uint8_t>(fanOut + 0.5f);
if (fanPwm > FAN_MIX_MAX_PWM) {
fanPwm = FAN_MIX_MAX_PWM;
}
}
regulatingFanPwm_ = fanPwm;
regulatingFanPwm_ = fanWithMinStir(avgTempC, fanPwm);
}
uint8_t fanWithMinStir(float avgTempC, uint8_t pwm) const {
if (avgTempC < IDLE_AUTO_FAN_OFF_TEMP_C) {
return 0;
}
if (pwm < stirFanPwm_) {
return stirFanPwm_;
}
return pwm;
}
static float clampPercent(float value) {
@@ -633,13 +667,14 @@ private:
PidController heatPi_;
PidController mixPi_;
PidAutotuner autotuner_;
FanStepResponse stepresp_;
FanCharacterize fanchars_;
float targetTempC_;
float heaterDutyPercent_;
float heaterAllowancePercent_;
float cornerSpreadC_;
float lastMaxTempC_;
uint8_t regulatingFanPwm_;
uint8_t stirFanPwm_;
uint8_t fanPwm_;
bool tuningLoaded_;
bool fanIdleOverride_;