c0d7b397d6b0464a737c54a8021129a6073aee70
[clinton/Smoothieware.git] / src / modules / tools / temperaturecontrol / TemperatureControl.cpp
1 /*
2 This file is part of Smoothie (http://smoothieware.org/). The motion control part is heavily based on Grbl (https://github.com/simen/grbl).
3 Smoothie is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
4 Smoothie is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
5 You should have received a copy of the GNU General Public License along with Smoothie. If not, see <http://www.gnu.org/licenses/>.
6 */
7
8 // TODO : THIS FILE IS LAME, MUST BE MADE MUCH BETTER
9
10 #include "libs/Module.h"
11 #include "libs/Kernel.h"
12 #include <math.h>
13 #include "TemperatureControl.h"
14 #include "TemperatureControlPool.h"
15 #include "libs/Pin.h"
16 #include "libs/Median.h"
17 #include "modules/robot/Conveyor.h"
18 #include "PublicDataRequest.h"
19 #include "TemperatureControlPublicAccess.h"
20
21 #include "MRI_Hooks.h"
22
23 #define UNDEFINED -1
24
25 #define thermistor_checksum CHECKSUM("thermistor")
26 #define r0_checksum CHECKSUM("r0")
27 #define readings_per_second_checksum CHECKSUM("readings_per_second")
28 #define max_pwm_checksum CHECKSUM("max_pwm")
29 #define pwm_frequency_checksum CHECKSUM("pwm_frequency")
30 #define bang_bang_checksum CHECKSUM("bang_bang")
31 #define hysteresis_checksum CHECKSUM("hysteresis")
32 #define t0_checksum CHECKSUM("t0")
33 #define beta_checksum CHECKSUM("beta")
34 #define vadc_checksum CHECKSUM("vadc")
35 #define vcc_checksum CHECKSUM("vcc")
36 #define r1_checksum CHECKSUM("r1")
37 #define r2_checksum CHECKSUM("r2")
38 #define thermistor_pin_checksum CHECKSUM("thermistor_pin")
39 #define heater_pin_checksum CHECKSUM("heater_pin")
40
41 #define get_m_code_checksum CHECKSUM("get_m_code")
42 #define set_m_code_checksum CHECKSUM("set_m_code")
43 #define set_and_wait_m_code_checksum CHECKSUM("set_and_wait_m_code")
44
45 #define designator_checksum CHECKSUM("designator")
46
47 #define p_factor_checksum CHECKSUM("p_factor")
48 #define i_factor_checksum CHECKSUM("i_factor")
49 #define d_factor_checksum CHECKSUM("d_factor")
50
51 #define i_max_checksum CHECKSUM("i_max")
52
53 #define preset1_checksum CHECKSUM("preset1")
54 #define preset2_checksum CHECKSUM("preset2")
55
56
57 TemperatureControl::TemperatureControl(uint16_t name) :
58 name_checksum(name), waiting(false), min_temp_violated(false) {}
59
60 void TemperatureControl::on_module_loaded(){
61
62 // We start not desiring any temp
63 this->target_temperature = UNDEFINED;
64
65 // Settings
66 this->on_config_reload(this);
67
68 this->acceleration_factor = 10;
69
70 // Register for events
71 register_for_event(ON_CONFIG_RELOAD);
72 this->register_for_event(ON_GCODE_EXECUTE);
73 this->register_for_event(ON_GCODE_RECEIVED);
74 this->register_for_event(ON_MAIN_LOOP);
75 this->register_for_event(ON_SECOND_TICK);
76 this->register_for_event(ON_GET_PUBLIC_DATA);
77 this->register_for_event(ON_SET_PUBLIC_DATA);
78 }
79
80 void TemperatureControl::on_main_loop(void* argument){
81 if (this->min_temp_violated) {
82 THEKERNEL->streams->printf("Error: MINTEMP triggered on P%d.%d! check your thermistors!\n", this->thermistor_pin.port_number, this->thermistor_pin.pin);
83 this->min_temp_violated = false;
84 }
85 }
86
87 // Get configuration from the config file
88 void TemperatureControl::on_config_reload(void* argument){
89
90 // General config
91 this->set_m_code = THEKERNEL->config->value(temperature_control_checksum, this->name_checksum, set_m_code_checksum)->by_default(104)->as_number();
92 this->set_and_wait_m_code = THEKERNEL->config->value(temperature_control_checksum, this->name_checksum, set_and_wait_m_code_checksum)->by_default(109)->as_number();
93 this->get_m_code = THEKERNEL->config->value(temperature_control_checksum, this->name_checksum, get_m_code_checksum)->by_default(105)->as_number();
94 this->readings_per_second = THEKERNEL->config->value(temperature_control_checksum, this->name_checksum, readings_per_second_checksum)->by_default(20)->as_number();
95
96 this->designator = THEKERNEL->config->value(temperature_control_checksum, this->name_checksum, designator_checksum)->by_default(string("T"))->as_string();
97
98 // Values are here : http://reprap.org/wiki/Thermistor
99 this->r0 = 100000;
100 this->t0 = 25;
101 this->beta = 4066;
102 this->r1 = 0;
103 this->r2 = 4700;
104
105 // Preset values for various common types of thermistors
106 ConfigValue* thermistor = THEKERNEL->config->value(temperature_control_checksum, this->name_checksum, thermistor_checksum);
107 if( thermistor->value.compare("EPCOS100K" ) == 0 ){ // Default
108 }else if( thermistor->value.compare("RRRF100K" ) == 0 ){ this->beta = 3960;
109 }else if( thermistor->value.compare("RRRF10K" ) == 0 ){ this->beta = 3964; this->r0 = 10000; this->r1 = 680; this->r2 = 1600;
110 }else if( thermistor->value.compare("Honeywell100K") == 0 ){ this->beta = 3974;
111 }else if( thermistor->value.compare("Semitec" ) == 0 ){ this->beta = 4267;
112 }else if( thermistor->value.compare("HT100K" ) == 0 ){ this->beta = 3990; }
113
114 // Preset values are overriden by specified values
115 this->r0 = THEKERNEL->config->value(temperature_control_checksum, this->name_checksum, r0_checksum )->by_default(this->r0 )->as_number(); // Stated resistance eg. 100K
116 this->t0 = THEKERNEL->config->value(temperature_control_checksum, this->name_checksum, t0_checksum )->by_default(this->t0 )->as_number(); // Temperature at stated resistance, eg. 25C
117 this->beta = THEKERNEL->config->value(temperature_control_checksum, this->name_checksum, beta_checksum)->by_default(this->beta)->as_number(); // Thermistor beta rating. See http://reprap.org/bin/view/Main/MeasuringThermistorBeta
118 this->r1 = THEKERNEL->config->value(temperature_control_checksum, this->name_checksum, r1_checksum )->by_default(this->r1 )->as_number();
119 this->r2 = THEKERNEL->config->value(temperature_control_checksum, this->name_checksum, r2_checksum )->by_default(this->r2 )->as_number();
120
121 this->preset1 = THEKERNEL->config->value(temperature_control_checksum, this->name_checksum, preset1_checksum)->by_default(0)->as_number();
122 this->preset2 = THEKERNEL->config->value(temperature_control_checksum, this->name_checksum, preset2_checksum)->by_default(0)->as_number();
123
124
125 // Thermistor math
126 j = (1.0 / beta);
127 k = (1.0 / (t0 + 273.15));
128
129 // sigma-delta output modulation
130 o = 0;
131
132 // Thermistor pin for ADC readings
133 this->thermistor_pin.from_string(THEKERNEL->config->value(temperature_control_checksum, this->name_checksum, thermistor_pin_checksum )->required()->as_string());
134 THEKERNEL->adc->enable_pin(&thermistor_pin);
135
136 // Heater pin
137 this->heater_pin.from_string( THEKERNEL->config->value(temperature_control_checksum, this->name_checksum, heater_pin_checksum)->required()->as_string())->as_output();
138 this->heater_pin.max_pwm( THEKERNEL->config->value(temperature_control_checksum, this->name_checksum, max_pwm_checksum)->by_default(255)->as_number() );
139
140 this->heater_pin.set(0);
141 this->heater_on= false;
142
143 // used to enable bang bang control of heater
144 this->use_bangbang= THEKERNEL->config->value(temperature_control_checksum, this->name_checksum, bang_bang_checksum)->by_default(false)->as_bool();
145 this->hysteresis= THEKERNEL->config->value(temperature_control_checksum, this->name_checksum, hysteresis_checksum)->by_default(2)->as_number();
146
147 set_low_on_debug(heater_pin.port_number, heater_pin.pin);
148
149 // activate SD-DAC timer
150 THEKERNEL->slow_ticker->attach( THEKERNEL->config->value(temperature_control_checksum, this->name_checksum, pwm_frequency_checksum)->by_default(2000)->as_number() , &heater_pin, &Pwm::on_tick);
151
152 // reading tick
153 THEKERNEL->slow_ticker->attach( this->readings_per_second, this, &TemperatureControl::thermistor_read_tick );
154 this->PIDdt= 1.0 / this->readings_per_second;
155
156 // PID
157 setPIDp( THEKERNEL->config->value(temperature_control_checksum, this->name_checksum, p_factor_checksum)->by_default(10 )->as_number() );
158 setPIDi( THEKERNEL->config->value(temperature_control_checksum, this->name_checksum, i_factor_checksum)->by_default(0.3f)->as_number() );
159 setPIDd( THEKERNEL->config->value(temperature_control_checksum, this->name_checksum, d_factor_checksum)->by_default(200)->as_number() );
160 // set to the same as max_pwm by default
161 this->i_max = THEKERNEL->config->value(temperature_control_checksum, this->name_checksum, i_max_checksum )->by_default(this->heater_pin.max_pwm())->as_number();
162 this->iTerm = 0.0;
163 this->lastInput= -1.0;
164 this->last_reading = 0.0;
165 }
166
167 void TemperatureControl::on_gcode_received(void* argument){
168 Gcode* gcode = static_cast<Gcode*>(argument);
169 if (gcode->has_m) {
170 // Get temperature
171 if( gcode->m == this->get_m_code ){
172 char buf[32]; // should be big enough for any status
173 int n= snprintf(buf, sizeof(buf), "%s:%3.1f /%3.1f @%d ", this->designator.c_str(), this->get_temperature(), ((target_temperature == UNDEFINED)?0.0:target_temperature), this->o);
174 gcode->txt_after_ok.append(buf, n);
175 gcode->mark_as_taken();
176
177 } else if (gcode->m == 301) {
178 gcode->mark_as_taken();
179 if (gcode->has_letter('S') && (gcode->get_value('S') == this->pool_index))
180 {
181 if (gcode->has_letter('P'))
182 setPIDp( gcode->get_value('P') );
183 if (gcode->has_letter('I'))
184 setPIDi( gcode->get_value('I') );
185 if (gcode->has_letter('D'))
186 setPIDd( gcode->get_value('D') );
187 if (gcode->has_letter('X'))
188 this->i_max = gcode->get_value('X');
189 }
190 //gcode->stream->printf("%s(S%d): Pf:%g If:%g Df:%g X(I_max):%g Pv:%g Iv:%g Dv:%g O:%d\n", this->designator.c_str(), this->pool_index, this->p_factor, this->i_factor/this->PIDdt, this->d_factor*this->PIDdt, this->i_max, this->p, this->i, this->d, o);
191 gcode->stream->printf("%s(S%d): Pf:%g If:%g Df:%g X(I_max):%g O:%d\n", this->designator.c_str(), this->pool_index, this->p_factor, this->i_factor/this->PIDdt, this->d_factor*this->PIDdt, this->i_max, o);
192
193 } else if (gcode->m == 303) {
194 if (gcode->has_letter('E') && (gcode->get_value('E') == this->pool_index)) {
195 gcode->mark_as_taken();
196 float target = 150.0;
197 if (gcode->has_letter('S')) {
198 target = gcode->get_value('S');
199 gcode->stream->printf("Target: %5.1f\n", target);
200 }
201 int ncycles= 8;
202 if (gcode->has_letter('C')) {
203 ncycles= gcode->get_value('C');
204 }
205 gcode->stream->printf("Start PID tune, command is %s\n", gcode->command.c_str());
206 this->pool->PIDtuner->begin(this, target, gcode->stream, ncycles);
207 }
208
209 } else if (gcode->m == 500 || gcode->m == 503){// M500 saves some volatile settings to config override file, M503 just prints the settings
210 gcode->stream->printf(";PID settings:\nM301 S%d P%1.4f I%1.4f D%1.4f\n", this->pool_index, this->p_factor, this->i_factor/this->PIDdt, this->d_factor*this->PIDdt);
211 gcode->mark_as_taken();
212
213 } else if( ( gcode->m == this->set_m_code || gcode->m == this->set_and_wait_m_code ) && gcode->has_letter('S') ) {
214 // Attach gcodes to the last block for on_gcode_execute
215 THEKERNEL->conveyor->append_gcode(gcode);
216
217 // push an empty block if we have to wait, so the Planner can get things right, and we can prevent subsequent non-move gcodes from executing
218 if (gcode->m == this->set_and_wait_m_code)
219 // ensure that no subsequent gcodes get executed with our M109 or similar
220 THEKERNEL->conveyor->queue_head_block();
221 }
222 }
223 }
224
225 void TemperatureControl::on_gcode_execute(void* argument){
226 Gcode* gcode = static_cast<Gcode*>(argument);
227 if( gcode->has_m){
228 if (((gcode->m == this->set_m_code) || (gcode->m == this->set_and_wait_m_code))
229 && gcode->has_letter('S'))
230 {
231 float v = gcode->get_value('S');
232
233 if (v == 0.0)
234 {
235 this->target_temperature = UNDEFINED;
236 this->heater_pin.set(0);
237 }
238 else
239 {
240 this->set_desired_temperature(v);
241
242 if( gcode->m == this->set_and_wait_m_code)
243 {
244 THEKERNEL->pauser->take();
245 this->waiting = true;
246 }
247 }
248 }
249 }
250 }
251
252 void TemperatureControl::on_get_public_data(void* argument){
253 PublicDataRequest* pdr = static_cast<PublicDataRequest*>(argument);
254
255 if(!pdr->starts_with(temperature_control_checksum)) return;
256
257 if(!pdr->second_element_is(this->name_checksum)) return; // will be bed or hotend
258
259 // ok this is targeted at us, so send back the requested data
260 if(pdr->third_element_is(current_temperature_checksum)) {
261 // this must be static as it will be accessed long after we have returned
262 static struct pad_temperature temp_return;
263 temp_return.current_temperature= this->get_temperature();
264 temp_return.target_temperature= (target_temperature == UNDEFINED) ? 0 : this->target_temperature;
265 temp_return.pwm= this->o;
266
267 pdr->set_data_ptr(&temp_return);
268 pdr->set_taken();
269 }
270 }
271
272 void TemperatureControl::on_set_public_data(void* argument){
273 PublicDataRequest* pdr = static_cast<PublicDataRequest*>(argument);
274
275 if(!pdr->starts_with(temperature_control_checksum)) return;
276
277 if(!pdr->second_element_is(this->name_checksum)) return; // will be bed or hotend
278
279 // ok this is targeted at us, so set the temp
280 float t= *static_cast<float*>(pdr->get_data_ptr());
281 this->set_desired_temperature(t);
282 pdr->set_taken();
283 }
284
285 void TemperatureControl::set_desired_temperature(float desired_temperature)
286 {
287 if (desired_temperature == 1.0)
288 desired_temperature = preset1;
289 else if (desired_temperature == 2.0)
290 desired_temperature = preset2;
291
292 target_temperature = desired_temperature;
293 if (desired_temperature == 0.0)
294 heater_pin.set((o = 0));
295 }
296
297 float TemperatureControl::get_temperature(){
298 return last_reading;
299 }
300
301 float TemperatureControl::adc_value_to_temperature(int adc_value)
302 {
303 if ((adc_value == 4095) || (adc_value == 0))
304 return INFINITY;
305 float r = r2 / ((4095.0 / adc_value) - 1.0);
306 if (r1 > 0)
307 r = (r1 * r) / (r1 - r);
308 return (1.0 / (k + (j * log(r / r0)))) - 273.15;
309 }
310
311 uint32_t TemperatureControl::thermistor_read_tick(uint32_t dummy){
312 int r = new_thermistor_reading();
313
314 float temperature = adc_value_to_temperature(r);
315
316 if (target_temperature > 0)
317 {
318 if ((r <= 1) || (r >= 4094))
319 {
320 this->min_temp_violated = true;
321 target_temperature = UNDEFINED;
322 heater_pin.set(0);
323 }
324 else
325 {
326 pid_process(temperature);
327 if ((temperature > target_temperature) && waiting)
328 {
329 THEKERNEL->pauser->release();
330 waiting = false;
331 }
332 }
333 }
334 else
335 {
336 heater_pin.set((o = 0));
337 }
338 last_reading = temperature;
339 return 0;
340 }
341
342 /**
343 * Based on https://github.com/br3ttb/Arduino-PID-Library
344 */
345 void TemperatureControl::pid_process(float temperature)
346 {
347 if(use_bangbang) {
348 // bang bang if very simple, if temp is < target - hysteresis turn on full else if temp is > target + hysteresis turn heater off
349 // good for relays
350 if(temperature > target_temperature+hysteresis && heater_on) {
351 heater_pin.set(false);
352 heater_on= false;
353 this->o= 0; // for display purposes only
354
355 }else if(temperature < target_temperature-hysteresis && !heater_on) {
356 if(heater_pin.max_pwm() >= 255) {
357 // turn on full
358 this->heater_pin.set(true);
359 this->o= 255; // for display purposes only
360 }else{
361 // only to whatever max pwm is configured
362 this->heater_pin.pwm(heater_pin.max_pwm());
363 this->o= heater_pin.max_pwm(); // for display purposes only
364 }
365 heater_on= true;
366 }
367 return;
368 }
369
370 // regular PID control
371 float error = target_temperature - temperature;
372 this->iTerm += (error * this->i_factor);
373 if (this->iTerm > this->i_max) this->iTerm = this->i_max;
374 else if (this->iTerm < 0.0) this->iTerm = 0.0;
375
376 if(this->lastInput < 0.0) this->lastInput= temperature; // set first time
377 float d= (temperature - this->lastInput);
378
379 // calculate the PID output
380 // TODO does this need to be scaled by max_pwm/256? I think not as p_factor already does that
381 this->o = (this->p_factor*error) + this->iTerm - (this->d_factor*d);
382
383 if (this->o >= heater_pin.max_pwm())
384 this->o = heater_pin.max_pwm();
385 else if (this->o < 0)
386 this->o = 0;
387
388 this->heater_pin.pwm(this->o);
389 this->lastInput= temperature;
390 }
391
392 int TemperatureControl::new_thermistor_reading()
393 {
394 int last_raw = THEKERNEL->adc->read(&thermistor_pin);
395 if (queue.size() >= queue.capacity()) {
396 uint16_t l;
397 queue.pop_front(l);
398 }
399 uint16_t r = last_raw;
400 queue.push_back(r);
401 for (int i=0; i<queue.size(); i++)
402 median_buffer[i] = *queue.get_ref(i);
403 uint16_t m = median_buffer[quick_median(median_buffer, queue.size())];
404 return m;
405 }
406
407 void TemperatureControl::on_second_tick(void* argument)
408 {
409 if (waiting)
410 THEKERNEL->streams->printf("%s:%3.1f /%3.1f @%d\n", designator.c_str(), get_temperature(), ((target_temperature == UNDEFINED)?0.0:target_temperature), o);
411 }
412
413 void TemperatureControl::setPIDp(float p) {
414 this->p_factor= p;
415 }
416
417 void TemperatureControl::setPIDi(float i) {
418 this->i_factor= i*this->PIDdt;
419 }
420
421 void TemperatureControl::setPIDd(float d) {
422 this->d_factor= d/this->PIDdt;
423 }