Welcome to mirror list, hosted at ThFree Co, Russian Federation.

Pid.cpp « Heating « src - github.com/Duet3D/RepRapFirmware.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
blob: 88ca87ec1b580fa612bdf6018fdc43d567c60637 (plain)
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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
/*
 * Pid.cpp
 *
 *  Created on: 21 Jul 2016
 *      Author: David
 */

#include "Pid.h"
#include "GCodes/GCodes.h"
#include "Heat.h"
#include "Platform.h"
#include "RepRap.h"

// Private constants
const uint32_t InitialTuningReadingInterval = 250;	// the initial reading interval in milliseconds
const uint32_t TempSettleTimeout = 20000;	// how long we allow the initial temperature to settle

// Static class variables

float *PID::tuningTempReadings = nullptr;	// the readings from the heater being tuned
float PID::tuningStartTemp;					// the temperature when we turned on the heater
float PID::tuningPwm;						// the PWM to use
float PID::tuningTargetTemp;				// the maximum temperature we are allowed to reach
uint32_t PID::tuningBeginTime;				// when we started the tuning process
uint32_t PID::tuningPhaseStartTime;			// when we started the current tuning phase
uint32_t PID::tuningReadingInterval;		// how often we are sampling
size_t PID::tuningReadingsTaken;			// how many samples we have taken

float PID::tuningHeaterOffTemp;				// the temperature when we turned the heater off
float PID::tuningPeakTemperature;			// the peak temperature reached, averaged over 3 readings (so slightly less than the true peak)
uint32_t PID::tuningHeatingTime;			// how long we had the heating on for
uint32_t PID::tuningPeakDelay;				// how many milliseconds the temperature continues to rise after turning the heater off

// Member functions and constructors

PID::PID(Platform& p, int8_t h) : platform(p), heater(h), mode(HeaterMode::off)
{
}

inline void PID::SetHeater(float power) const
{
	platform.SetHeater(heater, power);
}

void PID::Init(float pGain, float pTc, float pTd, float tempLimit, bool usePid)
{
	temperatureLimit = tempLimit;
	maxTempExcursion = DefaultMaxTempExcursion;
	maxHeatingFaultTime = DefaultMaxHeatingFaultTime;
	model.SetParameters(pGain, pTc, pTd, 1.0, usePid);
	Reset();

	if (model.IsEnabled())
	{
		SetHeater(0.0);
	}

	// Time the sensor was last sampled.  During startup, we use the current
	// time as the initial value so as to not trigger an immediate warning from the Tick ISR.
	lastSampleTime = millis();
}

void PID::Reset()
{
	mode = HeaterMode::off;
	previousTemperaturesGood = 0;
	previousTemperatureIndex = 0;
	activeTemperature = 0.0;
	standbyTemperature = 0.0;
	iAccumulator = 0.0;
	badTemperatureCount = 0;
	active = false; 						// default to standby temperature
	tuned = false;
	averagePWM = lastPwm = 0.0;
	heatingFaultCount = 0;
	temperature = BAD_ERROR_TEMPERATURE;
}

// Set the process model
bool PID::SetModel(float gain, float tc, float td, float maxPwm, bool usePid)
{
	const bool rslt = model.SetParameters(gain, tc, td, maxPwm, usePid);
	if (rslt)
	{
#if !defined(DUET_NG) && !defined(__RADDS__) && !defined(__ALLIGATOR__)
		if (heater == HEATERS - 1)
		{
			// The last heater on the Duet 0.8.5 + DueX4 shares its pin with Fan1
			platform.EnableSharedFan(!model.IsEnabled());
		}
#endif
		if (model.IsEnabled())
		{
			const float safeGain = (heater == reprap.GetHeat().GetBedHeater() || heater == reprap.GetHeat().GetChamberHeater())
									? 170.0 : 480.0;
			if (gain > safeGain)
			{
				platform.MessageF(GENERIC_MESSAGE,
						"Warning: Heater %u appears to be over-powered. If left on at full power, its temperature is predicted to reach %uC.\n",
						heater, (unsigned int)gain + 20);
			}
		}
		else
		{
			Reset();
		}
	}
	return rslt;
}

