heater: Introduce smooth_time config option; remove pid_deriv_time

Add generic temperature smoothing to the Heater class.  This is useful
to avoid min_extrude_temp and verify_heater errors due to measurement
noise.

Rename the pid_deriv_time config option to smooth_time so that the
smoothing amount need only be specified once.

Signed-off-by: Kevin O'Connor <kevin@koconnor.net>
This commit is contained in:
Kevin O'Connor 2018-07-06 13:27:05 -04:00
parent 32175bc66a
commit b0ee323e2e
3 changed files with 36 additions and 25 deletions

View File

@ -161,6 +161,10 @@ sensor_pin: analog13
#adc_voltage: 5.0 #adc_voltage: 5.0
# The ADC comparison voltage. This parameter is only valid when the # The ADC comparison voltage. This parameter is only valid when the
# sensor is an AD595 or "PT100 INA826". The default is 5 volts. # sensor is an AD595 or "PT100 INA826". The default is 5 volts.
#smooth_time: 2.0
# A time value (in seconds) over which temperature measurements will
# be smoothed to reduce the impact of measurement noise. The default
# is 2 seconds.
control: pid control: pid
# Control algorithm (either pid or watermark). This parameter must # Control algorithm (either pid or watermark). This parameter must
# be provided. # be provided.
@ -173,10 +177,6 @@ pid_Ki: 1.08
pid_Kd: 114 pid_Kd: 114
# Kd is the "derivative" constant for the pid. This parameter must # Kd is the "derivative" constant for the pid. This parameter must
# be provided for PID heaters. # be provided for PID heaters.
#pid_deriv_time: 2.0
# A time value (in seconds) over which the derivative in the pid
# will be smoothed to reduce the impact of measurement noise. The
# default is 2 seconds.
#pid_integral_max: #pid_integral_max:
# The maximum "windup" the integral term may accumulate. The default # The maximum "windup" the integral term may accumulate. The default
# is to use the same value as max_power. # is to use the same value as max_power.

View File

@ -86,7 +86,7 @@ class ControlAutoTune:
if temp > self.peak: if temp > self.peak:
self.peak = temp self.peak = temp
self.peak_time = read_time self.peak_time = read_time
def check_busy(self, eventtime, last_temp, target_temp): def check_busy(self, eventtime, smoothed_temp, target_temp):
if self.heating or len(self.peaks) < 12: if self.heating or len(self.peaks) < 12:
return True return True
return False return False

View File

