diff --git a/Firmware/Marlin_main.cpp b/Firmware/Marlin_main.cpp index e8027aa3..ed71ce35 100644 --- a/Firmware/Marlin_main.cpp +++ b/Firmware/Marlin_main.cpp @@ -7760,6 +7760,54 @@ Sigma_Exit: PID_autotune(temp, e, c); } break; + + /*! + ### M310 - Temperature model + #### Usage + + M310 [ C ] [ P ] [ I R ] [ S ] [ E ] [ W ] [ A ] [ T ] + + #### Parameters + - `P` - power + - `C` - capacitance + - `I` - resistance index position + - `R` - resistance value (requires `I`) + - `S` - set 0=disable 1=enable (default) + - `E` - error threshold (define min/max values in variants) + - `W` - warning threshold (define min/max values in variants) + - `T` - ambient temperature correction + - `A` - autotune C+R values + */ + case 310: + { + // parse all parameters + float P = NAN, C = NAN, R = NAN, E = NAN, W = NAN, T = NAN, A = NAN; + int8_t I = -1, S = -1; + if(code_seen('C')) C = code_value(); + if(code_seen('P')) P = code_value(); + if(code_seen('I')) I = code_value_short(); + if(code_seen('R')) R = code_value(); + if(code_seen('S')) S = code_value_short(); + if(code_seen('E')) E = code_value(); + if(code_seen('W')) W = code_value(); + if(code_seen('T')) T = code_value(); + if(code_seen('A')) A = code_value(); + + // report values if nothing has been requested + if(isnan(C) && isnan(P) && isnan(R) && isnan(E) && isnan(W) && isnan(T) && isnan(A) && I < 0 && S < 0) { + temp_model_report_settings(); + break; + } + + // update all set parameters + if(S >= 0) temp_model_set_enabled(S); + if(!isnan(C) || !isnan(P) || !isnan(T) || !isnan(W) || !isnan(E)) temp_model_set_params(C, P, T, W, E); + if(I >= 0 && !isnan(R)) temp_model_set_resistance(I, R); + + // run autotune + if(!isnan(A)) temp_model_autotune(A != 0? A: NAN); + } + break; /*! ### M400 - Wait for all moves to finish M400: Wait for current moves to finish @@ -9144,7 +9192,16 @@ Sigma_Exit: }; #endif -#ifdef TEMP_MODEL_LOGGING +#ifdef TEMP_MODEL_DEBUG + /*! + ## D70 - Enable low-level temperature model logging for offline simulation + #### Usage + + D70 [ I ] + + #### Parameters + - `I` - Enable 0-1 (default 0) + */ case 70: { if(code_seen('I')) temp_model_log_enable(code_value_short()); diff --git a/Firmware/temp_model.h b/Firmware/temp_model.h new file mode 100644 index 00000000..30445f0f --- /dev/null +++ b/Firmware/temp_model.h @@ -0,0 +1,110 @@ +// model-based temperature safety checker declarations +#ifndef TEMP_MGR_INTV +#error "this file is not a public interface, it should be used *only* within temperature.cpp!" +#endif + +#include "planner.h" + +constexpr uint8_t TEMP_MODEL_CAL_S = 60; // Maximum recording lenght during calibration (s) +constexpr float TEMP_MODEL_fS = 0.065; // simulation filter (1st-order IIR factor) +constexpr float TEMP_MODEL_fE = 0.05; // error filter (1st-order IIR factor) + +// transport delay buffer size (samples) +constexpr uint8_t TEMP_MODEL_LAG_SIZE = (TEMP_MODEL_LAG / TEMP_MGR_INTV + 0.5); + +// resistance values for all fan levels +constexpr uint8_t TEMP_MODEL_R_SIZE = (1 << FAN_SOFT_PWM_BITS); + +namespace temp_model { + +// recording scratch buffer +struct rec_entry +{ + float temp; // heater temperature + uint8_t pwm; // heater PWM +}; + +constexpr uint16_t rec_buffer_size = TEMP_MODEL_CAL_S / TEMP_MGR_INTV; +static rec_entry* const rec_buffer = (rec_entry*)block_buffer; // oh-hey, free memory! +static_assert(sizeof(rec_entry[rec_buffer_size]) <= sizeof(block_buffer), + "recording length too long to fit within available buffer"); + +struct model_data +{ + // temporary buffers + float dT_lag_buf[TEMP_MODEL_LAG_SIZE]; // transport delay buffer + uint8_t dT_lag_idx = 0; // transport delay buffer index + float dT_err_prev = 0; // previous temperature delta error + float T_prev = 0; // last temperature extruder + + // configurable parameters + float P; // heater power (W) + float C; // heatblock capacitance (J/K) + float R[TEMP_MODEL_R_SIZE]; // heatblock resistance for all fan levels (K/W) + float Ta_corr; // ambient temperature correction (K) + + // thresholds + float warn; // pre-warning threshold (K/s) + float err; // error threshold (K/s) + + // status flags + union + { + bool flags; + struct + { + bool uninitialized: 1; // model is not initialized + bool error: 1; // error threshold set + bool warning: 1; // warning threshold set + } flag_bits; + }; + + // pre-computed values (initialized via reset) + float C_i; // heatblock capacitance (precomputed dT/C) + float warn_s; // pre-warning threshold (per sample) + float err_s; // error threshold (per sample) + + // simulation functions + void reset(uint8_t heater_pwm, uint8_t fan_pwm, float heater_temp, float ambient_temp); + void step(uint8_t heater_pwm, uint8_t fan_pwm, float heater_temp, float ambient_temp); +}; + +static bool enabled; // model check enabled +static model_data data; // default heater data + +static void init(); // initialize and setup the model subsystem +static bool calibrated(); // return calibration/model validity status +static void check(); // check and trigger errors or warnings based on current state + +// warning state (updated from from isr context) +volatile static struct +{ + float dT_err; // temperature delta error (per sample) + bool warning: 1; // warning condition + bool assert: 1; // warning is still asserted +} warning_state; + +static void handle_warning(); // handle warnings from user context + +#ifdef TEMP_MODEL_DEBUG +static struct +{ + volatile struct + { + uint32_t stamp; + int8_t delta_ms; + uint8_t counter; + uint8_t cur_pwm; + float cur_temp; + float cur_amb; + } entry; + + uint8_t serial; + bool enabled; +} log_buf; + +static void log_usr(); // user log handler +static void log_isr(); // isr log handler +#endif + +} // namespace temp_model diff --git a/Firmware/temperature.cpp b/Firmware/temperature.cpp index d9353d2e..f306618f 100755 --- a/Firmware/temperature.cpp +++ b/Firmware/temperature.cpp @@ -48,6 +48,19 @@ #error "ADC_OVRSAMPL oversampling must match OVERSAMPLENR" #endif +// temperature manager timer configuration +#define TEMP_MGR_INTV 0.27 // seconds, ~3.7Hz +#define TIMER5_PRESCALE 256 +#define TIMER5_OCRA_OVF (uint16_t)(TEMP_MGR_INTV / ((long double)TIMER5_PRESCALE / F_CPU)) +#define TEMP_MGR_INTERRUPT_STATE() (TIMSK5 & (1<= B? A: B) -#endif -// samples required for settling the model (crude approximation) -static uint8_t TM_dT_smp = MAX(TM_dTs, MAX(3/TM_fS, 3/TM_fE)); - -static void check_temp_model() +void model_data::reset(uint8_t heater_pwm, uint8_t fan_pwm, float heater_temp, float ambient_temp) { - const float soft_pwm_inv = 1. / ((1 << 7) - 1); - const float soft_pwm_fan_inv = 1. / ((1 << FAN_SOFT_PWM_BITS) - 1); + // pre-compute invariant values + C_i = (TEMP_MGR_INTV / C); + warn_s = warn * TEMP_MGR_INTV; + err_s = err * TEMP_MGR_INTV; + + // initial values + memset(dT_lag_buf, 0, sizeof(dT_lag_buf)); + dT_lag_idx = 0; + dT_err_prev = 0; + T_prev = heater_temp; + + // perform one step to initialize the first delta + step(heater_pwm, fan_pwm, heater_temp, ambient_temp); + + // clear the initialization flag + flag_bits.uninitialized = false; +} + +void model_data::step(uint8_t heater_pwm, uint8_t fan_pwm, float heater_temp, float ambient_temp) +{ + constexpr float soft_pwm_inv = 1. / ((1 << 7) - 1); // input values - float heater_scale = soft_pwm_inv * soft_pwm[0]; - float fan_scale = soft_pwm_fan_inv * soft_pwm_fan; - float cur_temp_heater = current_temperature_isr[0]; - float cur_temp_ambient = current_temperature_ambient_isr + TM_aC; + const float heater_scale = soft_pwm_inv * heater_pwm; + const float cur_heater_temp = heater_temp; + const float cur_ambient_temp = ambient_temp + Ta_corr; + const float cur_R = R[fan_pwm]; // resistance at current fan power (K/W) - // model invariants - float C_i = (TEMP_MGR_INTV / TM_C); - - float dP = TM_P * heater_scale; // current power [W] - float R = TM_R + TM_Rf * fan_scale; // resistance (constant + fan modulation) - float dPl = (cur_temp_heater - cur_temp_ambient) / R; // [W] leakage power + float dP = P * heater_scale; // current power [W] + float dPl = (cur_heater_temp - cur_ambient_temp) / cur_R; // [W] leakage power float dT = (dP - dPl) * C_i; // expected temperature difference (K) // filter and lag dT - uint8_t next_dT_idx = (TM_dT_idx == (TM_dTs - 1) ? 0: TM_dT_idx + 1); - float lag_dT = TM_dT_buf[next_dT_idx]; - float prev_dT = TM_dT_buf[TM_dT_idx]; - float dTf = (prev_dT * (1. - TM_fS)) + (dT * TM_fS); - TM_dT_buf[next_dT_idx] = dTf; - TM_dT_idx = next_dT_idx; + uint8_t dT_next_idx = (dT_lag_idx == (TEMP_MODEL_LAG_SIZE - 1) ? 0: dT_lag_idx + 1); + float dT_lag = dT_lag_buf[dT_next_idx]; + float dT_lag_prev = dT_lag_buf[dT_lag_idx]; + float dT_f = (dT_lag_prev * (1.f - TEMP_MODEL_fS)) + (dT * TEMP_MODEL_fS); + dT_lag_buf[dT_next_idx] = dT_f; + dT_lag_idx = dT_next_idx; // calculate and filter dT_err - float dT_err = (cur_temp_heater - TM_T) - lag_dT; - float dT_err_f = (TM_dT_err * (1. - TM_fE)) + (dT_err * TM_fE); - TM_T = cur_temp_heater; - TM_dT_err = dT_err_f; + float dT_err = (cur_heater_temp - T_prev) - dT_lag; + float dT_err_f = (dT_err_prev * (1.f - TEMP_MODEL_fE)) + (dT_err * TEMP_MODEL_fE); + T_prev = cur_heater_temp; + dT_err_prev = dT_err_f; // check and trigger errors - if(TM_dT_smp) { - // model not ready - --TM_dT_smp; - } else { - if(dT_err_f > TM_err_s || dT_err_f < -TM_err_s) - set_temp_error(TempErrorSource::hotend, 0, TempErrorType::model); + flag_bits.error = (fabsf(dT_err_f) > err_s); + flag_bits.warning = (fabsf(dT_err_f) > warn_s); +} + +// verify calibration status and trigger a model reset if valid +void setup() +{ + if(!calibrated()) enabled = false; + data.flag_bits.uninitialized = true; +} + +void init() +{ + // TODO: initialize the model with eeprom values + data.P = TEMP_MODEL_P; + data.C = TEMP_MODEL_C; + for(uint8_t i = 0; i != TEMP_MODEL_R_SIZE; ++i) + data.R[i] = TEMP_MODEL_R; + data.Ta_corr = TEMP_MODEL_Ta_corr; + data.warn = TEMP_MODEL_W; + data.err = TEMP_MODEL_E; + + enabled = true; + setup(); +} + +bool calibrated() +{ + if(!(data.P >= 0)) return false; + if(!(data.C >= 0)) return false; + if(!(data.Ta_corr != NAN)) return false; + for(uint8_t i = 0; i != TEMP_MODEL_R_SIZE; ++i) { + if(!(temp_model::data.R[i] >= 0)) + return false; + } + if(!(data.warn != NAN)) return false; + if(!(data.err != NAN)) return false; + return true; +} + +void check() +{ + if(!enabled) return; + + uint8_t heater_pwm = soft_pwm[0]; + uint8_t fan_pwm = soft_pwm_fan; + float heater_temp = current_temperature_isr[0]; + float ambient_temp = current_temperature_ambient_isr; + + // check if a reset is required to seed the model: this needs to be done with valid + // ADC values, so we can't do that directly in init() + if(data.flag_bits.uninitialized) + data.reset(heater_pwm, fan_pwm, heater_temp, ambient_temp); + + // step the model + data.step(heater_pwm, fan_pwm, heater_temp, ambient_temp); + + // handle errors + if(data.flag_bits.error) + set_temp_error(TempErrorSource::hotend, 0, TempErrorType::model); + + // handle warning conditions as lower-priority but with greater feedback + warning_state.assert = data.flag_bits.warning; + if(warning_state.assert) { + warning_state.warning = true; + warning_state.dT_err = temp_model::data.dT_err_prev; } } -#ifdef TEMP_MODEL_LOGGING -static struct +void handle_warning() { - volatile struct + // update values + float warn = data.warn; + float dT_err; { - uint32_t stamp; - int8_t delta_ms; - uint8_t counter; - uint8_t cur_pwm; - float cur_temp; - float cur_amb; - } entry; + TempMgrGuard temp_mgr_guard; + dT_err = warning_state.dT_err; + } + dT_err /= TEMP_MGR_INTV; // per-sample => K/s - uint8_t serial; - bool enabled; -} temp_model_log_buf; + // TODO: alert the user on the lcd + printf_P(PSTR("TM: error |%f|>%f\n"), (double)dT_err, (double)warn); -static void temp_model_log_usr() + static bool beeper = false; + if(warning_state.assert) { + // beep periodically + beeper = !beeper; + WRITE(BEEPER, beeper); + } else { + // warning cleared, reset state + warning_state.warning = false; + beeper = false; + WRITE(BEEPER, LOW); + } +} + +#ifdef TEMP_MODEL_DEBUG +void log_usr() { - if(!temp_model_log_buf.enabled) - return; + if(!log_buf.enabled) return; - uint8_t counter = temp_model_log_buf.entry.counter; - if (counter == temp_model_log_buf.serial) - return; + uint8_t counter = log_buf.entry.counter; + if (counter == log_buf.serial) return; int8_t delta_ms; uint8_t cur_pwm; - float cur_temp; - float cur_amb; + + // avoid strict-aliasing warnings + union { float cur_temp; uint32_t cur_temp_b; }; + union { float cur_amb; uint32_t cur_amb_b; }; + { TempMgrGuard temp_mgr_guard; - delta_ms = temp_model_log_buf.entry.delta_ms; - counter = temp_model_log_buf.entry.counter; - cur_pwm = temp_model_log_buf.entry.cur_pwm; - cur_temp = temp_model_log_buf.entry.cur_temp; - cur_amb = temp_model_log_buf.entry.cur_amb; + delta_ms = log_buf.entry.delta_ms; + counter = log_buf.entry.counter; + cur_pwm = log_buf.entry.cur_pwm; + cur_temp = log_buf.entry.cur_temp; + cur_amb = log_buf.entry.cur_amb; } - uint8_t d = counter - temp_model_log_buf.serial; - temp_model_log_buf.serial = counter; + uint8_t d = counter - log_buf.serial; + log_buf.serial = counter; - printf_P(PSTR("TML %d %d %x %lx %lx\n"), (unsigned)d - 1, (int)delta_ms + 1, (int)cur_pwm, - *(unsigned long*)&cur_temp, *(unsigned long*)&cur_amb); + printf_P(PSTR("TML %d %d %x %lx %lx\n"), (unsigned)d - 1, (int)delta_ms + 1, + (int)cur_pwm, (unsigned long)cur_temp_b, (unsigned long)cur_amb_b); } -static void temp_model_log_isr() +void log_isr() { - if(!temp_model_log_buf.enabled) - return; + if(!log_buf.enabled) return; uint32_t stamp = _millis(); - uint8_t delta_ms = stamp - temp_model_log_buf.entry.stamp - (TEMP_MGR_INTV * 1000); - temp_model_log_buf.entry.stamp = stamp; + uint8_t delta_ms = stamp - log_buf.entry.stamp - (TEMP_MGR_INTV * 1000); + log_buf.entry.stamp = stamp; - ++temp_model_log_buf.entry.counter; - temp_model_log_buf.entry.delta_ms = delta_ms; - temp_model_log_buf.entry.cur_pwm = soft_pwm[0]; - temp_model_log_buf.entry.cur_temp = current_temperature_isr[0]; - temp_model_log_buf.entry.cur_amb = current_temperature_ambient_isr; + ++log_buf.entry.counter; + log_buf.entry.delta_ms = delta_ms; + log_buf.entry.cur_pwm = soft_pwm[0]; + log_buf.entry.cur_temp = current_temperature_isr[0]; + log_buf.entry.cur_amb = current_temperature_ambient_isr; +} +#endif + +} // namespace temp_model + +void temp_model_set_enabled(bool enabled) +{ + // set the enabled flag + { + TempMgrGuard temp_mgr_guard; + temp_model::enabled = enabled; + temp_model::setup(); + } + + // verify that the model has been enabled + if(enabled && !temp_model::enabled) + SERIAL_ECHOLNPGM("TM: invalid parameters, cannot enable"); } +void temp_model_set_params(float C, float P, float Ta_corr, float warn, float err) +{ + TempMgrGuard temp_mgr_guard; + if(!isnan(C) && C > 0) temp_model::data.C = C; + if(!isnan(P) && P > 0) temp_model::data.P = P; + if(!isnan(Ta_corr)) temp_model::data.Ta_corr = Ta_corr; + if(!isnan(err) && err > 0) temp_model::data.err = err; + if(!isnan(warn) && warn > 0) temp_model::data.warn = warn; + + // ensure warn <= err + if (temp_model::data.warn > temp_model::data.err) + temp_model::data.warn = temp_model::data.err; + + temp_model::setup(); +} + +void temp_model_set_resistance(uint8_t index, float R) +{ + if(index >= TEMP_MODEL_R_SIZE || R <= 0) + return; + + TempMgrGuard temp_mgr_guard; + temp_model::data.R[index] = R; + temp_model::setup(); +} + +void temp_model_report_settings() +{ + printf_P(PSTR("%STemperature Model settings:\n" + "%S M310 P%.2f C%.2f S%u E%.2f W%.2f T%.2f\n"), + echomagic, echomagic, (double)temp_model::data.P, (double)temp_model::data.C, (unsigned)temp_model::enabled, + (double)temp_model::data.err, (double)temp_model::data.warn, (double)temp_model::data.Ta_corr); + for(uint8_t i = 0; i != TEMP_MODEL_R_SIZE; ++i) + printf_P(PSTR("%S M310 I%u R%.2f\n"), echomagic, (unsigned)i, (double)temp_model::data.R[i]); +} + +void temp_model_autotune(float temp) +{ + // TODO +} + +#ifdef TEMP_MODEL_DEBUG void temp_model_log_enable(bool enable) { if(enable) { TempMgrGuard temp_mgr_guard; - temp_model_log_buf.entry.stamp = _millis(); + temp_model::log_buf.entry.stamp = _millis(); } - temp_model_log_buf.enabled = enable; + temp_model::log_buf.enabled = enable; } #endif #endif diff --git a/Firmware/temperature.h b/Firmware/temperature.h index 558c1fe2..ad170ecf 100755 --- a/Firmware/temperature.h +++ b/Firmware/temperature.h @@ -228,9 +228,18 @@ FORCE_INLINE void autotempShutdown(){ } void PID_autotune(float temp, int extruder, int ncycles); -#ifdef TEMP_MODEL_LOGGING + +#ifdef TEMP_MODEL +void temp_model_set_enabled(bool enabled); +void temp_model_set_params(float C = NAN, float P = NAN, float Ta_corr = NAN, float warn = NAN, float err = NAN); +void temp_model_set_resistance(uint8_t index, float R); +void temp_model_report_settings(); +void temp_model_autotune(float temp = NAN); + +#ifdef TEMP_MODEL_DEBUG void temp_model_log_enable(bool enable); #endif +#endif #ifdef FAN_SOFT_PWM extern unsigned char fanSpeedSoftPwm; diff --git a/Firmware/variants/1_75mm_MK3S-EINSy10a-E3Dv6full.h b/Firmware/variants/1_75mm_MK3S-EINSy10a-E3Dv6full.h index 205e43f0..574689cc 100644 --- a/Firmware/variants/1_75mm_MK3S-EINSy10a-E3Dv6full.h +++ b/Firmware/variants/1_75mm_MK3S-EINSy10a-E3Dv6full.h @@ -418,9 +418,33 @@ #define TEMP_RUNAWAY_EXTRUDER_TIMEOUT 45 // model-based temperature check -#define TEMP_MODEL_CHECK 1 -#define TEMP_MODEL_CHECK_WARN_ONLY 1 -#define TEMP_MODEL_LOGGING 1 +#define TEMP_MODEL 1 // enable model-based temperature checks +#define TEMP_MODEL_DEBUG 1 // extended runtime logging + +#define TEMP_MODEL_P 38. // heater power (W) + +#define TEMP_MODEL_C 11. // initial guess for heatblock capacitance (J/K) +#define TEMP_MODEL_Cl 5 // C estimation lower limit +#define TEMP_MODEL_Ch 20 // C estimation upper limit +#define TEMP_MODEL_C_thr 0.01 // C estimation iteration threshold +#define TEMP_MODEL_C_itr 30 // C estimation iteration limit + +#define TEMP_MODEL_R 25 // initial guess for heatblock resistance (K/W) +#define TEMP_MODEL_Rl 5 // R estimation lower limit +#define TEMP_MODEL_Rh 50 // R estimation upper limit +#define TEMP_MODEL_R_thr 0.01 // R estimation iteration threshold +#define TEMP_MODEL_R_itr 30 // R estimation iteration limit + +#define TEMP_MODEL_Rf_D -15 // initial guess for resistance change with full-power fan +#define TEMP_MODEL_Ta_corr -7 // Default ambient temperature correction +#define TEMP_MODEL_LAG 2.1 // Temperature transport delay (s) + +#define TEMP_MODEL_W 1.2 // Default pre-warning threshold (K/s) +#define TEMP_MODEL_E 1.74 // Default error threshold (K/s) + +#define TEMP_MODEL_CAL_Th 230 // Default calibration working temperature (C) +#define TEMP_MODEL_CAL_Tl 50 // Default calibration cooling temperature (C) + /*------------------------------------ MOTOR CURRENT SETTINGS