// Read and store the temperature of this heater and returns the error code.
TemperatureError PID::ReadTemperature()
{
	TemperatureError err = TemperatureError::success;				// assume no error
	temperature = platform.GetTemperature(heater, err);			// in the event of an error, err is set and BAD_ERROR_TEMPERATURE is returned
	if (err == TemperatureError::success)
	{
		if (temperature < BAD_LOW_TEMPERATURE)
		{
			err = TemperatureError::openCircuit;
		}
		else if (temperature > temperatureLimit)
		{
			err = TemperatureError::tooHigh;
		}
	}
	return err;
}

// This must be called whenever the heater is turned on, and any time the heater is active and the target temperature is changed
void PID::SwitchOn()
{
	if (mode == HeaterMode::fault)
	{
		if (reprap.Debug(Module::moduleHeat))
		{
			platform.MessageF(GENERIC_MESSAGE, "Heater %d not switched on due to temperature fault\n", heater);
		}
	}
	else if (model.IsEnabled())
	{
//debugPrintf("Heater %d on temp %.1f\n", heater, temperature);
		const float target = (active) ? activeTemperature : standbyTemperature;
		const HeaterMode oldMode = mode;
		mode = (temperature + TEMPERATURE_CLOSE_ENOUGH < target) ? HeaterMode::heating
				: (temperature > target + TEMPERATURE_CLOSE_ENOUGH) ? HeaterMode::cooling
					: HeaterMode::stable;
		if (mode != oldMode)
		{
			heatingFaultCount = 0;
			if (mode == HeaterMode::heating)
			{
				timeSetHeating = millis();
			}
			if (reprap.Debug(Module::moduleHeat) && oldMode == HeaterMode::off)
			{
				platform.MessageF(GENERIC_MESSAGE, "Heater %d switched on\n", heater);
			}
		}
	}
}

// Switch off the specified heater. If in tuning mode, delete the array used to store tuning temperature readings.
void PID::SwitchOff()
{
	lastPwm = 0.0;
	if (model.IsEnabled())
	{
		SetHeater(0.0);
		if (IsTuning())
		{
			delete tuningTempReadings;
			tuningTempReadings = nullptr;
		}
		if (mode > HeaterMode::off)
		{
			mode = HeaterMode::off;
			if (reprap.Debug(Module::moduleHeat))
			{
				platform.MessageF(GENERIC_MESSAGE, "Heater %d switched off\n", heater);
			}
		}
	}
}

