diff --git a/Marlin/Configuration.h b/Marlin/Configuration.h index 8818b04a84f2..fee89898a375 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,32 +731,30 @@ //====================== PID > Bed Temperature Control ====================== //=========================================================================== +// @section bed temp + +/** + * Max Bed Power + * Applies to all forms of bed control (PID, bang-bang, and bang-bang with hysteresis). + * When set to any value below 255, enables a form of PWM to the bed that acts like a divider + * so don't use it unless you are OK with PWM on your bed. (See the comment on enabling PIDTEMPBED) + */ +#define MAX_BED_POWER 255 // limits duty cycle to bed; 255=full current + /** * 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 + * + * With this option disabled, bang-bang will be used. BED_LIMIT_SWITCHING enables hysteresis. */ //#define PIDTEMPBED -//#define BED_LIMIT_SWITCHING - -/** - * Max Bed Power - * Applies to all forms of bed control (PID, bang-bang, and bang-bang with hysteresis). - * When set to any value below 255, enables a form of PWM to the bed that acts like a divider - * so don't use it unless you are OK with PWM on your bed. (See the comment on enabling PIDTEMPBED) - */ -#define MAX_BED_POWER 255 // limits duty cycle to bed; 255=full current - #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 95bc40e1ecbb..e7032a2e5579 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 d0c005ea4eb1..7d2d94952ff0 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 a92d07018319..8bcaf5d83fc5 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,166 +939,175 @@ 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; - if (ELAPSED(ms, next_report_ms)) { - next_report_ms += 1000UL; + celsius_float_t Temperature::MPC_autotuner::temp_samples[16]; + uint8_t Temperature::MPC_autotuner::sample_count; + uint16_t Temperature::MPC_autotuner::sample_distance; - print_heater_states(e); - SERIAL_EOL(); - } + // Parameters from differential analysis + celsius_float_t Temperature::MPC_autotuner::temp_fastest; - 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() { - 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); + #if HAS_FAN + float Temperature::MPC_autotuner::power_fan255; + #endif - TERN_(TEMP_TUNING_MAINTAIN_FAN, adaptive_fan_slowing = true); - } - } on_exit(e); + Temperature::MPC_autotuner::MPC_autotuner(const uint8_t extruderIdx) : e(extruderIdx) { + TERN_(TEMP_TUNING_MAINTAIN_FAN, adaptive_fan_slowing = false); + } - SERIAL_ECHOLNPGM(STR_MPC_AUTOTUNE_START, e); - MPCHeaterInfo &hotend = temp_hotend[e]; - MPC_t &mpc = hotend.mpc; + Temperature::MPC_autotuner::~MPC_autotuner() { + wait_for_heatup = false; - TERN_(TEMP_TUNING_MAINTAIN_FAN, adaptive_fan_slowing = false); + ui.reset_status(); - // Move to center of bed, just above bed height and cool with max fan - gcode.home_all_axes(true); - disable_all_heaters(); + temp_hotend[e].target = 0.0f; + temp_hotend[e].soft_pwm_amount = 0; #if HAS_FAN - zero_fan_speeds(); - set_fan_speed(TERN(SINGLEFAN, 0, e), 255); + set_fan_speed(TERN(SINGLEFAN, 0, e), 0); planner.sync_fan_speeds(fan_speed); #endif - do_blocking_move_to(xyz_pos_t(MPC_TUNING_POS)); - SERIAL_ECHOLNPGM(STR_MPC_COOLING_TO_AMBIENT); - #if ENABLED(DWIN_LCD_PROUI) - DWIN_MPCTuning(MPCTEMP_START); - LCD_ALERTMESSAGE(MSG_MPC_COOLING_TO_AMBIENT); - #else - LCD_MESSAGE(MSG_COOLING); - #endif + do_z_clearance(MPC_TUNING_END_Z, false); - millis_t ms = millis(), next_report_ms = ms, next_test_ms = ms + 10000UL; - celsius_float_t current_temp = degHotend(e), - ambient_temp = current_temp; + 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(ms, e, current_temp, next_report_ms)) return; + if (housekeeping() == CANCELLED) return CANCELLED; - if (ELAPSED(ms, next_test_ms)) { + 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 += 10000UL; + next_test_ms += test_interval_ms; } } wait_for_heatup = false; - #if HAS_FAN - set_fan_speed(TERN(SINGLEFAN, 0, e), 0); - planner.sync_fan_speeds(fan_speed); + #if ENABLED(DEBUG_MPC_AUTOTUNE) + SERIAL_ECHOLNPGM("MPC_autotuner::measure_ambient_temp() Completed"); + SERIAL_ECHOLNPGM("====="); + SERIAL_ECHOLNPGM("ambient_temp ", get_ambient_temp()); #endif - hotend.modeled_ambient_temp = ambient_temp; + 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; - 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; + + // 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(ms, e, current_temp, next_report_ms)) return; + 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 (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]; + 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; + 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; + if (current_temp >= 200.0f) break; + + next_test_time_ms += test_interval_ms * sample_distance; - next_test_ms += 1000UL * sample_distance; + } + else { + // Third regime (after 200deg) finished gathering data so finish + break; + } } } wait_for_heatup = false; hotend.soft_pwm_amount = 0; - // 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]; - 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)); + elapsed_heating_time = MS_TO_SEC_PRECISE(curr_time_ms - heat_start_time_ms); - 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)); - TERN_(MPC_INCLUDE_FAN, mpc.fan255_adjustment = 0.0f); + // 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--; - 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 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; - // 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)); - 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, + 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 @@ -1114,67 +1118,193 @@ volatile bool Temperature::raw_temps_ready = false; wait_for_heatup = true; for (;;) { // Can be interrupted with M108 - if (housekeeping(ms, e, current_temp, next_report_ms)) return; + if (housekeeping() == CANCELLED) return CANCELLED; - if (ELAPSED(ms, next_test_ms)) { + if (ELAPSED(curr_time_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)) + 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(ms, test_end_ms) && !fan0_done) { + 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 = ms + settle_time; + settle_end_ms = curr_time_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)) + 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(ms, test_end_ms)) break; + else if (ELAPSED(curr_time_ms, test_end_ms)) break; last_temp = current_temp; - next_test_ms += MPC_dT * 1000; + next_test_ms += test_interval_ms; } - if (!WITHIN(current_temp, t3 - 15.0f, hotend.target + 15.0f)) { + // 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)); - break; + wait_for_heatup = false; + return FAILED; } } 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); + 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); + disable_all_heaters(); #if HAS_FAN - const float power_fan255 = total_energy_fan255 * 1000 / test_duration, - ambient_xfer_coeff_fan255 = power_fan255 / (hotend.target - ambient_temp); - mpc.applyFanAdjustment(ambient_xfer_coeff_fan255); + zero_fan_speeds(); + set_fan_speed(TERN(SINGLEFAN, 0, e), 255); + planner.sync_fan_speeds(fan_speed); #endif + do_blocking_move_to(xyz_pos_t(MPC_TUNING_POS)); - // 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)); + // Determine ambient temperature. + SERIAL_ECHOLNPGM(STR_MPC_COOLING_TO_AMBIENT); + #if ENABLED(DWIN_LCD_PROUI) + DWIN_MPCTuning(MPCTEMP_START); + LCD_ALERTMESSAGE(MSG_MPC_COOLING_TO_AMBIENT); + #else + LCD_MESSAGE(MSG_COOLING); + #endif - SERIAL_ECHOLNPGM(STR_MPC_AUTOTUNE_FINISHED); - TERN_(DWIN_LCD_PROUI, DWIN_MPCTuning(MPC_DONE)); + if (tuner.measure_ambient_temp() != MPC_autotuner::MeasurementState::SUCCESS) return; + hotend.modeled_ambient_temp = tuner.get_ambient_temp(); - #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 HAS_FAN + set_fan_speed(TERN(SINGLEFAN, 0, e), 0); + planner.sync_fan_speeds(fan_speed); + #endif + + // 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)); + + if (tuner.measure_heatup() != MPC_autotuner::MeasurementState::SUCCESS) return; + + // Calculate physical constants from three equally-spaced samples + 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)) / tuner.get_sample_interval(); + + #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); + + 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; + 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 + 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 = 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)); + 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 27aef11c7eea..4bd185c4251b 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);