diff --git a/Marlin/Configuration.h b/Marlin/Configuration.h index 8818b04a84..fee89898a3 100644 --- a/Marlin/Configuration.h +++ b/Marlin/Configuration.h @@ -650,14 +650,18 @@ // @section hotend temp -// Enable PIDTEMP for PID control or MPCTEMP for Predictive Model. -// temperature control. Disable both for bang-bang heating. -#define PIDTEMP // See the PID Tuning Guide at https://reprap.org/wiki/PID_Tuning -//#define MPCTEMP // ** EXPERIMENTAL ** +/** + * 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 ** 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_K1 0.95 // Smoothing factor within any PID loop +#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) //#define PID_DEBUG // Print PID debug data to the serial port. Use 'M303 D' to toggle activation. @@ -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 ==================== diff --git a/Marlin/src/core/millis_t.h b/Marlin/src/core/millis_t.h index 95bc40e1ec..e7032a2e55 100644 --- a/Marlin/src/core/millis_t.h +++ b/Marlin/src/core/millis_t.h @@ -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)) diff --git a/Marlin/src/gcode/temp/M306.cpp b/Marlin/src/gcode/temp/M306.cpp index d0c005ea4e..7d2d94952f 100644 --- a/Marlin/src/gcode/temp/M306.cpp +++ b/Marlin/src/gcode/temp/M306.cpp @@ -42,7 +42,10 @@ * R 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; } diff --git a/Marlin/src/module/temperature.cpp b/Marlin/src/module/temperature.cpp index a92d070183..8bcaf5d83f 100644 --- a/Marlin/src/module/temperature.cpp +++ b/Marlin/src/module/temperature.cpp @@ -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,60 +939,266 @@ 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 ¤t_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); + } + + Temperature::MPC_autotuner::~MPC_autotuner() { + wait_for_heatup = false; + + ui.reset_status(); + + temp_hotend[e].target = 0.0f; + temp_hotend[e].soft_pwm_amount = 0; + #if HAS_FAN + set_fan_speed(TERN(SINGLEFAN, 0, e), 0); + planner.sync_fan_speeds(fan_speed); + #endif + + do_z_clearance(MPC_TUNING_END_Z, false); + + TERN_(TEMP_TUNING_MAINTAIN_FAN, adaptive_fan_slowing = true); + } + + 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 (ELAPSED(ms, next_report_ms)) { - next_report_ms += 1000UL; + #if ENABLED(DEBUG_MPC_AUTOTUNE) + SERIAL_ECHOLNPGM("MPC_autotuner::measure_ambient_temp() Completed"); + SERIAL_ECHOLNPGM("====="); + SERIAL_ECHOLNPGM("ambient_temp ", get_ambient_temp()); + #endif - print_heater_states(e); - SERIAL_EOL(); + 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; - hal.idletask(); - TERN(DWIN_CREALITY_LCD, DWIN_Update(), ui.update()); + hotend.soft_pwm_amount = 0; - if (!wait_for_heatup) { - SERIAL_ECHOLNPGM(STR_MPC_AUTOTUNE_INTERRUPTED); - TERN_(DWIN_LCD_PROUI, DWIN_MPCTuning(MPC_INTERRUPTED)); - return true; - } + elapsed_heating_time = MS_TO_SEC_PRECISE(curr_time_ms - heat_start_time_ms); - return false; - }; + // 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--; - struct OnExit { - uint8_t e; - OnExit(const uint8_t _e) { this->e = _e; } - ~OnExit() { - wait_for_heatup = false; + #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 - ui.reset_status(); + return SUCCESS; + } - temp_hotend[e].target = 0.0f; - temp_hotend[e].soft_pwm_amount = 0; - #if HAS_FAN - set_fan_speed(TERN(SINGLEFAN, 0, e), 0); - planner.sync_fan_speeds(fan_speed); - #endif - - do_z_clearance(MPC_TUNING_END_Z, 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_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 - // 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)); + if (tuning_type == AUTO || tuning_type == FORCE_ASYMPTOTIC) { + // Calculate a new and better asymptotic temperature and re-evaluate the other constants + 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(); + + #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)); - #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); - SERIAL_ECHOLNPGM("asymp_temp ", asymp_temp); - SERIAL_ECHOLNPAIR_F("block_responsiveness ", block_responsiveness, 4); - #endif - 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; diff --git a/Marlin/src/module/temperature.h b/Marlin/src/module/temperature.h index 27aef11c7e..4bd185c425 100644 --- a/Marlin/src/module/temperature.h +++ b/Marlin/src/module/temperature.h @@ -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,11 +1215,68 @@ class Temperature { } #endif - #endif + #endif // HAS_PID_HEATING #if ENABLED(MPC_AUTOTUNE) - void MPC_autotune(const uint8_t e); - #endif + + // 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);