// This is the main heater control loop function
void PID::Spin()
{
	if (model.IsEnabled())
	{
		// Read the temperature
		const TemperatureError err = ReadTemperature();

		// Handle any temperature reading error and calculate the temperature rate of change, if possible
		if (err != TemperatureError::success)
		{
			previousTemperaturesGood <<= 1;				// this reading isn't a good one
			if (mode > HeaterMode::off)					// don't worry about errors when reading heaters that are switched off or flagged as having faults
			{
				// Error may be a temporary error and may correct itself after a few additional reads
				badTemperatureCount++;
				if (badTemperatureCount > MAX_BAD_TEMPERATURE_COUNT)
				{
					lastPwm = 0.0;
					SetHeater(0.0);						// do this here just to be sure, in case the call to platform.Message causes a delay
					if (IsTuning())
					{
						delete tuningTempReadings;
						tuningTempReadings = nullptr;
					}
					mode = HeaterMode::fault;
					platform.MessageF(GENERIC_MESSAGE, "Error: Temperature reading fault on heater %d: %s\n", heater, TemperatureErrorString(err));
					reprap.FlagTemperatureFault(heater);
				}
			}
			// We leave lastPWM alone if we have a temporary temperature reading error
		}
		else
		{
			// We have an apparently-good temperature reading. Calculate the derivative, if possible.
			float derivative = 0.0;
			bool gotDerivative = false;
			badTemperatureCount = 0;
			if ((previousTemperaturesGood & (1 << (NumPreviousTemperatures - 1))) != 0)
			{
				const float tentativeDerivative = SecondsToMillis * (temperature - previousTemperatures[previousTemperatureIndex])
								/ (float)(platform.HeatSampleInterval() * NumPreviousTemperatures);
				// Some sensors give occasional temperature spikes. We don't expect the temperature to increase by more than 10C/second.
				if (fabsf(tentativeDerivative) <= 10.0)
				{
					derivative = tentativeDerivative;
					gotDerivative = true;
				}
			}
			previousTemperatures[previousTemperatureIndex] = temperature;
			previousTemperaturesGood = (previousTemperaturesGood << 1) | 1;

			// Get the target temperature and the error
			const float targetTemperature = (active) ? activeTemperature : standbyTemperature;
			const float error = targetTemperature - temperature;

			// Do the heating checks
			switch(mode)
			{
			case HeaterMode::heating:
				{
					if (error <= TEMPERATURE_CLOSE_ENOUGH)
					{
						mode = HeaterMode::stable;
						heatingFaultCount = 0;
					}
					else if (gotDerivative)
					{
						const float expectedRate = GetExpectedHeatingRate();
						if (derivative + AllowedTemperatureDerivativeNoise < expectedRate
							&& (float)(millis() - timeSetHeating) > model.GetDeadTime() * SecondsToMillis * 2)
						{
							++heatingFaultCount;
							if (heatingFaultCount * platform.HeatSampleInterval() > maxHeatingFaultTime * SecondsToMillis)
							{
								SetHeater(0.0);					// do this here just to be sure
								mode = HeaterMode::fault;
								reprap.GetGCodes().CancelPrint();
								platform.MessageF(GENERIC_MESSAGE,
											"Error: heating fault on heater %d, temperature rising much more slowly than the expected %.1f" DEGREE_SYMBOL "C/sec\n",
											heater, expectedRate);
								reprap.FlagTemperatureFault(heater);
							}
						}
						else if (heatingFaultCount != 0)
						{
							--heatingFaultCount;
						}
					}
					else
					{
						// Leave the heating fault count alone
					}
				}
				break;

			case HeaterMode::stable:
				if (fabsf(error) > maxTempExcursion && temperature > MaxAmbientTemperature)
				{
					++heatingFaultCount;
					if (heatingFaultCount * platform.HeatSampleInterval() > maxHeatingFaultTime * SecondsToMillis)
					{
						SetHeater(0.0);					// do this here just to be sure
						mode = HeaterMode::fault;
						reprap.GetGCodes().CancelPrint();
						platform.MessageF(GENERIC_MESSAGE, "Error: heating fault on heater %d, temperature excursion exceeded %.1fC\n", heater, maxTempExcursion);
					}
				}
				else if (heatingFaultCount != 0)
				{
					--heatingFaultCount;
				}
				break;

			case HeaterMode::cooling:
				if (-error <= TEMPERATURE_CLOSE_ENOUGH && targetTemperature > MaxAmbientTemperature)
				{
					// We have cooled to close to the target temperature, so we should now maintain that temperature
					mode = HeaterMode::stable;
					heatingFaultCount = 0;
				}
				else
				{
					// We could check for temperature excessive or not falling here, but without an alarm or a power-off mechanism, there is not much we can do
					// TODO emergency stop?
				}
				break;

			default:		// this covers off, fault, and the auto tuning states
				break;
			}

			// Calculate the PWM
			if (mode <= HeaterMode::off)
			{
				lastPwm = 0.0;
			}
			else if (mode < HeaterMode::tuning0)
			{
				// Performing normal temperature control
				if (model.UsePid())
				{
					// Using PID mode. Determine the PID parameters to use.
					const bool inLoadMode = (mode == HeaterMode::stable);	// use standard PID when maintaining temperature
					const PidParameters& params = model.GetPidParameters(inLoadMode);

					// If the P and D terms together demand that the heater is full on or full off, disregard the I term
					const float errorMinusDterm = error - (params.tD * derivative);
					const float pPlusD = params.kP * errorMinusDterm;
					const float expectedPwm = constrain<float>((temperature - NormalAmbientTemperature)/model.GetGain(), 0.0, model.GetMaxPwm());
					if (pPlusD + expectedPwm > model.GetMaxPwm())
					{
						lastPwm = model.GetMaxPwm();
						// If we are heating up, preset the I term to the expected PWM at this temperature, ready for the switch over to PID
						if (mode == HeaterMode::heating && error > 0.0 && derivative > 0.0)
						{
							iAccumulator = expectedPwm;
						}
					}
					else if (pPlusD + expectedPwm < 0.0)
					{
						lastPwm = 0.0;
					}
					else
					{
						// In the following we use a modified PID when the temperature is a long way off target.
						// During initial heating or cooling, the D term represents expected overshoot, which we don't want to add to the I accumulator.
						// When we are in load mode, the I term is much larger and the D term doesn't represent overshoot, so use normal PID.
						const float errorToUse = (inLoadMode || model.ArePidParametersOverridden()) ? error : errorMinusDterm;
						iAccumulator = constrain<float>
										(iAccumulator + (errorToUse * params.kP * params.recipTi * platform.HeatSampleInterval() * MillisToSeconds),
											0.0, model.GetMaxPwm());
						lastPwm = constrain<float>(pPlusD + iAccumulator, 0.0, model.GetMaxPwm());
					}
				}
				else
				{
					// Using bang-bang mode
					lastPwm = (error > 0.0) ? model.GetMaxPwm() : 0.0;
				}
			}
			else
			{
				DoTuningStep();
			}
		}

		// Set the heater power and update the average PWM
		SetHeater(lastPwm);
		averagePWM = averagePWM * (1.0 - platform.HeatSampleInterval()/(HEAT_PWM_AVERAGE_TIME * SecondsToMillis)) + lastPwm;
		previousTemperatureIndex = (previousTemperatureIndex + 1) % NumPreviousTemperatures;

		// For temperature sensors which do not require frequent sampling and averaging,
		// their temperature is read here and error/safety handling performed.  However,
		// unlike the Tick ISR, this code is not executed at interrupt level and consequently
		// runs the risk of having undesirable delays between calls.  To guard against this,
		// we record for each PID object when it was last sampled and have the Tick ISR
		// take action if there is a significant delay since the time of last sampling.
		lastSampleTime = millis();

//  	debugPrintf("Heater %d: e=%f, P=%f, I=%f, d=%f, r=%f\n", heater, error, pp.kP*error, temp_iState, temp_dState, result);
	}
}