@ -21,23 +21,34 @@ class error(Exception):
class Heater: class Heater:
error = error error = error
def __init__(self, config, sensor): def __init__(self, config, sensor):
self.sensor = sensor
self.name = config.get_name()
printer = config.get_printer() printer = config.get_printer()
self.name = config.get_name()
# Setup sensor
self.sensor = sensor
self.min_temp = config.getfloat('min_temp', minval=KELVIN_TO_CELCIUS) self.min_temp = config.getfloat('min_temp', minval=KELVIN_TO_CELCIUS)
self.max_temp = config.getfloat('max_temp', above=self.min_temp) self.max_temp = config.getfloat('max_temp', above=self.min_temp)
self.sensor.setup_minmax(self.min_temp, self.max_temp) self.sensor.setup_minmax(self.min_temp, self.max_temp)
self.sensor.setup_callback(self.temperature_callback) self.sensor.setup_callback(self.temperature_callback)
self.pwm_delay = self.sensor.get_report_time_delta() self.pwm_delay = self.sensor.get_report_time_delta()
# Setup temperature checks
self.min_extrude_temp = config.getfloat( self.min_extrude_temp = config.getfloat(
'min_extrude_temp', 170., minval=self.min_temp, maxval=self.max_temp) 'min_extrude_temp', 170., minval=self.min_temp, maxval=self.max_temp)
is_fileoutput = printer.get_start_args().get('debugoutput') is not None
self.can_extrude = self.min_extrude_temp <= 0. or is_fileoutput
self.max_power = config.getfloat('max_power', 1., above=0., maxval=1.) self.max_power = config.getfloat('max_power', 1., above=0., maxval=1.)
self.smooth_time = config.getfloat('smooth_time', 2., above=0.)
self.inv_smooth_time = 1. / self.smooth_time
self.lock = threading.Lock() self.lock = threading.Lock()
self.last_temp = 0. self.last_temp = self.smoothed_temp = self.target_temp = 0.
self.last_temp_time = 0. self.last_temp_time = 0.
self.target_temp = 0. # pwm caching
self.next_pwm_time = 0.
self.last_pwm_value = 0.
# Setup control algorithm sub-class
algos = {'watermark': ControlBangBang, 'pid': ControlPID} algos = {'watermark': ControlBangBang, 'pid': ControlPID}
algo = config.getchoice('control', algos) algo = config.getchoice('control', algos)
self.control = algo(self, config)
# Setup output heater pin
heater_pin = config.get('heater_pin') heater_pin = config.get('heater_pin')
ppins = printer.lookup_object('pins') ppins = printer.lookup_object('pins')
if algo is ControlBangBang and self.max_power == 1.: if algo is ControlBangBang and self.max_power == 1.:
@ -48,12 +59,6 @@ class Heater:
'pwm_cycle_time', 0.100, above=0., maxval=self.pwm_delay) 'pwm_cycle_time', 0.100, above=0., maxval=self.pwm_delay)
self.mcu_pwm.setup_cycle_time(pwm_cycle_time) self.mcu_pwm.setup_cycle_time(pwm_cycle_time)
self.mcu_pwm.setup_max_duration(MAX_HEAT_TIME) self.mcu_pwm.setup_max_duration(MAX_HEAT_TIME)
is_fileoutput = self.mcu_pwm.get_mcu().is_fileoutput()
self.can_extrude = self.min_extrude_temp <= 0. or is_fileoutput
self.control = algo(self, config)
# pwm caching
self.next_pwm_time = 0.
self.last_pwm_value = 0.
# Load additional modules # Load additional modules
printer.try_load_module(config, "verify_heater %s" % (self.name,)) printer.try_load_module(config, "verify_heater %s" % (self.name,))
printer.try_load_module(config, "pid_calibrate") printer.try_load_module(config, "pid_calibrate")
@ -73,16 +78,22 @@ class Heater:
self.mcu_pwm.set_pwm(pwm_time, value) self.mcu_pwm.set_pwm(pwm_time, value)
def temperature_callback(self, read_time, temp): def temperature_callback(self, read_time, temp):
with self.lock: with self.lock:
time_diff = read_time - self.last_temp_time
self.last_temp = temp self.last_temp = temp
self.last_temp_time = read_time self.last_temp_time = read_time
self.can_extrude = (temp >= self.min_extrude_temp)
self.control.temperature_update(read_time, temp, self.target_temp) self.control.temperature_update(read_time, temp, self.target_temp)
temp_diff = temp - self.smoothed_temp
adj_time = min(time_diff * self.inv_smooth_time, 1.)
self.smoothed_temp += temp_diff * adj_time
self.can_extrude = (self.smoothed_temp >= self.min_extrude_temp)
#logging.debug("temp: %.3f %f = %f", read_time, temp) #logging.debug("temp: %.3f %f = %f", read_time, temp)
# External commands # External commands
def get_pwm_delay(self): def get_pwm_delay(self):
return self.pwm_delay return self.pwm_delay
def get_max_power(self): def get_max_power(self):
return self.max_power return self.max_power
def get_smooth_time(self):
return self.smooth_time
def set_temp(self, print_time, degrees): def set_temp(self, print_time, degrees):
if degrees and (degrees < self.min_temp or degrees > self.max_temp): if degrees and (degrees < self.min_temp or degrees > self.max_temp):
raise error("Requested temperature (%.1f) out of range (%.1f:%.1f)" raise error("Requested temperature (%.1f) out of range (%.1f:%.1f)"
@ -94,11 +105,11 @@ class Heater:
with self.lock: with self.lock:
if self.last_temp_time < print_time: if self.last_temp_time < print_time:
return 0., self.target_temp return 0., self.target_temp
return self.last_temp, self.target_temp return self.smoothed_temp, self.target_temp
def check_busy(self, eventtime): def check_busy(self, eventtime):
with self.lock: with self.lock:
return self.control.check_busy( return self.control.check_busy(
eventtime, self.last_temp, self.target_temp) eventtime, self.smoothed_temp, self.target_temp)
def set_control(self, control): def set_control(self, control):
with self.lock: with self.lock:
old_control = self.control old_control = self.control
@ -120,8 +131,8 @@ class Heater:
def get_status(self, eventtime): def get_status(self, eventtime):
with self.lock: with self.lock:
target_temp = self.target_temp target_temp = self.target_temp
last_temp = self.last_temp smoothed_temp = self.smoothed_temp
return {'temperature': last_temp, 'target': target_temp} return {'temperature': smoothed_temp, 'target': target_temp}
###################################################################### ######################################################################
@ -143,8 +154,8 @@ class ControlBangBang:
self.heater.set_pwm(read_time, self.heater_max_power) self.heater.set_pwm(read_time, self.heater_max_power)
else: else:
self.heater.set_pwm(read_time, 0.) self.heater.set_pwm(read_time, 0.)
def check_busy(self, eventtime, last_temp, target_temp): def check_busy(self, eventtime, smoothed_temp, target_temp):
return last_temp < target_temp-self.max_delta return smoothed_temp < target_temp-self.max_delta
###################################################################### ######################################################################
@ -161,7 +172,7 @@ class ControlPID:
self.Kp = config.getfloat('pid_Kp') / PID_PARAM_BASE self.Kp = config.getfloat('pid_Kp') / PID_PARAM_BASE
self.Ki = config.getfloat('pid_Ki') / PID_PARAM_BASE self.Ki = config.getfloat('pid_Ki') / PID_PARAM_BASE
self.Kd = config.getfloat('pid_Kd') / PID_PARAM_BASE self.Kd = config.getfloat('pid_Kd') / PID_PARAM_BASE
self.min_deriv_time = config.getfloat('pid_deriv_time', 2., above=0.) self.min_deriv_time = heater.get_smooth_time()
imax = config.getfloat('pid_integral_max', self.heater_max_power, imax = config.getfloat('pid_integral_max', self.heater_max_power,
minval=0.) minval=0.)
self.temp_integ_max = imax / self.Ki self.temp_integ_max = imax / self.Ki
@ -194,8 +205,8 @@ class ControlPID:
self.prev_temp_deriv = temp_deriv self.prev_temp_deriv = temp_deriv
if co == bounded_co: if co == bounded_co:
self.prev_temp_integ = temp_integ self.prev_temp_integ = temp_integ
def check_busy(self, eventtime, last_temp, target_temp): def check_busy(self, eventtime, smoothed_temp, target_temp):
temp_diff = target_temp - last_temp temp_diff = target_temp - smoothed_temp
return (abs(temp_diff) > PID_SETTLE_DELTA return (abs(temp_diff) > PID_SETTLE_DELTA
or abs(self.prev_temp_deriv) > PID_SETTLE_SLOPE) or abs(self.prev_temp_deriv) > PID_SETTLE_SLOPE)