aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--config/printer-wanhao-duplicator-i3-v2.1-2017.cfg6
-rw-r--r--docs/G-Codes.md8
-rw-r--r--klippy/extras/pid_calibrate.py127
-rw-r--r--klippy/gcode.py14
-rw-r--r--klippy/heater.py139
5 files changed, 147 insertions, 147 deletions
diff --git a/config/printer-wanhao-duplicator-i3-v2.1-2017.cfg b/config/printer-wanhao-duplicator-i3-v2.1-2017.cfg
index d1495afb..d7dd3738 100644
--- a/config/printer-wanhao-duplicator-i3-v2.1-2017.cfg
+++ b/config/printer-wanhao-duplicator-i3-v2.1-2017.cfg
@@ -45,11 +45,11 @@
# PID values from stock Wanhao firmware (Repetier) do not
# translate directly to klipper. You will need to run klipper's
# PID autotune function for the extruder and bed. After getting the
-# klipper firmware up and running, run the M303 autotune procedures
+# klipper firmware up and running, run the PID_CALIBRATE procedures
# by sending these commands via octoprint terminal (one per autotune):
#
-# extruder: M303 E0 S<temp>
-# heated bed: M303 E-1 S<temp>
+# extruder: PID_CALIBRATE HEATER=extruder TARGET=<temp>
+# heated bed: PID_CALIBRATE HEATER=heater_bed TARGET=<temp>
#
# After the autotune process completes, PID parameter results
# can be found in the Octoprint terminal tab (if you're quick)
diff --git a/docs/G-Codes.md b/docs/G-Codes.md
index 73fd2e93..25ff7550 100644
--- a/docs/G-Codes.md
+++ b/docs/G-Codes.md
@@ -26,7 +26,6 @@ Klipper supports the following standard G-Code commands:
- Get current position: `M114`
- Get firmware version: `M115`
- Set home offset: `M206 [X<pos>] [Y<pos>] [Z<pos>]`
-- Run PID tuning: `M303 [E<index>] S<temperature>`
For further details on the above commands see the
[RepRap G-Code documentation](http://reprap.org/wiki/G-code).
@@ -65,6 +64,13 @@ The following standard commands are supported:
verify that an endstop is working correctly.
- `GET_POSITION`: Return information on the current location of the
toolhead.
+- `PID_CALIBRATE HEATER=<config_name> TARGET=<temperature>
+ [WRITE_FILE=1]`: Perform a PID calibration test. The specified
+ heater will be enabled until the specified target temperature is
+ reached, and then the heater will be turned off and on for several
+ cycles. If the WRITE_FILE parameter is enabled, then the file
+ /tmp/heattest.txt will be created with a log of all temperature
+ samples taken during the test.
- `RESTART`: This will cause the host software to reload its config
and perform an internal reset. This command will not clear error
state from the micro-controller (see FIRMWARE_RESTART) nor will it
diff --git a/klippy/extras/pid_calibrate.py b/klippy/extras/pid_calibrate.py
new file mode 100644
index 00000000..477f3a93
--- /dev/null
+++ b/klippy/extras/pid_calibrate.py
@@ -0,0 +1,127 @@
+# Calibration of heater PID settings
+#
+# Copyright (C) 2016-2018 Kevin O'Connor <kevin@koconnor.net>
+#
+# This file may be distributed under the terms of the GNU GPLv3 license.
+import math, logging
+import extruder, heater
+
+class PIDCalibrate:
+ def __init__(self, config):
+ self.printer = config.get_printer()
+ self.gcode = self.printer.lookup_object('gcode')
+ self.gcode.register_command(
+ 'PID_CALIBRATE', self.cmd_PID_CALIBRATE,
+ desc=self.cmd_PID_CALIBRATE_help)
+ cmd_PID_CALIBRATE_help = "Run PID calibration test"
+ def cmd_PID_CALIBRATE(self, params):
+ heater_name = self.gcode.get_str('HEATER', params)
+ target = self.gcode.get_float('TARGET', params)
+ write_file = self.gcode.get_int('WRITE_FILE', params, 0)
+ try:
+ heater = extruder.get_printer_heater(self.printer, heater_name)
+ except self.printer.config_error as e:
+ raise self.gcode.error(str(e))
+ print_time = self.printer.lookup_object('toolhead').get_last_move_time()
+ calibrate = ControlAutoTune(heater)
+ old_control = heater.set_control(calibrate)
+ try:
+ heater.set_temp(print_time, target)
+ except heater.error as e:
+ raise self.gcode.error(str(e))
+ self.gcode.bg_temp(heater)
+ heater.set_control(old_control)
+ if write_file:
+ calibrate.write_file('/tmp/heattest.txt')
+ Kp, Ki, Kd = calibrate.calc_final_pid()
+ logging.info("Autotune: final: Kp=%f Ki=%f Kd=%f", Kp, Ki, Kd)
+ self.gcode.respond_info(
+ "PID parameters: pid_Kp=%.3f pid_Ki=%.3f pid_Kd=%.3f\n"
+ "To use these parameters, update the printer config file with\n"
+ "the above and then issue a RESTART command" % (Kp, Ki, Kd))
+
+TUNE_PID_DELTA = 5.0
+
+class ControlAutoTune:
+ def __init__(self, heater):
+ self.heater = heater
+ # Heating control
+ self.heating = False
+ self.peak = 0.
+ self.peak_time = 0.
+ # Peak recording
+ self.peaks = []
+ # Sample recording
+ self.last_pwm = 0.
+ self.pwm_samples = []
+ self.temp_samples = []
+ # Heater control
+ def set_pwm(self, read_time, value):
+ if value != self.last_pwm:
+ self.pwm_samples.append((read_time + heater.PWM_DELAY, value))
+ self.last_pwm = value
+ self.heater.set_pwm(read_time, value)
+ def adc_callback(self, read_time, temp):
+ self.temp_samples.append((read_time, temp))
+ if self.heating and temp >= self.heater.target_temp:
+ self.heating = False
+ self.check_peaks()
+ elif (not self.heating
+ and temp <= self.heater.target_temp - TUNE_PID_DELTA):
+ self.heating = True
+ self.check_peaks()
+ if self.heating:
+ self.set_pwm(read_time, self.heater.max_power)
+ if temp < self.peak:
+ self.peak = temp
+ self.peak_time = read_time
+ else:
+ self.set_pwm(read_time, 0.)
+ if temp > self.peak:
+ self.peak = temp
+ self.peak_time = read_time
+ def check_busy(self, eventtime):
+ if self.heating or len(self.peaks) < 12:
+ return True
+ return False
+ # Analysis
+ def check_peaks(self):
+ self.peaks.append((self.peak, self.peak_time))
+ if self.heating:
+ self.peak = 9999999.
+ else:
+ self.peak = -9999999.
+ if len(self.peaks) < 4:
+ return
+ self.calc_pid(len(self.peaks)-1)
+ def calc_pid(self, pos):
+ temp_diff = self.peaks[pos][0] - self.peaks[pos-1][0]
+ time_diff = self.peaks[pos][1] - self.peaks[pos-2][1]
+ max_power = self.heater.max_power
+ Ku = 4. * (2. * max_power) / (abs(temp_diff) * math.pi)
+ Tu = time_diff
+
+ Ti = 0.5 * Tu
+ Td = 0.125 * Tu
+ Kp = 0.6 * Ku * heater.PID_PARAM_BASE
+ Ki = Kp / Ti
+ Kd = Kp * Td
+ logging.info("Autotune: raw=%f/%f Ku=%f Tu=%f Kp=%f Ki=%f Kd=%f",
+ temp_diff, max_power, Ku, Tu, Kp, Ki, Kd)
+ return Kp, Ki, Kd
+ def calc_final_pid(self):
+ cycle_times = [(self.peaks[pos][1] - self.peaks[pos-2][1], pos)
+ for pos in range(4, len(self.peaks))]
+ midpoint_pos = sorted(cycle_times)[len(cycle_times)/2][1]
+ return self.calc_pid(midpoint_pos)
+ # Offline analysis helper
+ def write_file(self, filename):
+ pwm = ["pwm: %.3f %.3f" % (time, value)
+ for time, value in self.pwm_samples]
+ out = ["%.3f %.3f" % (time, temp) for time, temp in self.temp_samples]
+ f = open(filename, "wb")
+ f.write('\n'.join(pwm + out))
+ f.close()
+
+def load_config(config):
+ return PIDCalibrate(config)
diff --git a/klippy/gcode.py b/klippy/gcode.py
index dee03a16..050377f0 100644
--- a/klippy/gcode.py
+++ b/klippy/gcode.py
@@ -369,7 +369,7 @@ class GCodeParser:
'G1', 'G4', 'G28', 'M18', 'M400',
'G20', 'M82', 'M83', 'G90', 'G91', 'G92', 'M114', 'M206', 'M220', 'M221',
'M105', 'M104', 'M109', 'M140', 'M190', 'M106', 'M107',
- 'M112', 'M115', 'IGNORE', 'QUERY_ENDSTOPS', 'GET_POSITION', 'PID_TUNE',
+ 'M112', 'M115', 'IGNORE', 'QUERY_ENDSTOPS', 'GET_POSITION',
'RESTART', 'FIRMWARE_RESTART', 'ECHO', 'STATUS', 'HELP']
# G-Code movement commands
cmd_G1_aliases = ['G0']
@@ -569,18 +569,6 @@ class GCodeParser:
"gcode homing: %s" % (
mcu_pos, stepper_pos, kinematic_pos, toolhead_pos,
gcode_pos, origin_pos, homing_pos))
- cmd_PID_TUNE_help = "Run PID Tuning"
- cmd_PID_TUNE_aliases = ["M303"]
- def cmd_PID_TUNE(self, params):
- # Run PID tuning
- heater_index = self.get_int('E', params, 0)
- if (heater_index < -1 or heater_index >= len(self.heaters) - 1
- or self.heaters[heater_index] is None):
- self.respond_error("Heater not configured")
- heater = self.heaters[heater_index]
- temp = self.get_float('S', params)
- heater.start_auto_tune(temp)
- self.bg_temp(heater)
def request_restart(self, result):
if self.is_printer_ready:
self.respond_info("Preparing to restart...")
diff --git a/klippy/heater.py b/klippy/heater.py
index 7d746d22..d8021332 100644
--- a/klippy/heater.py
+++ b/klippy/heater.py
@@ -98,6 +98,7 @@ REPORT_TIME = 0.300
MAX_HEAT_TIME = 5.0
AMBIENT_TEMP = 25.
PID_PARAM_BASE = 255.
+PWM_DELAY = REPORT_TIME + SAMPLE_TIME*SAMPLE_COUNT
class error(Exception):
pass
@@ -141,8 +142,9 @@ class PrinterHeater:
# pwm caching
self.next_pwm_time = 0.
self.last_pwm_value = 0.
- # Load verify_heater module
+ # Load additional modules
printer.try_load_module(config, "verify_heater %s" % (self.name,))
+ printer.try_load_module(config, "pid_calibrate")
def set_pwm(self, read_time, value):
if self.target_temp <= 0.:
value = 0.
@@ -150,7 +152,7 @@ class PrinterHeater:
and abs(value - self.last_pwm_value) < 0.05):
# No significant change in value - can suppress update
return
- pwm_time = read_time + REPORT_TIME + SAMPLE_TIME*SAMPLE_COUNT
+ pwm_time = read_time + PWM_DELAY
self.next_pwm_time = pwm_time + 0.75 * MAX_HEAT_TIME
self.last_pwm_value = value
logging.debug("%s: pwm=%.3f@%.3f (from %.3f@%.3f [%.3f])",
@@ -181,16 +183,12 @@ class PrinterHeater:
def check_busy(self, eventtime):
with self.lock:
return self.control.check_busy(eventtime)
- def start_auto_tune(self, degrees):
- if degrees and (degrees < self.min_temp or degrees > self.max_temp):
- raise error("Requested temperature (%.1f) out of range (%.1f:%.1f)"
- % (degrees, self.min_temp, self.max_temp))
+ def set_control(self, control):
with self.lock:
- self.control = ControlAutoTune(self, self.control)
- self.target_temp = degrees
- def finish_auto_tune(self, old_control):
- self.control = old_control
- self.target_temp = 0
+ old_control = self.control
+ self.control = control
+ self.target_temp = 0.
+ return old_control
def stats(self, eventtime):
with self.lock:
target_temp = self.target_temp
@@ -278,125 +276,6 @@ class ControlPID:
return (abs(temp_diff) > PID_SETTLE_DELTA
or abs(self.prev_temp_deriv) > PID_SETTLE_SLOPE)
-
-######################################################################
-# Ziegler-Nichols PID autotuning
-######################################################################
-
-TUNE_PID_DELTA = 5.0
-
-class ControlAutoTune:
- def __init__(self, heater, old_control):
- self.heater = heater
- self.old_control = old_control
- self.heating = False
- self.peaks = []
- self.peak = 0.
- self.peak_time = 0.
- def adc_callback(self, read_time, temp):
- if self.heating and temp >= self.heater.target_temp:
- self.heating = False
- self.check_peaks()
- elif (not self.heating
- and temp <= self.heater.target_temp - TUNE_PID_DELTA):
- self.heating = True
- self.check_peaks()
- if self.heating:
- self.heater.set_pwm(read_time, self.heater.max_power)
- if temp < self.peak:
- self.peak = temp
- self.peak_time = read_time
- else:
- self.heater.set_pwm(read_time, 0.)
- if temp > self.peak:
- self.peak = temp
- self.peak_time = read_time
- def check_peaks(self):
- self.peaks.append((self.peak, self.peak_time))
- if self.heating:
- self.peak = 9999999.
- else:
- self.peak = -9999999.
- if len(self.peaks) < 4:
- return
- self.calc_pid(len(self.peaks)-1)
- def calc_pid(self, pos):
- temp_diff = self.peaks[pos][0] - self.peaks[pos-1][0]
- time_diff = self.peaks[pos][1] - self.peaks[pos-2][1]
- max_power = self.heater.max_power
- Ku = 4. * (2. * max_power) / (abs(temp_diff) * math.pi)
- Tu = time_diff
-
- Ti = 0.5 * Tu
- Td = 0.125 * Tu
- Kp = 0.6 * Ku * PID_PARAM_BASE
- Ki = Kp / Ti
- Kd = Kp * Td
- logging.info("Autotune: raw=%f/%f Ku=%f Tu=%f Kp=%f Ki=%f Kd=%f",
- temp_diff, max_power, Ku, Tu, Kp, Ki, Kd)
- return Kp, Ki, Kd
- def final_calc(self):
- cycle_times = [(self.peaks[pos][1] - self.peaks[pos-2][1], pos)
- for pos in range(4, len(self.peaks))]
- midpoint_pos = sorted(cycle_times)[len(cycle_times)/2][1]
- Kp, Ki, Kd = self.calc_pid(midpoint_pos)
- logging.info("Autotune: final: Kp=%f Ki=%f Kd=%f", Kp, Ki, Kd)
- gcode = self.heater.printer.lookup_object('gcode')
- gcode.respond_info(
- "PID parameters: pid_Kp=%.3f pid_Ki=%.3f pid_Kd=%.3f\n"
- "To use these parameters, update the printer config file with\n"
- "the above and then issue a RESTART command" % (Kp, Ki, Kd))
- def check_busy(self, eventtime):
- if self.heating or len(self.peaks) < 12:
- return True
- self.final_calc()
- self.heater.finish_auto_tune(self.old_control)
- return False
-
-
-######################################################################
-# Tuning information test
-######################################################################
-
-class ControlBumpTest:
- def __init__(self, heater, old_control):
- self.heater = heater
- self.old_control = old_control
- self.temp_samples = {}
- self.pwm_samples = {}
- self.state = 0
- def set_pwm(self, read_time, value):
- self.pwm_samples[read_time + 2*REPORT_TIME] = value
- self.heater.set_pwm(read_time, value)
- def adc_callback(self, read_time, temp):
- self.temp_samples[read_time] = temp
- if not self.state:
- self.set_pwm(read_time, 0.)
- if len(self.temp_samples) >= 20:
- self.state += 1
- elif self.state == 1:
- if temp < self.heater.target_temp:
- self.set_pwm(read_time, self.heater.max_power)
- return
- self.set_pwm(read_time, 0.)
- self.state += 1
- elif self.state == 2:
- self.set_pwm(read_time, 0.)
- if temp <= (self.heater.target_temp + AMBIENT_TEMP) / 2.:
- self.dump_stats()
- self.state += 1
- def dump_stats(self):
- out = ["%.3f %.1f %d" % (time, temp, self.pwm_samples.get(time, -1.))
- for time, temp in sorted(self.temp_samples.items())]
- f = open("/tmp/heattest.txt", "wb")
- f.write('\n'.join(out))
- f.close()
- def check_busy(self, eventtime):
- if self.state < 3:
- return True
- self.heater.finish_auto_tune(self.old_control)
- return False
-
def add_printer_objects(printer, config):
if config.has_section('heater_bed'):
printer.add_object('heater_bed', PrinterHeater(