void PID::SetActiveTemperature(float t)
{
	if (t > temperatureLimit)
	{
		platform.MessageF(GENERIC_MESSAGE, "Error: Temperature %.1f too high for heater %d!\n", t, heater);
	}
	else
	{
		activeTemperature = t;
		if (mode > HeaterMode::off && active)
		{
			SwitchOn();
		}
	}
}

void PID::SetStandbyTemperature(float t)
{
	if (t > temperatureLimit)
	{
		platform.MessageF(GENERIC_MESSAGE, "Error: Temperature %.1f too high for heater %d!\n", t, heater);
	}
	else
	{
		standbyTemperature = t;
		if (mode > HeaterMode::off && !active)
		{
			SwitchOn();
		}
	}
}

void PID::Activate()
{
	if (mode != HeaterMode::fault)
	{
		active = true;
		SwitchOn();
	}
}

void PID::Standby()
{
	if (mode != HeaterMode::fault)
	{
		active = false;
		SwitchOn();
	}
}

void PID::ResetFault()
{
	mode = HeaterMode::off;
	SwitchOff();
	badTemperatureCount = 0;
}

float PID::GetAveragePWM() const
{
	return averagePWM * platform.HeatSampleInterval()/(HEAT_PWM_AVERAGE_TIME * SecondsToMillis);
}

