1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
|
# 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
from . import heaters
class PIDCalibrate:
def __init__(self, config):
self.printer = config.get_printer()
gcode = self.printer.lookup_object("gcode")
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, gcmd):
heater_name = gcmd.get("HEATER")
target = gcmd.get_float("TARGET")
write_file = gcmd.get_int("WRITE_FILE", 0)
pheaters = self.printer.lookup_object("heaters")
try:
heater = pheaters.lookup_heater(heater_name)
except self.printer.config_error as e:
raise gcmd.error(str(e))
self.printer.lookup_object("toolhead").get_last_move_time()
calibrate = ControlAutoTune(heater, target)
old_control = heater.set_control(calibrate)
try:
pheaters.set_temperature(heater, target, True)
except self.printer.command_error as e:
heater.set_control(old_control)
raise
heater.set_control(old_control)
if write_file:
calibrate.write_file("/tmp/heattest.txt")
if calibrate.check_busy(0.0, 0.0, 0.0):
raise gcmd.error("pid_calibrate interrupted")
# Log and report results
Kp, Ki, Kd = calibrate.calc_final_pid()
logging.info("Autotune: final: Kp=%f Ki=%f Kd=%f", Kp, Ki, Kd)
gcmd.respond_info(
"PID parameters: pid_Kp=%.3f pid_Ki=%.3f pid_Kd=%.3f\n"
"The SAVE_CONFIG command will update the printer config file\n"
"with these parameters and restart the printer." % (Kp, Ki, Kd)
)
# Store results for SAVE_CONFIG
cfgname = heater.get_name()
configfile = self.printer.lookup_object("configfile")
configfile.set(cfgname, "control", "pid")
configfile.set(cfgname, "pid_Kp", "%.3f" % (Kp,))
configfile.set(cfgname, "pid_Ki", "%.3f" % (Ki,))
configfile.set(cfgname, "pid_Kd", "%.3f" % (Kd,))
TUNE_PID_DELTA = 5.0
class ControlAutoTune:
def __init__(self, heater, target):
self.heater = heater
self.heater_max_power = heater.get_max_power()
self.calibrate_temp = target
# Heating control
self.heating = False
self.peak = 0.0
self.peak_time = 0.0
# Peak recording
self.peaks = []
# Sample recording
self.last_pwm = 0.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 + self.heater.get_pwm_delay(), value))
self.last_pwm = value
self.heater.set_pwm(read_time, value)
def temperature_update(self, read_time, temp, target_temp):
self.temp_samples.append((read_time, temp))
# Check if the temperature has crossed the target and
# enable/disable the heater if so.
if self.heating and temp >= target_temp:
self.heating = False
self.check_peaks()
self.heater.alter_target(self.calibrate_temp - TUNE_PID_DELTA)
elif not self.heating and temp <= target_temp:
self.heating = True
self.check_peaks()
self.heater.alter_target(self.calibrate_temp)
# Check if this temperature is a peak and record it if so
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.0)
if temp > self.peak:
self.peak = temp
self.peak_time = read_time
def check_busy(self, eventtime, smoothed_temp, target_temp):
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.0
else:
self.peak = -9999999.0
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]
# Use Astrom-Hagglund method to estimate Ku and Tu
amplitude = 0.5 * abs(temp_diff)
Ku = 4.0 * self.heater_max_power / (math.pi * amplitude)
Tu = time_diff
# Use Ziegler-Nichols method to generate PID parameters
Ti = 0.5 * Tu
Td = 0.125 * Tu
Kp = 0.6 * Ku * heaters.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,
self.heater_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, "w")
f.write("\n".join(pwm + out))
f.close()
def load_config(config):
return PIDCalibrate(config)
|