🚸 Improved MPCTEMP autotune (#25503)

This commit is contained in:
StevilKnevil 2023-05-12 01:09:02 +01:00 committed by GitHub
parent 8fb9b5804e
commit 01f5bd3330
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 425 additions and 221 deletions

View file

@ -650,13 +650,17 @@
// @section hotend temp
// Enable PIDTEMP for PID control or MPCTEMP for Predictive Model.
// temperature control. Disable both for bang-bang heating.
/**
* Temperature Control
*
* (NONE) : Bang-bang heating
* PIDTEMP : PID temperature control (~4.1K)
* MPCTEMP : Predictive Model temperature control. (~1.8K without auto-tune)
*/
#define PIDTEMP // See the PID Tuning Guide at https://reprap.org/wiki/PID_Tuning
//#define MPCTEMP // ** EXPERIMENTAL **
//#define MPCTEMP // ** EXPERIMENTAL ** See https://marlinfw.org/docs/features/model_predictive_control.html
#define BANG_MAX 255 // Limits current to nozzle while in bang-bang mode; 255=full current
#define PID_MAX BANG_MAX // Limits current to nozzle while PID is active (see PID_FUNCTIONAL_RANGE below); 255=full current
#define PID_MAX 255 // Limit hotend current while PID is active (see PID_FUNCTIONAL_RANGE below); 255=full current
#define PID_K1 0.95 // Smoothing factor within any PID loop
#if ENABLED(PIDTEMP)
@ -675,6 +679,8 @@
#define DEFAULT_Ki 1.08
#define DEFAULT_Kd 114.00
#endif
#else
#define BANG_MAX 255 // Limit hotend current while in bang-bang mode; 255=full current
#endif
/**
@ -686,11 +692,11 @@
* @section mpctemp
*/
#if ENABLED(MPCTEMP)
//#define MPC_AUTOTUNE // Include a method to do MPC auto-tuning (~5664-5882 bytes of flash)
//#define MPC_EDIT_MENU // Add MPC editing to the "Advanced Settings" menu. (~1300 bytes of flash)
//#define MPC_AUTOTUNE // Include a method to do MPC auto-tuning (~6.3K bytes of flash)
//#define MPC_EDIT_MENU // Add MPC editing to the "Advanced Settings" menu. (~1.3K bytes of flash)
//#define MPC_AUTOTUNE_MENU // Add MPC auto-tuning to the "Advanced Settings" menu. (~350 bytes of flash)
#define MPC_MAX BANG_MAX // (0..255) Current to nozzle while MPC is active.
#define MPC_MAX 255 // (0..255) Current to nozzle while MPC is active.
#define MPC_HEATER_POWER { 40.0f } // (W) Heat cartridge powers.
#define MPC_INCLUDE_FAN // Model the fan speed?
@ -725,23 +731,7 @@
//====================== PID > Bed Temperature Control ======================
//===========================================================================
/**
* PID Bed Heating
*
* If this option is enabled set PID constants below.
* If this option is disabled, bang-bang will be used and BED_LIMIT_SWITCHING will enable hysteresis.
*
* The PID frequency will be the same as the extruder PWM.
* If PID_dT is the default, and correct for the hardware/configuration, that means 7.689Hz,
* which is fine for driving a square wave into a resistive load and does not significantly
* impact FET heating. This also works fine on a Fotek SSR-10DA Solid State Relay into a 250W
* heater. If your configuration is significantly different than this and you don't understand
* the issues involved, don't use bed PID until someone else verifies that your hardware works.
* @section bed temp
*/
//#define PIDTEMPBED
//#define BED_LIMIT_SWITCHING
// @section bed temp
/**
* Max Bed Power
@ -751,6 +741,20 @@
*/
#define MAX_BED_POWER 255 // limits duty cycle to bed; 255=full current
/**
* PID Bed Heating
*
* The PID frequency will be the same as the extruder PWM.
* If PID_dT is the default, and correct for the hardware/configuration, that means 7.689Hz,
* which is fine for driving a square wave into a resistive load and does not significantly
* impact FET heating. This also works fine on a Fotek SSR-10DA Solid State Relay into a 250W
* heater. If your configuration is significantly different than this and you don't understand
* the issues involved, don't use bed PID until someone else verifies that your hardware works.
*
* With this option disabled, bang-bang will be used. BED_LIMIT_SWITCHING enables hysteresis.
*/
//#define PIDTEMPBED
#if ENABLED(PIDTEMPBED)
//#define MIN_BED_POWER 0
//#define PID_BED_DEBUG // Print Bed PID debug data to the serial port.
@ -762,7 +766,9 @@
#define DEFAULT_bedKd 305.4
// FIND YOUR OWN: "M303 E-1 C8 S90" to run autotune on the bed at 90 degreesC for 8 cycles.
#endif // PIDTEMPBED
#else
//#define BED_LIMIT_SWITCHING // Keep the bed temperature within BED_HYSTERESIS of the target
#endif
//===========================================================================
//==================== PID > Chamber Temperature Control ====================

View file

@ -28,6 +28,7 @@ typedef uint32_t millis_t;
#define SEC_TO_MS(N) millis_t((N)*1000UL)
#define MIN_TO_MS(N) SEC_TO_MS((N)*60UL)
#define MS_TO_SEC(N) millis_t((N)/1000UL)
#define MS_TO_SEC_PRECISE(N) (float(N)/1000.0f)
#define PENDING(NOW,SOON) ((int32_t)(NOW-(SOON))<0)
#define ELAPSED(NOW,SOON) (!PENDING(NOW,SOON))

View file

@ -42,7 +42,10 @@
* R<kelvin/second/kelvin> Sensor responsiveness (= transfer coefficient / heat capcity).
*
* With MPC_AUTOTUNE:
* T Autotune the specified or active extruder.
* T Autotune the extruder specified with 'E' or the active extruder.
* S0 : Autotuning method AUTO (default)
* S1 : Autotuning method DIFFERENTIAL
* S2 : Autotuning method ASYMPTOTIC
*/
void GcodeSuite::M306() {
@ -54,8 +57,15 @@ void GcodeSuite::M306() {
#if ENABLED(MPC_AUTOTUNE)
if (parser.seen_test('T')) {
Temperature::MPCTuningType tuning_type;
const uint8_t type = parser.byteval('S', 0);
switch (type) {
case 1: tuning_type = Temperature::MPCTuningType::FORCE_DIFFERENTIAL; break;
case 2: tuning_type = Temperature::MPCTuningType::FORCE_ASYMPTOTIC; break;
default: tuning_type = Temperature::MPCTuningType::AUTO; break;
}
LCD_MESSAGE(MSG_MPC_AUTOTUNE);
thermalManager.MPC_autotune(e);
thermalManager.MPC_autotune(e, tuning_type);
ui.reset_status();
return;
}

View file

@ -722,16 +722,14 @@ volatile bool Temperature::raw_temps_ready = false;
TERN_(DWIN_PID_TUNE, DWIN_PidTuning(isbed ? PIDTEMPBED_START : PIDTEMP_START));
if (target > GHV(CHAMBER_MAX_TARGET, BED_MAX_TARGET, temp_range[heater_id].maxtemp - (HOTEND_OVERSHOOT))) {
SERIAL_ECHOPGM(STR_PID_AUTOTUNE);
SERIAL_ECHOLNPGM(STR_PID_TEMP_TOO_HIGH);
SERIAL_ECHOPGM(STR_PID_AUTOTUNE); SERIAL_ECHOLNPGM(STR_PID_TEMP_TOO_HIGH);
TERN_(EXTENSIBLE_UI, ExtUI::onPidTuning(ExtUI::result_t::PID_TEMP_TOO_HIGH));
TERN_(DWIN_PID_TUNE, DWIN_PidTuning(PID_TEMP_TOO_HIGH));
TERN_(HOST_PROMPT_SUPPORT, hostui.notify(GET_TEXT_F(MSG_PID_TEMP_TOO_HIGH)));
return;
}
SERIAL_ECHOPGM(STR_PID_AUTOTUNE);
SERIAL_ECHOLNPGM(STR_PID_AUTOTUNE_START);
SERIAL_ECHOPGM(STR_PID_AUTOTUNE); SERIAL_ECHOLNPGM(STR_PID_AUTOTUNE_START);
disable_all_heaters();
TERN_(AUTO_POWER_CONTROL, powerManager.power_on());
@ -816,8 +814,7 @@ volatile bool Temperature::raw_temps_ready = false;
#define MAX_OVERSHOOT_PID_AUTOTUNE 30
#endif
if (current_temp > target + MAX_OVERSHOOT_PID_AUTOTUNE) {
SERIAL_ECHOPGM(STR_PID_AUTOTUNE);
SERIAL_ECHOLNPGM(STR_PID_TEMP_TOO_HIGH);
SERIAL_ECHOPGM(STR_PID_AUTOTUNE); SERIAL_ECHOLNPGM(STR_PID_TEMP_TOO_HIGH);
TERN_(EXTENSIBLE_UI, ExtUI::onPidTuning(ExtUI::result_t::PID_TEMP_TOO_HIGH));
TERN_(DWIN_PID_TUNE, DWIN_PidTuning(PID_TEMP_TOO_HIGH));
TERN_(HOST_PROMPT_SUPPORT, hostui.notify(GET_TEXT_F(MSG_PID_TEMP_TOO_HIGH)));
@ -859,14 +856,12 @@ volatile bool Temperature::raw_temps_ready = false;
TERN_(DWIN_PID_TUNE, DWIN_PidTuning(PID_TUNING_TIMEOUT));
TERN_(EXTENSIBLE_UI, ExtUI::onPidTuning(ExtUI::result_t::PID_TUNING_TIMEOUT));
TERN_(HOST_PROMPT_SUPPORT, hostui.notify(GET_TEXT_F(MSG_PID_TIMEOUT)));
SERIAL_ECHOPGM(STR_PID_AUTOTUNE);
SERIAL_ECHOLNPGM(STR_PID_TIMEOUT);
SERIAL_ECHOPGM(STR_PID_AUTOTUNE); SERIAL_ECHOLNPGM(STR_PID_TIMEOUT);
break;
}
if (cycles > ncycles && cycles > 2) {
SERIAL_ECHOPGM(STR_PID_AUTOTUNE);
SERIAL_ECHOLNPGM(STR_PID_AUTOTUNE_FINISHED);
SERIAL_ECHOPGM(STR_PID_AUTOTUNE); SERIAL_ECHOLNPGM(STR_PID_AUTOTUNE_FINISHED);
TERN_(HOST_PROMPT_SUPPORT, hostui.notify(GET_TEXT_F(MSG_PID_AUTOTUNE_DONE)));
#if EITHER(PIDTEMPBED, PIDTEMPCHAMBER)
@ -944,38 +939,26 @@ volatile bool Temperature::raw_temps_ready = false;
#define SINGLEFAN 1
#endif
void Temperature::MPC_autotune(const uint8_t e) {
auto housekeeping = [] (millis_t &ms, const uint8_t e, celsius_float_t &current_temp, millis_t &next_report_ms) {
ms = millis();
#define DEBUG_MPC_AUTOTUNE 1
if (updateTemperaturesIfReady()) { // temp sample ready
current_temp = degHotend(e);
TERN_(HAS_FAN_LOGIC, manage_extruder_fans(ms));
millis_t Temperature::MPC_autotuner::curr_time_ms, Temperature::MPC_autotuner::next_report_ms;
celsius_float_t Temperature::MPC_autotuner::temp_samples[16];
uint8_t Temperature::MPC_autotuner::sample_count;
uint16_t Temperature::MPC_autotuner::sample_distance;
// Parameters from differential analysis
celsius_float_t Temperature::MPC_autotuner::temp_fastest;
#if HAS_FAN
float Temperature::MPC_autotuner::power_fan255;
#endif
Temperature::MPC_autotuner::MPC_autotuner(const uint8_t extruderIdx) : e(extruderIdx) {
TERN_(TEMP_TUNING_MAINTAIN_FAN, adaptive_fan_slowing = false);
}
if (ELAPSED(ms, next_report_ms)) {
next_report_ms += 1000UL;
print_heater_states(e);
SERIAL_EOL();
}
hal.idletask();
TERN(DWIN_CREALITY_LCD, DWIN_Update(), ui.update());
if (!wait_for_heatup) {
SERIAL_ECHOLNPGM(STR_MPC_AUTOTUNE_INTERRUPTED);
TERN_(DWIN_LCD_PROUI, DWIN_MPCTuning(MPC_INTERRUPTED));
return true;
}
return false;
};
struct OnExit {
uint8_t e;
OnExit(const uint8_t _e) { this->e = _e; }
~OnExit() {
Temperature::MPC_autotuner::~MPC_autotuner() {
wait_for_heatup = false;
ui.reset_status();
@ -991,13 +974,231 @@ volatile bool Temperature::raw_temps_ready = false;
TERN_(TEMP_TUNING_MAINTAIN_FAN, adaptive_fan_slowing = true);
}
} on_exit(e);
SERIAL_ECHOLNPGM(STR_MPC_AUTOTUNE_START, e);
Temperature::MPC_autotuner::MeasurementState Temperature::MPC_autotuner::measure_ambient_temp() {
init_timers();
const millis_t test_interval_ms = 10000UL;
millis_t next_test_ms = curr_time_ms + test_interval_ms;
ambient_temp = current_temp = degHotend(e);
wait_for_heatup = true;
for (;;) { // Can be interrupted with M108
if (housekeeping() == CANCELLED) return CANCELLED;
if (ELAPSED(curr_time_ms, next_test_ms)) {
if (current_temp >= ambient_temp) {
ambient_temp = (ambient_temp + current_temp) / 2.0f;
break;
}
ambient_temp = current_temp;
next_test_ms += test_interval_ms;
}
}
wait_for_heatup = false;
#if ENABLED(DEBUG_MPC_AUTOTUNE)
SERIAL_ECHOLNPGM("MPC_autotuner::measure_ambient_temp() Completed");
SERIAL_ECHOLNPGM("=====");
SERIAL_ECHOLNPGM("ambient_temp ", get_ambient_temp());
#endif
return SUCCESS;
}
Temperature::MPC_autotuner::MeasurementState Temperature::MPC_autotuner::measure_heatup() {
init_timers();
constexpr millis_t test_interval_ms = 1000UL;
millis_t next_test_time_ms = curr_time_ms + test_interval_ms;
MPCHeaterInfo &hotend = temp_hotend[e];
current_temp = degHotend(e);
millis_t heat_start_time_ms = curr_time_ms;
sample_count = 0;
sample_distance = 1;
t1_time = 0;
hotend.target = 200.0f; // So M105 looks nice
hotend.soft_pwm_amount = (MPC_MAX) >> 1;
// Initialise rate of change to to steady state at current time
temp_samples[0] = temp_samples[1] = temp_samples[2] = current_temp;
time_fastest = rate_fastest = 0;
wait_for_heatup = true;
for (;;) { // Can be interrupted with M108
if (housekeeping() == CANCELLED) return CANCELLED;
if (ELAPSED(curr_time_ms, next_test_time_ms)) {
if (current_temp < 100.0f) {
// Initial regime (below 100deg): Measure rate of change of heating for differential tuning
// Update the buffer of previous readings
temp_samples[0] = temp_samples[1];
temp_samples[1] = temp_samples[2];
temp_samples[2] = current_temp;
// Measure the rate of change of temperature, https://en.wikipedia.org/wiki/Symmetric_derivative
const float h = MS_TO_SEC_PRECISE(test_interval_ms),
curr_rate = (temp_samples[2] - temp_samples[0]) / 2 * h;
if (curr_rate > rate_fastest) {
// Update fastest values
rate_fastest = curr_rate;
temp_fastest = temp_samples[1];
time_fastest = get_elapsed_heating_time();
}
next_test_time_ms += test_interval_ms;
}
else if (current_temp < 200.0f) {
// Second regime (after 100deg) measure 3 points to determine asymptotic temperature
// If there are too many samples, space them more widely
if (sample_count == COUNT(temp_samples)) {
for (uint8_t i = 0; i < COUNT(temp_samples) / 2; i++)
temp_samples[i] = temp_samples[i * 2];
sample_count /= 2;
sample_distance *= 2;
}
if (sample_count == 0) t1_time = MS_TO_SEC_PRECISE(curr_time_ms - heat_start_time_ms);
temp_samples[sample_count++] = current_temp;
if (current_temp >= 200.0f) break;
next_test_time_ms += test_interval_ms * sample_distance;
}
else {
// Third regime (after 200deg) finished gathering data so finish
break;
}
}
}
wait_for_heatup = false;
hotend.soft_pwm_amount = 0;
elapsed_heating_time = MS_TO_SEC_PRECISE(curr_time_ms - heat_start_time_ms);
// Ensure sample count is odd so that we have 3 equally spaced samples
if (sample_count == 0) return FAILED;
if (sample_count % 2 == 0) sample_count--;
#if ENABLED(DEBUG_MPC_AUTOTUNE)
SERIAL_ECHOLNPGM("MPC_autotuner::measure_heatup() Completed");
SERIAL_ECHOLNPGM("=====");
SERIAL_ECHOLNPGM("t1_time ", t1_time);
SERIAL_ECHOLNPGM("sample_count ", sample_count);
SERIAL_ECHOLNPGM("sample_distance ", sample_distance);
for (uint8_t i = 0; i < sample_count; i++)
SERIAL_ECHOLNPGM("sample ", i, " : ", temp_samples[i]);
SERIAL_ECHOLNPGM("t1 ", get_sample_1_temp(), " t2 ", get_sample_2_temp(), " t3 ", get_sample_3_temp());
#endif
return SUCCESS;
}
Temperature::MPC_autotuner::MeasurementState Temperature::MPC_autotuner::measure_transfer() {
init_timers();
const millis_t test_interval_ms = SEC_TO_MS(MPC_dT);
millis_t next_test_ms = curr_time_ms + test_interval_ms;
MPCHeaterInfo &hotend = temp_hotend[e];
MPC_t &mpc = hotend.mpc;
TERN_(TEMP_TUNING_MAINTAIN_FAN, adaptive_fan_slowing = false);
constexpr millis_t settle_time = 20000UL, test_duration = 20000UL;
millis_t settle_end_ms = curr_time_ms + settle_time,
test_end_ms = settle_end_ms + test_duration;
float total_energy_fan0 = 0.0f;
#if HAS_FAN
bool fan0_done = false;
float total_energy_fan255 = 0.0f;
#endif
float last_temp = current_temp;
wait_for_heatup = true;
for (;;) { // Can be interrupted with M108
if (housekeeping() == CANCELLED) return CANCELLED;
if (ELAPSED(curr_time_ms, next_test_ms)) {
hotend.soft_pwm_amount = (int)get_pid_output_hotend(e) >> 1;
if (ELAPSED(curr_time_ms, settle_end_ms) && !ELAPSED(curr_time_ms, test_end_ms) && TERN1(HAS_FAN, !fan0_done))
total_energy_fan0 += mpc.heater_power * hotend.soft_pwm_amount / 127 * MPC_dT + (last_temp - current_temp) * mpc.block_heat_capacity;
#if HAS_FAN
else if (ELAPSED(curr_time_ms, test_end_ms) && !fan0_done) {
set_fan_speed(TERN(SINGLEFAN, 0, e), 255);
planner.sync_fan_speeds(fan_speed);
settle_end_ms = curr_time_ms + settle_time;
test_end_ms = settle_end_ms + test_duration;
fan0_done = true;
}
else if (ELAPSED(curr_time_ms, settle_end_ms) && !ELAPSED(curr_time_ms, test_end_ms))
total_energy_fan255 += mpc.heater_power * hotend.soft_pwm_amount / 127 * MPC_dT + (last_temp - current_temp) * mpc.block_heat_capacity;
#endif
else if (ELAPSED(curr_time_ms, test_end_ms)) break;
last_temp = current_temp;
next_test_ms += test_interval_ms;
}
// Ensure we don't drift too far from the window between the last sampled temp and the target temperature
if (!WITHIN(current_temp, get_sample_3_temp() - 15.0f, hotend.target + 15.0f)) {
SERIAL_ECHOLNPGM(STR_MPC_TEMPERATURE_ERROR);
TERN_(DWIN_LCD_PROUI, DWIN_MPCTuning(MPC_TEMP_ERROR));
wait_for_heatup = false;
return FAILED;
}
}
wait_for_heatup = false;
power_fan0 = total_energy_fan0 / MS_TO_SEC_PRECISE(test_duration);
TERN_(HAS_FAN, power_fan255 = (total_energy_fan255 * 1000) / test_duration);
#if ENABLED(DEBUG_MPC_AUTOTUNE)
SERIAL_ECHOLNPGM("MPC_autotuner::measure_transfer() Completed");
SERIAL_ECHOLNPGM("=====");
SERIAL_ECHOLNPGM("power_fan0 ", power_fan0);
TERN_(HAS_FAN, SERIAL_ECHOLNPGM("power_fan255 ", power_fan255));
#endif
return SUCCESS;
}
Temperature::MPC_autotuner::MeasurementState Temperature::MPC_autotuner::housekeeping() {
const millis_t report_interval_ms = 1000UL;
curr_time_ms = millis();
if (updateTemperaturesIfReady()) { // temp sample ready
current_temp = degHotend(e);
TERN_(HAS_FAN_LOGIC, manage_extruder_fans(curr_time_ms));
}
if (ELAPSED(curr_time_ms, next_report_ms)) {
next_report_ms += report_interval_ms;
print_heater_states(e);
SERIAL_EOL();
}
hal.idletask();
TERN(DWIN_CREALITY_LCD, DWIN_Update(), ui.update());
if (!wait_for_heatup) {
SERIAL_ECHOLNPGM(STR_MPC_AUTOTUNE_INTERRUPTED);
TERN_(DWIN_LCD_PROUI, DWIN_MPCTuning(MPC_INTERRUPTED));
return MeasurementState::CANCELLED;
}
return MeasurementState::SUCCESS;
}
void Temperature::MPC_autotune(const uint8_t e, MPCTuningType tuning_type=AUTO) {
SERIAL_ECHOLNPGM(STR_MPC_AUTOTUNE_START, e);
MPC_autotuner tuner(e);
MPCHeaterInfo &hotend = temp_hotend[e];
MPC_t &mpc = hotend.mpc;
// Move to center of bed, just above bed height and cool with max fan
gcode.home_all_axes(true);
@ -1009,6 +1210,7 @@ volatile bool Temperature::raw_temps_ready = false;
#endif
do_blocking_move_to(xyz_pos_t(MPC_TUNING_POS));
// Determine ambient temperature.
SERIAL_ECHOLNPGM(STR_MPC_COOLING_TO_AMBIENT);
#if ENABLED(DWIN_LCD_PROUI)
DWIN_MPCTuning(MPCTEMP_START);
@ -1017,164 +1219,92 @@ volatile bool Temperature::raw_temps_ready = false;
LCD_MESSAGE(MSG_COOLING);
#endif
millis_t ms = millis(), next_report_ms = ms, next_test_ms = ms + 10000UL;
celsius_float_t current_temp = degHotend(e),
ambient_temp = current_temp;
wait_for_heatup = true;
for (;;) { // Can be interrupted with M108
if (housekeeping(ms, e, current_temp, next_report_ms)) return;
if (ELAPSED(ms, next_test_ms)) {
if (current_temp >= ambient_temp) {
ambient_temp = (ambient_temp + current_temp) / 2.0f;
break;
}
ambient_temp = current_temp;
next_test_ms += 10000UL;
}
}
wait_for_heatup = false;
if (tuner.measure_ambient_temp() != MPC_autotuner::MeasurementState::SUCCESS) return;
hotend.modeled_ambient_temp = tuner.get_ambient_temp();
#if HAS_FAN
set_fan_speed(TERN(SINGLEFAN, 0, e), 0);
planner.sync_fan_speeds(fan_speed);
#endif
hotend.modeled_ambient_temp = ambient_temp;
// Heat to 200 degrees
SERIAL_ECHOLNPGM(STR_MPC_HEATING_PAST_200);
TERN(DWIN_LCD_PROUI, LCD_ALERTMESSAGE(MSG_MPC_HEATING_PAST_200), LCD_MESSAGE(MSG_HEATING));
hotend.target = 200.0f; // So M105 looks nice
hotend.soft_pwm_amount = (MPC_MAX) >> 1;
const millis_t heat_start_time = next_test_ms = ms;
celsius_float_t temp_samples[16];
uint8_t sample_count = 0;
uint16_t sample_distance = 1;
float t1_time = 0;
wait_for_heatup = true;
for (;;) { // Can be interrupted with M108
if (housekeeping(ms, e, current_temp, next_report_ms)) return;
if (ELAPSED(ms, next_test_ms)) {
// Record samples between 100C and 200C
if (current_temp >= 100.0f) {
// If there are too many samples, space them more widely
if (sample_count == COUNT(temp_samples)) {
for (uint8_t i = 0; i < COUNT(temp_samples) / 2; i++)
temp_samples[i] = temp_samples[i*2];
sample_count /= 2;
sample_distance *= 2;
}
if (sample_count == 0) t1_time = float(ms - heat_start_time) / 1000.0f;
temp_samples[sample_count++] = current_temp;
}
if (current_temp >= 200.0f) break;
next_test_ms += 1000UL * sample_distance;
}
}
wait_for_heatup = false;
hotend.soft_pwm_amount = 0;
if (tuner.measure_heatup() != MPC_autotuner::MeasurementState::SUCCESS) return;
// Calculate physical constants from three equally-spaced samples
sample_count = (sample_count + 1) / 2 * 2 - 1;
const float t1 = temp_samples[0],
t2 = temp_samples[(sample_count - 1) >> 1],
t3 = temp_samples[sample_count - 1];
const float t1 = tuner.get_sample_1_temp(),
t2 = tuner.get_sample_2_temp(),
t3 = tuner.get_sample_3_temp();
float asymp_temp = (t2 * t2 - t1 * t3) / (2 * t2 - t1 - t3),
block_responsiveness = -log((t2 - asymp_temp) / (t1 - asymp_temp)) / (sample_distance * (sample_count >> 1));
block_responsiveness = -log((t2 - asymp_temp) / (t1 - asymp_temp)) / tuner.get_sample_interval();
mpc.ambient_xfer_coeff_fan0 = mpc.heater_power * (MPC_MAX) / 255 / (asymp_temp - ambient_temp);
mpc.block_heat_capacity = mpc.ambient_xfer_coeff_fan0 / block_responsiveness;
mpc.sensor_responsiveness = block_responsiveness / (1.0f - (ambient_temp - asymp_temp) * exp(-block_responsiveness * t1_time) / (t1 - asymp_temp));
#if ENABLED(DEBUG_MPC_AUTOTUNE)
SERIAL_ECHOLNPGM("asymp_temp ", asymp_temp);
SERIAL_ECHOLNPAIR_F("block_responsiveness ", block_responsiveness, 4);
#endif
// Make initial guess at transfer coefficients
mpc.ambient_xfer_coeff_fan0 = mpc.heater_power * (MPC_MAX) / 255 / (asymp_temp - tuner.get_ambient_temp());
TERN_(MPC_INCLUDE_FAN, mpc.fan255_adjustment = 0.0f);
hotend.modeled_block_temp = asymp_temp + (ambient_temp - asymp_temp) * exp(-block_responsiveness * (ms - heat_start_time) / 1000.0f);
hotend.modeled_sensor_temp = current_temp;
if (tuning_type == AUTO || tuning_type == FORCE_ASYMPTOTIC) {
// Analytic tuning
mpc.block_heat_capacity = mpc.ambient_xfer_coeff_fan0 / block_responsiveness;
mpc.sensor_responsiveness = block_responsiveness / (1.0f - (tuner.get_ambient_temp() - asymp_temp) * exp(-block_responsiveness * tuner.get_sample_1_time()) / (t1 - asymp_temp));
}
// If analytic tuning fails, fall back to differential tuning
if (tuning_type == AUTO) {
if (mpc.sensor_responsiveness <= 0 || mpc.block_heat_capacity <= 0)
tuning_type = FORCE_DIFFERENTIAL;
}
if (tuning_type == FORCE_DIFFERENTIAL) {
// Differential tuning
mpc.block_heat_capacity = mpc.heater_power / tuner.get_rate_fastest();
mpc.sensor_responsiveness = tuner.get_rate_fastest() / (tuner.get_rate_fastest() * tuner.get_time_fastest() + tuner.get_ambient_temp() - tuner.get_time_fastest());
}
hotend.modeled_block_temp = asymp_temp + (tuner.get_ambient_temp() - asymp_temp) * exp(-block_responsiveness * tuner.get_elapsed_heating_time());
hotend.modeled_sensor_temp = tuner.get_last_measured_temp();
// Allow the system to stabilize under MPC, then get a better measure of ambient loss with and without fan
SERIAL_ECHOLNPGM(STR_MPC_MEASURING_AMBIENT, hotend.modeled_block_temp);
TERN(DWIN_LCD_PROUI, LCD_ALERTMESSAGE(MSG_MPC_MEASURING_AMBIENT), LCD_MESSAGE(MSG_MPC_MEASURING_AMBIENT));
// Use the estimated overshoot of the temperature as the target to achieve.
hotend.target = hotend.modeled_block_temp;
next_test_ms = ms + MPC_dT * 1000;
constexpr millis_t settle_time = 20000UL, test_duration = 20000UL;
millis_t settle_end_ms = ms + settle_time,
test_end_ms = settle_end_ms + test_duration;
float total_energy_fan0 = 0.0f;
if (tuner.measure_transfer() != MPC_autotuner::MeasurementState::SUCCESS) return;
// Update the transfer coefficients
mpc.ambient_xfer_coeff_fan0 = tuner.get_power_fan0() / (hotend.target - tuner.get_ambient_temp());
#if HAS_FAN
bool fan0_done = false;
float total_energy_fan255 = 0.0f;
#endif
float last_temp = current_temp;
wait_for_heatup = true;
for (;;) { // Can be interrupted with M108
if (housekeeping(ms, e, current_temp, next_report_ms)) return;
if (ELAPSED(ms, next_test_ms)) {
hotend.soft_pwm_amount = (int)get_pid_output_hotend(e) >> 1;
if (ELAPSED(ms, settle_end_ms) && !ELAPSED(ms, test_end_ms) && TERN1(HAS_FAN, !fan0_done))
total_energy_fan0 += mpc.heater_power * hotend.soft_pwm_amount / 127 * MPC_dT + (last_temp - current_temp) * mpc.block_heat_capacity;
#if HAS_FAN
else if (ELAPSED(ms, test_end_ms) && !fan0_done) {
set_fan_speed(TERN(SINGLEFAN, 0, e), 255);
planner.sync_fan_speeds(fan_speed);
settle_end_ms = ms + settle_time;
test_end_ms = settle_end_ms + test_duration;
fan0_done = true;
}
else if (ELAPSED(ms, settle_end_ms) && !ELAPSED(ms, test_end_ms))
total_energy_fan255 += mpc.heater_power * hotend.soft_pwm_amount / 127 * MPC_dT + (last_temp - current_temp) * mpc.block_heat_capacity;
#endif
else if (ELAPSED(ms, test_end_ms)) break;
last_temp = current_temp;
next_test_ms += MPC_dT * 1000;
}
if (!WITHIN(current_temp, t3 - 15.0f, hotend.target + 15.0f)) {
SERIAL_ECHOLNPGM(STR_MPC_TEMPERATURE_ERROR);
TERN_(DWIN_LCD_PROUI, DWIN_MPCTuning(MPC_TEMP_ERROR));
break;
}
}
wait_for_heatup = false;
const float power_fan0 = total_energy_fan0 * 1000 / test_duration;
mpc.ambient_xfer_coeff_fan0 = power_fan0 / (hotend.target - ambient_temp);
#if HAS_FAN
const float power_fan255 = total_energy_fan255 * 1000 / test_duration,
ambient_xfer_coeff_fan255 = power_fan255 / (hotend.target - ambient_temp);
const float ambient_xfer_coeff_fan255 = tuner.get_power_fan255() / (hotend.target - tuner.get_ambient_temp());
mpc.applyFanAdjustment(ambient_xfer_coeff_fan255);
#endif
if (tuning_type == AUTO || tuning_type == FORCE_ASYMPTOTIC) {
// Calculate a new and better asymptotic temperature and re-evaluate the other constants
asymp_temp = ambient_temp + mpc.heater_power * (MPC_MAX) / 255 / mpc.ambient_xfer_coeff_fan0;
block_responsiveness = -log((t2 - asymp_temp) / (t1 - asymp_temp)) / (sample_distance * (sample_count >> 1));
mpc.block_heat_capacity = mpc.ambient_xfer_coeff_fan0 / block_responsiveness;
mpc.sensor_responsiveness = block_responsiveness / (1.0f - (ambient_temp - asymp_temp) * exp(-block_responsiveness * t1_time) / (t1 - asymp_temp));
asymp_temp = tuner.get_ambient_temp() + mpc.heater_power * (MPC_MAX) / 255 / mpc.ambient_xfer_coeff_fan0;
block_responsiveness = -log((t2 - asymp_temp) / (t1 - asymp_temp)) / tuner.get_sample_interval();
SERIAL_ECHOLNPGM(STR_MPC_AUTOTUNE_FINISHED);
TERN_(DWIN_LCD_PROUI, DWIN_MPCTuning(MPC_DONE));
#if 0
SERIAL_ECHOLNPGM("t1_time ", t1_time);
SERIAL_ECHOLNPGM("sample_count ", sample_count);
SERIAL_ECHOLNPGM("sample_distance ", sample_distance);
for (uint8_t i = 0; i < sample_count; i++)
SERIAL_ECHOLNPGM("sample ", i, " : ", temp_samples[i]);
SERIAL_ECHOLNPGM("t1 ", t1, " t2 ", t2, " t3 ", t3);
#if ENABLED(DEBUG_MPC_AUTOTUNE)
SERIAL_ECHOLN("Refining estimates for:");
SERIAL_ECHOLNPGM("asymp_temp ", asymp_temp);
SERIAL_ECHOLNPAIR_F("block_responsiveness ", block_responsiveness, 4);
#endif
// Update analytic tuning values based on the above
mpc.block_heat_capacity = mpc.ambient_xfer_coeff_fan0 / block_responsiveness;
mpc.sensor_responsiveness = block_responsiveness / (1.0f - (tuner.get_ambient_temp() - asymp_temp) * exp(-block_responsiveness * tuner.get_sample_1_time()) / (t1 - asymp_temp));
}
SERIAL_ECHOLNPGM(STR_MPC_AUTOTUNE_FINISHED);
TERN_(DWIN_LCD_PROUI, DWIN_MPCTuning(MPC_DONE));
SERIAL_ECHOLNPGM("MPC_BLOCK_HEAT_CAPACITY ", mpc.block_heat_capacity);
SERIAL_ECHOLNPAIR_F("MPC_SENSOR_RESPONSIVENESS ", mpc.sensor_responsiveness, 4);
SERIAL_ECHOLNPAIR_F("MPC_AMBIENT_XFER_COEFF ", mpc.ambient_xfer_coeff_fan0, 4);
@ -1702,9 +1832,9 @@ void Temperature::mintemp_error(const heater_id_t heater_id) {
// Check if temperature is within the correct band
if (WITHIN(temp_bed.celsius, BED_MINTEMP, BED_MAXTEMP)) {
#if ENABLED(BED_LIMIT_SWITCHING)
if (temp_bed.is_above_target((BED_HYSTERESIS) - 1))
if (temp_bed.is_above_target(BED_HYSTERESIS))
temp_bed.soft_pwm_amount = 0;
else if (temp_bed.is_below_target((BED_HYSTERESIS) - 1))
else if (temp_bed.is_below_target(BED_HYSTERESIS))
temp_bed.soft_pwm_amount = MAX_BED_POWER >> 1;
#else // !PIDTEMPBED && !BED_LIMIT_SWITCHING
temp_bed.soft_pwm_amount = temp_bed.is_below_target() ? MAX_BED_POWER >> 1 : 0;
@ -1778,7 +1908,7 @@ void Temperature::mintemp_error(const heater_id_t heater_id) {
#ifndef MIN_COOLING_SLOPE_DEG_CHAMBER_VENT
#define MIN_COOLING_SLOPE_DEG_CHAMBER_VENT 1.5
#endif
if (!flag_chamber_excess_heat && temp_chamber.is_above_target((HIGH_EXCESS_HEAT_LIMIT) - 1)) {
if (!flag_chamber_excess_heat && temp_chamber.is_above_target(HIGH_EXCESS_HEAT_LIMIT)) {
// Open vent after MIN_COOLING_SLOPE_TIME_CHAMBER_VENT seconds if the
// temperature didn't drop at least MIN_COOLING_SLOPE_DEG_CHAMBER_VENT
if (next_cool_check_ms == 0 || ELAPSED(ms, next_cool_check_ms)) {
@ -1792,7 +1922,7 @@ void Temperature::mintemp_error(const heater_id_t heater_id) {
next_cool_check_ms = 0;
old_temp = 9999;
}
if (flag_chamber_excess_heat && temp_chamber.is_above_target((LOW_EXCESS_HEAT_LIMIT) - 1))
if (flag_chamber_excess_heat && temp_chamber.is_above_target(LOW_EXCESS_HEAT_LIMIT))
flag_chamber_excess_heat = false;
#endif
}
@ -1824,9 +1954,9 @@ void Temperature::mintemp_error(const heater_id_t heater_id) {
}
else {
#if ENABLED(CHAMBER_LIMIT_SWITCHING)
if (temp_chamber.is_above_target((TEMP_CHAMBER_HYSTERESIS) - 1))
if (temp_chamber.is_above_target(TEMP_CHAMBER_HYSTERESIS))
temp_chamber.soft_pwm_amount = 0;
else if (temp_chamber.is_below_target((TEMP_CHAMBER_HYSTERESIS) - 1))
else if (temp_chamber.is_below_target(TEMP_CHAMBER_HYSTERESIS))
temp_chamber.soft_pwm_amount = (MAX_CHAMBER_POWER) >> 1;
#else
temp_chamber.soft_pwm_amount = temp_chamber.is_below_target() ? (MAX_CHAMBER_POWER) >> 1 : 0;

View file

@ -150,7 +150,7 @@ typedef struct { float p, i, d, c, f; } raw_pidcf_t;
#if HAS_PID_HEATING
#define PID_K2 (1-float(PID_K1))
#define PID_K2 (1.0f - float(PID_K1))
#define PID_dT ((OVERSAMPLENR * float(ACTUAL_ADC_SAMPLES)) / (TEMP_TIMER_FREQUENCY))
// Apply the scale factors to the PID values
@ -231,7 +231,7 @@ typedef struct { float p, i, d, c, f; } raw_pidcf_t;
};
#endif
#endif // HAS_PID_HEATING
#if ENABLED(PIDTEMP)
@ -1215,12 +1215,69 @@ class Temperature {
}
#endif
#endif
#endif // HAS_PID_HEATING
#if ENABLED(MPC_AUTOTUNE)
void MPC_autotune(const uint8_t e);
// Utility class to perform MPCTEMP auto tuning measurements
class MPC_autotuner {
public:
enum MeasurementState { CANCELLED, FAILED, SUCCESS };
MPC_autotuner(const uint8_t extruderIdx);
~MPC_autotuner();
MeasurementState measure_ambient_temp();
MeasurementState measure_heatup();
MeasurementState measure_transfer();
celsius_float_t get_ambient_temp() { return ambient_temp; }
celsius_float_t get_last_measured_temp() { return current_temp; }
float get_elapsed_heating_time() { return elapsed_heating_time; }
float get_sample_1_time() { return t1_time; }
static float get_sample_1_temp() { return temp_samples[0]; }
static float get_sample_2_temp() { return temp_samples[(sample_count - 1) >> 1]; }
static float get_sample_3_temp() { return temp_samples[sample_count - 1]; }
static float get_sample_interval() { return sample_distance * (sample_count >> 1); }
static celsius_float_t get_temp_fastest() { return temp_fastest; }
float get_time_fastest() { return time_fastest; }
float get_rate_fastest() { return rate_fastest; }
float get_power_fan0() { return power_fan0; }
#if HAS_FAN
static float get_power_fan255() { return power_fan255; }
#endif
protected:
static void init_timers() { curr_time_ms = next_report_ms = millis(); }
MeasurementState housekeeping();
uint8_t e;
float elapsed_heating_time;
celsius_float_t ambient_temp, current_temp;
float t1_time;
static millis_t curr_time_ms, next_report_ms;
static celsius_float_t temp_samples[16];
static uint8_t sample_count;
static uint16_t sample_distance;
// Parameters from differential analysis
static celsius_float_t temp_fastest;
float time_fastest, rate_fastest;
float power_fan0;
#if HAS_FAN
static float power_fan255;
#endif
};
enum MPCTuningType { AUTO, FORCE_ASYMPTOTIC, FORCE_DIFFERENTIAL };
static void MPC_autotune(const uint8_t e, MPCTuningType tuning_type);
#endif // MPC_AUTOTUNE
#if ENABLED(PROBING_HEATERS_OFF)
static void pause_heaters(const bool p);
#endif