// Get a conservative estimate of the expected heating rate at the current temperature and average PWM. The result may be negative.
float PID::GetExpectedHeatingRate() const
{
	// In the following we allow for the gain being only 75% of what we think it should be, to avoid false alarms
	const float maxTemperatureRise = 0.75 * model.GetGain() * GetAveragePWM();		// this is the highest temperature above ambient we expect the heater can reach at this PWM
	const float initialHeatingRate = maxTemperatureRise/model.GetTimeConstant();	// this is the expected heating rate at ambient temperature
	return (maxTemperatureRise >= 20.0)
			? (maxTemperatureRise + NormalAmbientTemperature - temperature) * initialHeatingRate/maxTemperatureRise
			: 0.0;
}

// Auto tune this PID
void PID::StartAutoTune(float targetTemp, float maxPwm, StringRef& reply)
{
	// Starting an auto tune
	if (!model.IsEnabled())
	{
		reply.printf("Error: heater %d cannot be auto tuned while it is disabled", heater);
	}
	else if (lastPwm > 0.0 || GetAveragePWM() > 0.02)
	{
		reply.printf("Error: heater %d must be off and cold before auto tuning it", heater);
	}
	else
	{
		const TemperatureError err = ReadTemperature();
		if (err != TemperatureError::success)
		{
			reply.printf("Error: heater %d reported error '%s' at start of auto tuning", heater, TemperatureErrorString(err));
		}
		else
		{
			mode = HeaterMode::tuning0;
			tuningReadingsTaken = 0;
			tuned = false;					// assume failure

			// We don't normally allow dynamic memory allocation when running. However, auto tuning is rarely done and it
			// would be wasteful to allocate a permanent array just in case we are going to run it, so we make an exception here.
			tuningTempReadings = new float[MaxTuningTempReadings];
			tuningTempReadings[0] = temperature;
			tuningReadingInterval = platform.HeatSampleInterval();
			tuningPwm = min<float>(maxPwm, model.GetMaxPwm());
			tuningTargetTemp = targetTemp;
			reply.printf("Auto tuning heater %d using target temperature %.1fC and PWM %.2f - do not leave printer unattended", heater, targetTemp, maxPwm);
		}
	}
}

void PID::GetAutoTuneStatus(StringRef& reply)	// Get the auto tune status or last result
{
	if (mode >= HeaterMode::tuning0)
	{
		reply.printf("Heater %d is being tuned, phase %u of %u",
						heater,
						(unsigned int)mode - (unsigned int)HeaterMode::tuning0 + 1,
						(unsigned int)HeaterMode::lastTuningMode - (unsigned int)HeaterMode::tuning0 + 1);
	}
	else if (tuned)
	{
		reply.printf("Heater %d tuning succeeded, use M307 H%d to see result", heater, heater);
	}
	else
	{
		reply.printf("Heater %d tuning failed");
	}
}

