Compare commits
2 Commits
f8f5167af5
...
3ab1b3e8fc
| Author | SHA1 | Date | |
|---|---|---|---|
| 3ab1b3e8fc | |||
| 0e4a30adf7 |
@@ -51,6 +51,7 @@ static const float APPROACH_BAND_C = 4.0f;
|
|||||||
// When spread is good, allow hottest corner slightly above target so avg can reach setpoint
|
// 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 GOOD_SPREAD_C = 5.0f;
|
||||||
static const float BALANCED_MAX_ABOVE_TARGET_C = 2.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;
|
static const uint16_t HEATER_CYCLE_MS = 3000;
|
||||||
|
|
||||||
@@ -71,10 +72,13 @@ static const float SPREAD_EMA_ALPHA = 0.45f;
|
|||||||
// PID auto-tune (relay method) — run with: autotune 45
|
// PID auto-tune (relay method) — run with: autotune 45
|
||||||
static const float AUTOTUNE_HYSTERESIS_C = 0.4f;
|
static const float AUTOTUNE_HYSTERESIS_C = 0.4f;
|
||||||
static const float AUTOTUNE_PREHEAT_BAND_C = 5.0f;
|
static const float AUTOTUNE_PREHEAT_BAND_C = 5.0f;
|
||||||
static const float AUTOTUNE_PREHEAT_DUTY = 80.0f;
|
static const float AUTOTUNE_PREHEAT_DUTY = 100.0f;
|
||||||
|
static const uint8_t AUTOTUNE_PREHEAT_FAN_PWM = 70; // light mixing only while preheating
|
||||||
static const float AUTOTUNE_ABORT_ABOVE_C = 15.0f;
|
static const float AUTOTUNE_ABORT_ABOVE_C = 15.0f;
|
||||||
static const uint8_t AUTOTUNE_CYCLES_REQUIRED = 6;
|
static const uint8_t AUTOTUNE_CYCLES_REQUIRED = 6;
|
||||||
static const uint32_t AUTOTUNE_TIMEOUT_MS = 1800000UL;
|
static const uint32_t AUTOTUNE_PREHEAT_TIMEOUT_MS = 1200000UL; // 20 min
|
||||||
|
static const uint32_t AUTOTUNE_SESSION_TIMEOUT_MS = 3600000UL; // 60 min total
|
||||||
|
static const uint32_t AUTOTUNE_RELAY_PERIOD_MAX_MS = 2400000UL; // count periods up to 40 min
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Timing
|
// Timing
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ public:
|
|||||||
spreadSamples_(0),
|
spreadSamples_(0),
|
||||||
cycleCount_(0),
|
cycleCount_(0),
|
||||||
aboveSetpoint_(false),
|
aboveSetpoint_(false),
|
||||||
|
sessionStartMs_(0),
|
||||||
phaseStartMs_(0),
|
phaseStartMs_(0),
|
||||||
resultKp_(PID_KP),
|
resultKp_(PID_KP),
|
||||||
resultKi_(PID_KI),
|
resultKi_(PID_KI),
|
||||||
@@ -34,6 +35,29 @@ public:
|
|||||||
|
|
||||||
bool isActive() const { return phase_ == Phase::Preheat || phase_ == Phase::Relay; }
|
bool isActive() const { return phase_ == Phase::Preheat || phase_ == Phase::Relay; }
|
||||||
|
|
||||||
|
uint32_t elapsedMs(uint32_t nowMs) const {
|
||||||
|
if (sessionStartMs_ == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return nowMs - sessionStartMs_;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t cycleCount() const { return cycleCount_; }
|
||||||
|
uint8_t periodCount() const { return periodCount_; }
|
||||||
|
|
||||||
|
float preheatTargetC() const { return setpointC_ - AUTOTUNE_PREHEAT_BAND_C; }
|
||||||
|
|
||||||
|
const char *phaseName() const {
|
||||||
|
switch (phase_) {
|
||||||
|
case Phase::Preheat:
|
||||||
|
return "preheat";
|
||||||
|
case Phase::Relay:
|
||||||
|
return "relay";
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool start(float setpointC) {
|
bool start(float setpointC) {
|
||||||
if (setpointC < 25.0f || setpointC > TARGET_MAX_C) {
|
if (setpointC < 25.0f || setpointC > TARGET_MAX_C) {
|
||||||
return false;
|
return false;
|
||||||
@@ -44,10 +68,13 @@ public:
|
|||||||
relayLow_ = setpointC - AUTOTUNE_HYSTERESIS_C;
|
relayLow_ = setpointC - AUTOTUNE_HYSTERESIS_C;
|
||||||
resetMeasurements();
|
resetMeasurements();
|
||||||
phase_ = Phase::Preheat;
|
phase_ = Phase::Preheat;
|
||||||
phaseStartMs_ = millis();
|
sessionStartMs_ = millis();
|
||||||
|
phaseStartMs_ = sessionStartMs_;
|
||||||
Serial.print(F("autotune: preheat to "));
|
Serial.print(F("autotune: preheat to "));
|
||||||
|
Serial.print(preheatTargetC(), 1);
|
||||||
|
Serial.print(F("-"));
|
||||||
Serial.print(setpointC_, 1);
|
Serial.print(setpointC_, 1);
|
||||||
Serial.println(F("C"));
|
Serial.println(F("C avg"));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,9 +83,13 @@ public:
|
|||||||
Serial.println(F("autotune: cancelled"));
|
Serial.println(F("autotune: cancelled"));
|
||||||
}
|
}
|
||||||
phase_ = Phase::Idle;
|
phase_ = Phase::Idle;
|
||||||
|
sessionStartMs_ = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void reset() { phase_ = Phase::Idle; }
|
void reset() {
|
||||||
|
phase_ = Phase::Idle;
|
||||||
|
sessionStartMs_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
float setpoint() const { return setpointC_; }
|
float setpoint() const { return setpointC_; }
|
||||||
|
|
||||||
@@ -81,13 +112,23 @@ public:
|
|||||||
return phase_;
|
return phase_;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nowMs - phaseStartMs_ > AUTOTUNE_TIMEOUT_MS) {
|
if (nowMs - sessionStartMs_ > AUTOTUNE_SESSION_TIMEOUT_MS) {
|
||||||
fail(F("autotune: abort — timeout"));
|
fail(F("autotune: abort — session timeout"));
|
||||||
return phase_;
|
return phase_;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (phase_ == Phase::Preheat) {
|
if (phase_ == Phase::Preheat) {
|
||||||
if (avgTempC >= setpointC_ - AUTOTUNE_PREHEAT_BAND_C) {
|
fanPwmOut = AUTOTUNE_PREHEAT_FAN_PWM;
|
||||||
|
if (nowMs - phaseStartMs_ > AUTOTUNE_PREHEAT_TIMEOUT_MS) {
|
||||||
|
Serial.print(F("autotune: preheat failed — avg "));
|
||||||
|
Serial.print(avgTempC, 1);
|
||||||
|
Serial.print(F("C after "));
|
||||||
|
Serial.print((nowMs - sessionStartMs_) / 60000UL);
|
||||||
|
Serial.println(F(" min"));
|
||||||
|
fail(F("autotune: abort — preheat timeout"));
|
||||||
|
return phase_;
|
||||||
|
}
|
||||||
|
if (avgTempC >= preheatTargetC()) {
|
||||||
enterRelay(avgTempC, nowMs);
|
enterRelay(avgTempC, nowMs);
|
||||||
} else {
|
} else {
|
||||||
heaterDutyOut = AUTOTUNE_PREHEAT_DUTY;
|
heaterDutyOut = AUTOTUNE_PREHEAT_DUTY;
|
||||||
@@ -132,7 +173,9 @@ private:
|
|||||||
peakSinceCross_ = avgTempC;
|
peakSinceCross_ = avgTempC;
|
||||||
valleySinceCross_ = avgTempC;
|
valleySinceCross_ = avgTempC;
|
||||||
lastCrossMs_ = 0;
|
lastCrossMs_ = 0;
|
||||||
Serial.println(F("autotune: relay test started"));
|
Serial.print(F("autotune: relay test started ("));
|
||||||
|
Serial.print((nowMs - sessionStartMs_) / 1000UL);
|
||||||
|
Serial.println(F("s preheat)"));
|
||||||
}
|
}
|
||||||
|
|
||||||
void resetMeasurements() {
|
void resetMeasurements() {
|
||||||
@@ -158,13 +201,18 @@ private:
|
|||||||
|
|
||||||
Serial.print(F("autotune: cycle "));
|
Serial.print(F("autotune: cycle "));
|
||||||
Serial.print(cycleCount_);
|
Serial.print(cycleCount_);
|
||||||
|
Serial.print(F("/"));
|
||||||
|
Serial.print(AUTOTUNE_CYCLES_REQUIRED);
|
||||||
Serial.print(F(" amp="));
|
Serial.print(F(" amp="));
|
||||||
Serial.println(amplitude, 2);
|
Serial.print(amplitude, 2);
|
||||||
|
Serial.print(F("C elapsed="));
|
||||||
|
Serial.print((nowMs - sessionStartMs_) / 1000UL);
|
||||||
|
Serial.println(F("s"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastCrossMs_ > 0) {
|
if (lastCrossMs_ > 0) {
|
||||||
const uint32_t period = nowMs - lastCrossMs_;
|
const uint32_t period = nowMs - lastCrossMs_;
|
||||||
if (period > 8000 && period < 900000) {
|
if (period > 8000 && period < AUTOTUNE_RELAY_PERIOD_MAX_MS) {
|
||||||
periodSumMs_ += period;
|
periodSumMs_ += period;
|
||||||
++periodCount_;
|
++periodCount_;
|
||||||
}
|
}
|
||||||
@@ -173,11 +221,11 @@ private:
|
|||||||
peakSinceCross_ = valleySinceCross_;
|
peakSinceCross_ = valleySinceCross_;
|
||||||
|
|
||||||
if (cycleCount_ >= AUTOTUNE_CYCLES_REQUIRED && periodCount_ >= 3 && amplitudeCount_ >= 3) {
|
if (cycleCount_ >= AUTOTUNE_CYCLES_REQUIRED && periodCount_ >= 3 && amplitudeCount_ >= 3) {
|
||||||
finish();
|
finish(nowMs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void finish() {
|
void finish(uint32_t nowMs) {
|
||||||
const float avgPeriodSec =
|
const float avgPeriodSec =
|
||||||
static_cast<float>(periodSumMs_ / periodCount_) / 1000.0f;
|
static_cast<float>(periodSumMs_ / periodCount_) / 1000.0f;
|
||||||
const float avgAmplitude = amplitudeSum_ / static_cast<float>(amplitudeCount_);
|
const float avgAmplitude = amplitudeSum_ / static_cast<float>(amplitudeCount_);
|
||||||
@@ -212,7 +260,9 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
phase_ = Phase::Done;
|
phase_ = Phase::Done;
|
||||||
Serial.println(F("autotune: done"));
|
Serial.print(F("autotune: done in "));
|
||||||
|
Serial.print((nowMs - sessionStartMs_) / 1000UL);
|
||||||
|
Serial.println(F("s"));
|
||||||
Serial.print(F(" Kp="));
|
Serial.print(F(" Kp="));
|
||||||
Serial.print(resultKp_, 3);
|
Serial.print(resultKp_, 3);
|
||||||
Serial.print(F(" Ki="));
|
Serial.print(F(" Ki="));
|
||||||
@@ -226,6 +276,7 @@ private:
|
|||||||
void fail(const __FlashStringHelper *reason) {
|
void fail(const __FlashStringHelper *reason) {
|
||||||
Serial.println(reason);
|
Serial.println(reason);
|
||||||
phase_ = Phase::Failed;
|
phase_ = Phase::Failed;
|
||||||
|
sessionStartMs_ = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
Phase phase_;
|
Phase phase_;
|
||||||
@@ -243,6 +294,7 @@ private:
|
|||||||
uint16_t spreadSamples_;
|
uint16_t spreadSamples_;
|
||||||
uint8_t cycleCount_;
|
uint8_t cycleCount_;
|
||||||
bool aboveSetpoint_;
|
bool aboveSetpoint_;
|
||||||
|
uint32_t sessionStartMs_;
|
||||||
uint32_t phaseStartMs_;
|
uint32_t phaseStartMs_;
|
||||||
float resultKp_;
|
float resultKp_;
|
||||||
float resultKi_;
|
float resultKi_;
|
||||||
|
|||||||
@@ -111,6 +111,16 @@ public:
|
|||||||
|
|
||||||
bool isAutotuning() const { return autotuner_.isActive(); }
|
bool isAutotuning() const { return autotuner_.isActive(); }
|
||||||
|
|
||||||
|
uint32_t autotuneElapsedMs(uint32_t nowMs) const { return autotuner_.elapsedMs(nowMs); }
|
||||||
|
|
||||||
|
const char *autotunePhaseName() const { return autotuner_.phaseName(); }
|
||||||
|
|
||||||
|
uint8_t autotuneCycleCount() const { return autotuner_.cycleCount(); }
|
||||||
|
|
||||||
|
uint8_t autotunePeriodCount() const { return autotuner_.periodCount(); }
|
||||||
|
|
||||||
|
float autotunePreheatTargetC() const { return autotuner_.preheatTargetC(); }
|
||||||
|
|
||||||
bool commitAutotuneIfDone() {
|
bool commitAutotuneIfDone() {
|
||||||
if (autotuner_.phase() != PidAutotuner::Phase::Done) {
|
if (autotuner_.phase() != PidAutotuner::Phase::Done) {
|
||||||
return false;
|
return false;
|
||||||
@@ -368,12 +378,24 @@ private:
|
|||||||
bool isBalancedChamber() const { return cornerSpreadC_ <= GOOD_SPREAD_C; }
|
bool isBalancedChamber() const { return cornerSpreadC_ <= GOOD_SPREAD_C; }
|
||||||
|
|
||||||
float maxHeatStopTemp(float avgTempC) const {
|
float maxHeatStopTemp(float avgTempC) const {
|
||||||
if (isBalancedChamber() && avgTempC < targetTempC_) {
|
if (avgTempC >= targetTempC_) {
|
||||||
return targetTempC_ + BALANCED_MAX_ABOVE_TARGET_C;
|
|
||||||
}
|
|
||||||
return targetTempC_;
|
return targetTempC_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float stopAt = targetTempC_;
|
||||||
|
if (isBalancedChamber()) {
|
||||||
|
stopAt = targetTempC_ + BALANCED_MAX_ABOVE_TARGET_C;
|
||||||
|
} else {
|
||||||
|
stopAt = targetTempC_ + cornerSpreadC_ * SPREAD_HEADROOM_FACTOR + 1.0f;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float belowCutoff = cutoffThreshold() - 0.1f;
|
||||||
|
if (stopAt > belowCutoff) {
|
||||||
|
stopAt = belowCutoff;
|
||||||
|
}
|
||||||
|
return stopAt;
|
||||||
|
}
|
||||||
|
|
||||||
float allowanceFromMaxCorner(float maxTempC, float avgTempC) const {
|
float allowanceFromMaxCorner(float maxTempC, float avgTempC) const {
|
||||||
if (isBalancedChamber() && avgTempC < targetTempC_) {
|
if (isBalancedChamber() && avgTempC < targetTempC_) {
|
||||||
return 100.0f;
|
return 100.0f;
|
||||||
|
|||||||
14
src/main.cpp
14
src/main.cpp
@@ -138,7 +138,17 @@ void printStatus(float avgTemp, float minTemp, float maxTemp) {
|
|||||||
Serial.print(thermal.isFailSafeActive() ? F("YES") : F("no"));
|
Serial.print(thermal.isFailSafeActive() ? F("YES") : F("no"));
|
||||||
Serial.print(F(" mode="));
|
Serial.print(F(" mode="));
|
||||||
if (thermal.isAutotuning()) {
|
if (thermal.isAutotuning()) {
|
||||||
Serial.print(F("autotune"));
|
Serial.print(F("autotune/"));
|
||||||
|
Serial.print(thermal.autotunePhaseName());
|
||||||
|
Serial.print(F(" "));
|
||||||
|
Serial.print(thermal.autotuneElapsedMs(millis()) / 1000UL);
|
||||||
|
Serial.print(F("s "));
|
||||||
|
Serial.print(thermal.autotuneCycleCount());
|
||||||
|
Serial.print(F("/"));
|
||||||
|
Serial.print(AUTOTUNE_CYCLES_REQUIRED);
|
||||||
|
Serial.print(F("cyc pre>="));
|
||||||
|
Serial.print(thermal.autotunePreheatTargetC(), 0);
|
||||||
|
Serial.print(F("C"));
|
||||||
} else if (thermal.isAdaptive()) {
|
} else if (thermal.isAdaptive()) {
|
||||||
Serial.print(F("learned"));
|
Serial.print(F("learned"));
|
||||||
} else {
|
} else {
|
||||||
@@ -284,7 +294,7 @@ void processSerialLine(const char *line) {
|
|||||||
Serial.println(F("ERR autotune already running"));
|
Serial.println(F("ERR autotune already running"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Serial.println(F("OK autotune started — keep chamber closed, wait ~10-20 min"));
|
Serial.println(F("OK autotune started — preheat then relay, typically 15-40 min"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user