3DP: Heating Part 3
Ugh. My last post is good in some parts, bad in others. Reworked!
In order to reuse as much code as possible, I blended several structures into a larger one, and refactored the code. I also added a temperature monitor to stop the printer in case there is a temperature problem with heating.
If the temperature does not rise fast enough, or if the temperature does not drop fast enough, there is a problem. The best thing to do is to stop the machine.
The new structure to monitor a heating device (will help control Bed, Extruder, or Chamber). There is the PID control section (the PID code has not changed), a Temperature protection section, and an identifier for the heater. This code is not yet tested, so will change:
typedef struct myPidControls {
double Input;
double Output;
//Define the aggressive and conservative Tuning Parameters
double aggKp, aggKi, aggKd;
double consKp, consKi, consKd;
int16_t *PWM;
int16_t CurrentSlot;
int16_t MaxSlotCount;
int16_t PWM_Value;
int16_t PWM_Sum;
int16_t offset;
double OffsetMultiplier; // amount to bend the knee when temperature has reached kneeTemperature
// Can be 1.0, 0.9, 0.7, etc. Lower number bends knee more.
//double lastGap;
double kneeTemperature; // temperature at which we should start the running sum over.
} MY_PID_CONTROLS;
typedef struct temperatureProtectionStruct {
uint32_t MaximumNumberOfMilliSecondsToWaitWhileMaintaining; // 20000
uint32_t TemperatureHysteresisDegreesCWhileMaintaining; // 2
// when starting heater
uint32_t TemperatureRampUpMillisecondsWatchPeriod; // 20000
uint32_t TemperatureRampUpIncreaseDegreesC; // 2
// temperatures
double LastCurrentTemperature;
uint32_t lastMilliseconds;
double lastDesiredTemperature;
enum temperatureStates lastTemperatureState; // off or not off
} TEMPERATURE_PROTECTION;
typedef struct temperatureControlStruct {
double CurrentTemperature;
double DesiredTemperature;
enum temperatureStates TemperatureState;
MY_PID_CONTROLS PIDControls;
PID_TYPE PID;
TEMPERATURE_PROTECTION TemperatureProtection;
char HeaterASCIIidString[4];// "0\0xa"
} TEMPERATURE_CONTROL;
extern TEMPERATURE_CONTROL gBedTempControl;
extern TEMPERATURE_CONTROL gE0TempControl;
There is a new function called handleTemperatureWatch(). That function only determines if the printer shutdown routines should be called (is called Kill() in most 3D printers). This function is just a rough draft, but here it is. Note that by using long variables names, I get to read what the function is doing even without comments. Note the watch function does only that. This coding style that uses state machines allows you to think linearly and only about the problem at hand. (Note: I did some formatting in this cut/n/paste for the narrow code window):
//-------------------------------------------------------------
// Watch to make sure the temperature is changing the right amounts
//-------------------------------------------------------------
void handleTemperatureWatch(TEMPERATURE_CONTROL *pTempControl,TaskMonitor_t *TaskMonitorPtr){
TEMPERATURE_PROTECTION *pTP =
&pTempControl->TemperatureProtection;
uint32_t elapsedTime =
getElapsedTimeInMilliseconds(pTP->lastMilliseconds);
double tempDiff;
if (pTempControl->TemperatureState != tempOffState){
// if we are in the "maintaining" mode, then
if (pTempControl->TemperatureState == tempMaintainedState){
tempDiff = pTempControl->DesiredTemperature - pTempControl->CurrentTemperature;
if (tempDiff < 0.0)
tempDiff = -tempDiff; // simple absolute()function
// if temperature is out of our range
if (tempDiff >
pTP->TemperatureHysteresisDegreesCWhileMaintaining){
// if it has been out of our range too long, do a kill()
if (elapsedTime >
pTP->MaximumNumberOfMilliSecondsToWaitWhileMaintaining){
GCodePrintString(HEATING_FAILED_STRING);
GCodePrintString(
pTempControl->HeaterASCIIidString);
GCodePrintString(PRINTER_HALTED_STRING);
TaskDelay(100,TaskMonitorPtr);// let messages clear out
vDoFullStop(pdTRUE,TaskMonitorPtr);// halt printing
}else {
return;
}
} else {
// refresh last time, temperature within range
pTP->lastMilliseconds=getSystemTimeInMs();
}
return; // finished with "temp Maintained'
}
// if temperature is ramping up, then temperature rise must be occurring
if (pTempControl->TemperatureState == tempRampUpState){
// if we do not have a "last" temperature (is 0), then grab the current temperature
if (pTP->LastCurrentTemperature==0.0){
pTP->LastCurrentTemperature=pTempControl->CurrentTemperature;
}
// if the temperature has not gone up the increase, and time is past watchpoint
// then is error.
tempDiff = pTempControl->CurrentTemperature -
pTP->LastCurrentTemperature;
if (tempDiff < 0.0) tempDiff = -tempDiff; // simple absolute()function
// if temperature has not increased enough
if (tempDiff <
pTP->TemperatureRampUpIncreaseDegreesC ){
// then is enough time has passed.
if (elapsedTime >
pTP->TemperatureRampUpMillisecondsWatchPeriod){
// then is an error
GCodePrintString(HEATING_FAILED_STRING);
GCodePrintString(
pTempControl->HeaterASCIIidString);
GCodePrintString(PRINTER_HALTED_STRING);
TaskDelay(100,TaskMonitorPtr);// let messages clear out
vDoFullStop(pdTRUE,TaskMonitorPtr);// halt printing
}else {
// refresh last time, temperature within range
pTP->lastMilliseconds=getSystemTimeInMs();
}
return;
} else {
// pick up current temperature
pTP->LastCurrentTemperature =
pTempControl->CurrentTemperature;
}
}
}
// if temperature is ramping down (or turned off), then temperature fall must be occurring
if ( pTempControl->TemperatureState == tempRampDownState
|| pTempControl->TemperatureState == tempOffState ){
// if we do not have a "last" temperature (is 0), then grab the current temperature
if (pTP->LastCurrentTemperature==0.0){
pTP->LastCurrentTemperature=
pTempControl->CurrentTemperature;
}
// if the temperature has not gone down any, and time is past watchpoint
// then is error.
tempDiff = pTempControl->CurrentTemperature - pTP->LastCurrentTemperature;
// if temperature has not decreased enough
if (tempDiff >= 0 ){
// then is enough time has passed. (use same as maintained)
if (elapsedTime >
pTP->MaximumNumberOfMilliSecondsToWaitWhileMaintaining){
// if we are close to room temperature, ignore.
if (pTempControl->CurrentTemperature <= 30.0 )
return;
// then is an error
GCodePrintString(HEATING_FAILED_STRING);
GCodePrintString(
pTempControl->HeaterASCIIidString);
GCodePrintString(PRINTER_HALTED_STRING);
TaskDelay(100,TaskMonitorPtr);// let messages clear out
vDoFullStop(pdTRUE,TaskMonitorPtr);// halt printing
}else {
// refresh last time, temperature within range
pTP->lastMilliseconds=getSystemTimeInMs();
}
return;
}
}
}
The Bed temperature control task now looks simpler. It only calls the PID and PWM routines. Its’ only job is to turn the heater on or off, and regulate the PWM based on the PID decisions. Its’s initialization sets all the variables in the TEMPERATURE_CONTROL structure based on settings in the Configuration.h style files:
/*-----------------------------------------------------------*/
//-----------------------------------------------------------------------------
// task that monitors the bed temperature a few times per second
// It uses the PID values to control the PWM
//-----------------------------------------------------------------------------
static portTASK_FUNCTION( vBedTemperatureTask, pvParameters ){
static TaskMonitor_t *TaskMonitor = &TaskMonitorArray[BED_TEMPERATURE_TASK];
(void) pvParameters;
// initialize constants
gBedTempControl.PIDControls.aggKp=4,
gBedTempControl.PIDControls.aggKi=0.2,
gBedTempControl.PIDControls.aggKd=1;
//gBedTempControl.PID.consKp=1, gBedTempControl.PID.consKi=0.05, gBedTempControl.PID.consKd=0.25;
gBedTempControl.PIDControls.consKp=1,
gBedTempControl.PIDControls.consKi=0.05,
gBedTempControl.PIDControls.consKd=0.25;
gBedTempControl.PIDControls.PWM = BedPWM;
gBedTempControl.PIDControls.MaxSlotCount = BED_PWM_SLOTS;
gBedTempControl.PIDControls.kneeTemperature = BED_TEMPERATURE_KNEE_FOR_PID_CONTROL;
gBedTempControl.PIDControls.OffsetMultiplier = BED_TEMPERATURE_PID_KNEE_SLOPE_MULTIPLIER;
#ifdef ENABLE_THERMAL_PROTECTION_BED
gBedTempControl.TemperatureProtection.MaximumNumberOfMilliSecondsToWaitWhileMaintaining = THERMAL_PROTECTION_BED_TIME_IN_SECONDS*1000; // 20
gBedTempControl.TemperatureProtection.TemperatureHysteresisDegreesCWhileMaintaining = THERMAL_PROTECTION_BED_TEMPERATURE_HYSTERESIS_DEGREES_C; // 2
// when starting heater
gBedTempControl.TemperatureProtection.TemperatureRampUpMillisecondsWatchPeriod=THERMAL_BED_START_HEAT_WATCH_PERIOD*1000; // 20
gBedTempControl.TemperatureProtection.TemperatureRampUpIncreaseDegreesC= THERMAL_BED_START_TEMPERATURE_INCREASE_DEGREES_C; // 2
#endif
PID_init( &gBedTempControl.PID,
gBedTempControl.PIDControls.Input,
gBedTempControl.PIDControls.Output,
gBedTempControl.DesiredTemperature,
gBedTempControl.PIDControls.consKp,
gBedTempControl.PIDControls.consKi,
gBedTempControl.PIDControls.consKd,
BED_TEMPERATURE_MONITOR_RATE_IN_MILLISECONDS,
P_ON_M, // specify proportional on input reading
DIRECT);
PID_SetMode(&gBedTempControl.PID,AUTOMATIC);
for(;;) {
vTaskDelay(pdMS_TO_TICKS(BED_TEMPERATURE_MONITOR_RATE_IN_MILLISECONDS));//Default: 20 times per second
TaskMonitor->runCounter++;
if (gBedTempControl.TemperatureState == tempOffState) {// if bed temperature not on
#if USE_HARDWARE_PWM_FOR_BED_HEATER
PWM_BedHeater_WriteCompare(0);// 0 output, heater off
#else
PinWrite(BED_HEATER_PIN,LOW);// turn heater off
#endif
gBedTempControl.PIDControls.offset=0; // when turn off, reset the flag
continue;// don't execute code below, go to for(;;)
}
// we are "ON".
#if USE_HARDWARE_PWM_FOR_BED_HEATER
PWM_BedHeater_WriteCompare(
(int8_t)gBedTempControl.PID.Output);
#else
applySoftwarePWM(&gBedTempControl.PID,
&gBedTempControl.PID,BED_HEATER_PIN);
#endif
}
}
There is also a state machine controlling the temperature status. The states are as follows:
enum temperatureStates {
tempOffState=0
,tempRampUpState // turned on, ramp up
,tempReachedState // reached
,tempMaintainedState // keep here
,tempRampDownState // turned off, ramp down
,tempFailState // machine must be killed
};
The heating device is either off, ramping up, station keeping, ramping down, or failed. The following code determines which state the heating device is in. That is all this code does:
//------------------------------------------------------------
// determine which state the heater is in
//------------------------------------------------------------
void handleStateCalculation(TEMPERATURE_CONTROL *pTempControl){
if (pTempControl->DesiredTemperature ) {
// temp going up
if (pTempControl->CurrentTemperature < pTempControl->DesiredTemperature ){
pTempControl->TemperatureState = tempRampUpState;
} else {
// current temperature == to desired (with a small window)
if (pTempControl->CurrentTemperature >= pTempControl->DesiredTemperature -1.0
&& pTempControl->CurrentTemperature <= pTempControl->DesiredTemperature+1.0 ){
pTempControl->TemperatureState = tempMaintainedState;
} else {
// temp going down
pTempControl->TemperatureState = tempRampDownState;
}
}
} else {
// desired temperature is 0, turn off
pTempControl->TemperatureState = tempOffState;
}
}
The task that watches the heating devices and calculates the controls for the devices follows:
//------------------------------------------------------------
// task that calculates the bed and extruder temperatures a few times per second
//------------------------------------------------------------
static portTASK_FUNCTION( vTemperatureCalculateTask, pvParameters ){
static TaskMonitor_t *TaskMonitorPtr = &TaskMonitorArray[TEMPERATURE_CALC_TASK];
(void) pvParameters;
startADC();
for(;;) {
// wait enough time for the A/D sequencer to finish
vTaskDelay(pdMS_TO_TICKS(2));//Default: around 100 times per second (will vary)
TaskMonitorPtr->runCounter++;
handleADCReadings();// create the temperatures
//================================================
// BED
//================================================
// Now calculate the temperatures. Plug results into the PID structures
calculateBedTemperature();
handleStateCalculation(&gBedTempControl);
// only run the controller if is heating
if (gBedTempControl.TemperatureState != tempOffState) {
gBedTempControl.PID.Setpoint =
gBedTempControl.DesiredTemperature;
gBedTempControl.PID.Input =
gBedTempControl.CurrentTemperature;
PID_Compute(&gBedTempControl.PID);
#ifdef ENABLE_THERMAL_PROTECTION_BED
handleTemperatureWatch(&gBedTempControl,TaskMonitorPtr);
#endif
}
//================================================
// EXTRUDER
//================================================
calculateExtruderTemperature();
handleStateCalculation(&gE0TempControl);
if (gE0TempControl.TemperatureState != tempOffState){
gE0TempControl.PID.Setpoint =
gE0TempControl.DesiredTemperature;
gE0TempControl.PID.Input = gE0TempControl.CurrentTemperature;
PID_Compute(&gE0TempControl.PID);
#ifdef ENABLE_THERMAL_PROTECTION_EXTRUDER
handleTemperatureWatch(&gE0TempControl);
#endif
}
}
}
Next Time
I will be testing this code and making it work. It may delay the next post as I work out the kinks.
I expect to unveil the schematic for a CY8CKit-059 to Ramps 1.4 adapter board. With that board, 3D printing will be available to PSOC users. Just add a Ramps 1v4 board electronics and a 3D printer frame, some software, and start working. Hah!
I have updated the board for SPI LCD and possibly SD Card. Once that board is in and the SPI LCD (is called a 12864 display for 3D aficionados) is working, I will start posting some of that code.
Enjoy!