/* Notes on the auto tune algorithm
 *
 * Most 3D printer firmwares use the Åström-Hägglund relay tuning method (sometimes called Ziegler-Nichols + relay).
 * This gives results  of variable quality, but they seem to be generally satisfactory.
 *
 * We use Cohen-Coon tuning instead. This models the heating process as a first-order process (i.e. one that with constant heating
 * power approaches the equilibrium temperature exponentially) with dead time. This process is defined by three constants:
 *
 *  G is the gain of the system, i.e. the increase in ultimate temperature increase per unit of additional PWM
 *  td is the dead time, i.e. the time between increasing the heater PWM and the temperature following an exponential curve
 *  tc is the time constant of the exponential curve
 *
 * If the temperature is stable at T0 to begin with, the temperature at time t after increasing heater PWM by p is:
 *  T = T0 when t <= td
 *  T = T0 + G * p * (1 - exp((t - td)/tc)) when t >= td
 * In practice the transition from no change to the exponential curve is not instant, however this model is a reasonable approximation.
 *
 * Having a process model allows us to preset the I accumulator to a suitable value when switching between heater full on/off and using PID.
 * It will also make it easier to include feedforward terms in future.
 *
 * The auto tune procedure follows the following steps:
 * 1. Turn on any thermostatically-controlled fans that are triggered by the heater being tuned. This is done by code in the Platform module
 *    when it sees that a heater is being auto tuned.
 * 2. Accumulate temperature readings and wait for the starting temperature to stabilise. Abandon auto tuning if the starting temperature
 *    is not stable.
 * 3. Apply a known power to the heater and take temperature readings.
 * 4. Wait until the temperature vs time curve has flattened off, such that the temperature rise over the last 1/3 of the readings is less than the
 *    total temperature rise - which means we have been heating for about 3 time constants. Abandon auto tuning if we don't see a temperature rise
 *    after 30 seconds, or we exceed the target temperature plus 10C.
 * 5. Calculate the G, td and tc values that best fit the model to the temperature readings.
 * 6. Calculate the P, I and D parameters from G, td and tc using the modified Cohen-Coon tuning rules, or the Ho et al tuning rules.
 *    Cohen-Coon (modified to use half the original Kc value):
 *     Kc = (0.67/G) * (tc/td + 0.185)
 *     Ti = 2.5 * td * (tc + 0.185 * td)/(tc + 0.611 * td)
 *     Td = 0.37 * td * tc/(tc + 0.185 * td)
 *    Ho et al, best response to load changes:
 *     Kc = (1.435/G) * (td/tc)^-0.921
 *     Ti = 1.14 * (td/tc)^0.749
 *     Td = 0.482 * tc * (td/tc)^1.137
 *    Ho et al, best response to setpoint changes:
 *     Kc = (1.086/G) * (td/tc)^-0.869
 *     Ti = tc/(0.74 - 0.13 * td/tc)
 *     Td = 0.348 * tc * (td/tc)^0.914
 */

// This is called on each temperature sample when auto tuning
// It must set lastPWM to the required PWM, unless it is the same as last time.
void PID::DoTuningStep()
{
	// See if another sample is due
	if (tuningReadingsTaken == 0)
	{
		tuningPhaseStartTime = millis();
		if (mode == HeaterMode::tuning0)
		{
			tuningBeginTime = tuningPhaseStartTime;
		}
	}
	else if (millis() - tuningPhaseStartTime < tuningReadingsTaken * tuningReadingInterval)
	{
		return;		// not due yet
	}

	// See if we have room to store the new reading, and if not, double the sample interval
	if (tuningReadingsTaken == MaxTuningTempReadings)
	{
		// Double the sample interval
		tuningReadingsTaken /= 2;
		for (size_t i = 1; i < tuningReadingsTaken; ++i)
		{
			tuningTempReadings[i] = tuningTempReadings[i * 2];
		}
		tuningReadingInterval *= 2;
	}

	tuningTempReadings[tuningReadingsTaken] = temperature;
	++tuningReadingsTaken;

	switch(mode)
	{
	case HeaterMode::tuning0:
		// Waiting for initial temperature to settle after any thermostatic fans have turned on
		if (ReadingsStable(6000/platform.HeatSampleInterval(), 2.0))	// expect temperature to be stable within a 2C band for 6 seconds
		{
			// Starting temperature is stable, so move on
			tuningReadingsTaken = 1;
			tuningTempReadings[0] = tuningStartTemp = temperature;
			timeSetHeating = tuningPhaseStartTime = millis();
			lastPwm = tuningPwm;										// turn on heater at specified power
			tuningReadingInterval = platform.HeatSampleInterval();		// reset sampling interval
			mode = HeaterMode::tuning1;
			platform.Message(GENERIC_MESSAGE, "Auto tune phase 1, heater on\n");
			return;
		}
		if (millis() - tuningPhaseStartTime < 20000)
		{
			// Allow up to 20 seconds for starting temperature to settle
			return;
		}
		platform.Message(GENERIC_MESSAGE, "Auto tune cancelled because starting temperature is not stable\n");
		break;

	case HeaterMode::tuning1:
		// Heating up
		{
			const bool isBedOrChamberHeater = (heater == reprap.GetHeat().GetBedHeater() || heater == reprap.GetHeat().GetChamberHeater());
			const uint32_t heatingTime = millis() - tuningPhaseStartTime;
			const float extraTimeAllowed = (isBedOrChamberHeater) ? 60.0 : 30.0;
			if (heatingTime > (uint32_t)((model.GetDeadTime() + extraTimeAllowed) * SecondsToMillis) && (temperature - tuningStartTemp) < 3.0)
			{
				platform.Message(GENERIC_MESSAGE, "Auto tune cancelled because temperature is not increasing\n");
				break;
			}

			const uint32_t timeoutMinutes = (isBedOrChamberHeater) ? 20 : 5;
			if (heatingTime >= timeoutMinutes * 60 * (uint32_t)SecondsToMillis)
			{
				platform.Message(GENERIC_MESSAGE, "Auto tune cancelled because target temperature was not reached\n");
				break;
			}

			if (temperature >= tuningTargetTemp)							// if reached target
			{
				tuningHeatingTime = heatingTime;

				// Move on to next phase
				tuningReadingsTaken = 1;
				tuningHeaterOffTemp = tuningTempReadings[0] = temperature;
				tuningPhaseStartTime = millis();
				tuningReadingInterval = platform.HeatSampleInterval();		// reset sampling interval
				mode = HeaterMode::tuning2;
				lastPwm = 0.0;
				SetHeater(0.0);
				platform.Message(GENERIC_MESSAGE, "Auto tune phase 2, heater off\n");
			}
		}
		return;

	case HeaterMode::tuning2:
		// Heater turned off, looking for peak temperature
		{
			const int peakIndex = GetPeakTempIndex();
			if (peakIndex < 0)
			{
				if (millis() - tuningPhaseStartTime < 120 * 1000)			// allow 2 minutes for the bed temperature to start falling
				{
					return;			// still waiting for peak temperature
				}
				platform.Message(GENERIC_MESSAGE, "Auto tune cancelled because temperature is not falling\n");
			}
			else if (peakIndex == 0)
			{
				if (reprap.Debug(moduleHeat))
				{
					DisplayBuffer("At no peak found");
				}
				platform.Message(GENERIC_MESSAGE, "Auto tune cancelled because temperature peak was not identified\n");
			}
			else
			{
				tuningPeakTemperature = tuningTempReadings[peakIndex];
				tuningPeakDelay = peakIndex * tuningReadingInterval;

				// Move on to next phase
				tuningReadingsTaken = 1;
				tuningTempReadings[0] = temperature;
				tuningPhaseStartTime = millis();
				tuningReadingInterval = platform.HeatSampleInterval();		// reset sampling interval
				mode = HeaterMode::tuning3;
				platform.MessageF(GENERIC_MESSAGE, "Auto tune phase 3, peak temperature was %.1f\n", tuningPeakTemperature);
				return;
			}
		}
		break;

	case HeaterMode::tuning3:
		{
			// Heater is past the peak temperature and cooling down. Wait until it is part way back to the starting temperature so we can measure the cooling rate.
			// In the case of a bed that shows a reservoir effect, the choice of how far we wait for it to cool down will effect the result.
			// If we wait for it to cool down by 50% then we get a short time constant and a low gain, which causes overshoot. So try a bit more.
			const float coolDownProportion = 0.6;
			if (temperature > (tuningTempReadings[0] * (1.0 - coolDownProportion)) + (tuningStartTemp * coolDownProportion))
			{
				return;
			}
			CalculateModel();
		}
		break;

	default:
		// Should not happen, but if it does then quit
		break;
	}

	// If we get here, we have finished
	SwitchOff();								// sets mode and lastPWM, also deletes tuningTempReadings
}

// Return true if the last 'numReadings' readings are stable
/*static*/ bool PID::ReadingsStable(size_t numReadings, float maxDiff)
{
	if (tuningTempReadings == nullptr || tuningReadingsTaken < numReadings)
	{
		return false;
	}

	float minReading = tuningTempReadings[tuningReadingsTaken - numReadings];
	float maxReading = minReading;
	for (size_t i = tuningReadingsTaken - numReadings + 1; i < tuningReadingsTaken; ++i)
	{
		const float t = tuningTempReadings[i];
		if (t < minReading) { minReading = t; }
		if (t > maxReading) { maxReading = t; }
	}

	return maxReading - minReading <= maxDiff;
}

// Calculate which reading gave us the peak temperature.
// Return -1 if peak not identified yet, 0 if we failed to find a peak, else the index of the peak (which can't be zero because we always average 3 readings)
/*static*/ int PID::GetPeakTempIndex()
{
	// Check we have enough readings to look for the peak
	if (tuningReadingsTaken < 15)
	{
		return -1;							// too few readings
	}

	// Look for the peak
	int peakIndex = IdentifyPeak(1);
	if (peakIndex < 0)
	{
		peakIndex = IdentifyPeak(3);
		if (peakIndex < 0)
		{
			peakIndex = IdentifyPeak(5);
			if (peakIndex < 0)
			{
				return 0;					// more than one peak
			}
		}
	}

	// If we have found one peak and it's not too near the end of the readings, return it
	return ((size_t)peakIndex + 6 < tuningReadingsTaken) ? peakIndex : -1;
}

// See if there is exactly one peak in the readings.
// Return -1 if more than one peak, else the index of the peak. The so-called peak may be right at the end , in which case it isn't really a peak.
/*static*/ int PID::IdentifyPeak(size_t numToAverage)
{
	int firstPeakIndex = -1;
	float peakTempTimesN = -999.0;
	for (size_t i = 0; i + numToAverage <= tuningReadingsTaken; ++i)
	{
		float peak = 0.0;
		for (size_t j = 0; j < numToAverage; ++j)
		{
			peak += tuningTempReadings[i + j];
		}
		if (peak >= peakTempTimesN)
		{
			if ((int)i == firstPeakIndex + 1)
			{
				firstPeakIndex = (int)i;	// readings still going up or staying the same, so advance the first peak index
				peakTempTimesN = peak;
			}
			else
			{
				return -1;					// error, more than one peak
			}
		}
	}
	return firstPeakIndex + (numToAverage - 1)/2;
}

// Calculate the heater model from the accumulated heater parameters
void PID::CalculateModel()
{
	if (reprap.Debug(moduleHeat))
	{
		DisplayBuffer("At completion");
	}
	const float tc = (float)((tuningReadingsTaken - 1) * tuningReadingInterval)/(1000.0 * log((tuningTempReadings[0] - tuningStartTemp)/(tuningTempReadings[tuningReadingsTaken - 1] - tuningStartTemp)));
	const float td = (float)tuningPeakDelay * 0.001;
	const float heatingTime = (tuningHeatingTime - tuningPeakDelay) * 0.001;
	const float gain = (tuningHeaterOffTemp - tuningStartTemp)/(1.0 - exp(-heatingTime/tc));

	tuned = SetModel(gain, tc, td, model.GetMaxPwm(), true);
	if (tuned)
	{
		platform.MessageF(GENERIC_MESSAGE,
				"Auto tune heater %d completed in %u sec\n"
				"Use M307 H%d to see the result, or M500 to save the result in config-override.g\n",
				heater, (millis() - tuningBeginTime)/(uint32_t)SecondsToMillis, heater);
	}
	else
	{
		platform.MessageF(GENERIC_MESSAGE, "Auto tune of heater %u failed due to bad curve fit (G=%.1f, tc=%.1f, td=%.1f)\n", heater, gain, tc, td);
	}
}

void PID::DisplayBuffer(const char *intro)
{
	OutputBuffer *buf;
	if (OutputBuffer::Allocate(buf))
	{
		buf->catf("%s: interval %.1f sec, readings", intro, tuningReadingInterval * MillisToSeconds);
		for (size_t i = 0; i < tuningReadingsTaken; ++i)
		{
			buf->catf(" %.1f", tuningTempReadings[i]);
		}
		buf->cat("\n");
		platform.Message(HOST_MESSAGE, buf);
	}
}

// End