/* * Heat Control * Soldering heater program. * Temperature of the object to be heated is measured via a thermocouple, and a heat gun is controlled with a * zero-voltage switch to provide the heat. The algorithm is a modified proportional integral differential * (PID) control. * A 16 character by two line LCD display with five pushbuttons is the control interface. This display is controlled via * I2C. The control interface concept is a state machine. Five states allow setting the desired object temperature, the * proportional, integral, and differential coefficients, and the minimum throttle. A fifth state runs the heater under * PID control. * Copyright (c) 2021, 2022 Douglas A. Reneker */ #define TRUE 1 #define FALSE 0 #include #include #include #include // Thermocouple configuration from web tool // Channel 1 only, degrees C low burnout, type J thermocouple const char P1_04THM_CONFIG[20] = { 0x40, 0x00, 0x60, 0x03, 0x21, 0x00, 0x22, 0x00, 0x23, 0x00, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; float temperatureRead; // last temperature read // LED Control const int PinLED = LED_BUILTIN; // Whatever pin the LED is on int LEDState = LOW; // Heat Gun Control const int PinHeat = 0; // Pin that controls the heat gun const int Pin60Hz = 1; // Pin that senses the 60 Hz power line phase #define HEAT_ON HIGH // Turn the heater on #define HEAT_OFF LOW // Turn the heater off // Heat Gun Throttle #define THR_MAX 100 // Maximum Throttle setting int minThrottle = 12; // Minimum Throttle setting int Throttle; // Current Throttle setting int Thr_Sum = 0; // Sum of all Thr_Hist[] array int Thr_Hist[THR_MAX]; // Record of recent heat gun on and off cycles int Thr_Hist_Ptr; // Pointer into Thr_Hist[] // PID Control int PID_counter; // incremented at interrupt level, to define when controls are calculated #define PID_PERIOD 147 // run PID about every 2.5 seconds. Units are 1/60th of a second. int PID_exec; // set TRUE at interrupt level to cause PID to update. Cleared at base level. Adafruit_RGBLCDShield lcd = Adafruit_RGBLCDShield(); #define ON 0x07 // Backlight control definitions - monochrome #define OFF 0x00 // Button scanning defintions uint8_t buttons; // current state of buttons; bits for each button int button_press; // TRUE when a button is first detected int button_state; // used by interrupt to detect "leading edge" of button press #define NOT_PRESSED 1 #define PRESSING 2 // Operational state machine definitions int SH_state; // Current state of soldering heater #define SH_BEGIN 10 // Initial state #define SH_SET_INTEG 11 // Adjust Integral coeff. #define SH_SET_PROP 12 // Adjust Proportional coeff. #define SH_SET_DIFF 13 // Adjust Differential coeff. #define SH_SET_MIN 14 // Adjust the minimum Throttle #define SH_SET_TEMP 15 // Adjust Desired temperature in degrees C #define SH_RUN 16 // Run state // PID control coefficients float pid_integral; // integral coefficient #define INIT_INTEGRAL 80.0 #define INTEG_MAX 30.0 // Maximum effect the stored integral term can have on throttle float pid_proportional; // proportional coefficent #define INIT_PROP 20.0 float pid_diff; // differential coefficent #define INIT_DIFF 50.0 float integral_term=0.0; // integral value float prev_error=0.0; // previous error value // Desired temperature (degrees C) float temp_set; #define INIT_TEMP_SET 205.0 // starting point for setting temperature // Desired ramp rate (degrees C per minute) float ramp_rate; #define INIT_RAMP_RATE 20.0 // starting point for setting ramp rate void setup() { // init button scanning button_press = FALSE; // Assume nothing is pressed button_state = NOT_PRESSED; // set up the LCD's number of columns and rows: lcd.begin(16, 2); lcd.setBacklight(ON); lcd.print("SOLDERING HEATER"); // Splash message // Intialize the thermocouple interface while(!P1.init()){ ; //wait for Modules to sign on } P1.configureModule(P1_04THM_CONFIG, 1); //sends the config data to the module in slot 1 lcd.clear(); // initialize the PID coefficents pid_integral = INIT_INTEGRAL; pid_proportional = INIT_PROP; pid_diff = INIT_DIFF; // initialize the control values temp_set = INIT_TEMP_SET; Display_Value(0,1,(int)temp_set); New_State(SH_BEGIN, "BEGIN"); pinMode(PinLED, OUTPUT); // LED pin is an output digitalWrite(PinLED, LOW); pinMode(PinHeat, OUTPUT); // Zero Voltage Switch (ZVS) control is an output digitalWrite(PinHeat, HEAT_OFF); // Turn off the ZVS initially for(Thr_Hist_Ptr = 0; Thr_Hist_Ptr < THR_MAX; Thr_Hist_Ptr++) { Thr_Hist[Thr_Hist_Ptr] = 1; // set Throttle to maximum to start Thr_Sum++; } Thr_Hist_Ptr = 0; Throttle = Thr_Sum; // Initial Throttle setting corresponds to the initial Thr_Hist[] contents PID_exec = FALSE; PID_counter = 0; attachInterrupt(digitalPinToInterrupt(Pin60Hz), Interrupt_60Hz, RISING); // 60 Hz sense interrupts on rising edge } /* * loop() goes through several processing tasks; no task waits, to allow efficient time sharing * Tasks: * - Check for button pressing, and flag any new presses. * - If a button press is found, go through the state machine * - Read and display the current temperature * - If running, update the throttle, based on the PID algorithm */ void loop() { // Scan for button presses if(button_state == NOT_PRESSED) { // no buttons pushed previously if(buttons = lcd.readButtons()) { // if a button is now pressed button_press = TRUE; button_state = PRESSING; } } else { // a button was pushed previously if(!(buttons = lcd.readButtons())) { // if no button is pressed now button_state = NOT_PRESSED; } } // Operational state machine if(button_press) { // inital press of a button detected button_press = FALSE; // button press recognized, acted on below. switch(SH_state) { case SH_BEGIN: { // Starting point for this state machine if(buttons & (BUTTON_RIGHT | BUTTON_LEFT)){ New_State(SH_SET_TEMP, " TEMP"); Display_Value(0,1,(int)temp_set); } break; } case SH_SET_INTEG: { // Desired integral coefficient if(buttons & BUTTON_RIGHT) { New_State(SH_SET_PROP, " PROP"); lcd.setCursor(7,0); lcd.print(" "); Display_Value(7,0,(int)pid_proportional); } else if(buttons & BUTTON_LEFT) { New_State(SH_SET_TEMP, " TEMP"); lcd.setCursor(7,0); lcd.print(" "); Display_Value(0,1,temp_set); } else if(buttons & BUTTON_SELECT) { lcd.setCursor(7,0); lcd.print(" "); New_State(SH_RUN, " RUN"); } else if(buttons & BUTTON_UP) { pid_integral += 5.0; Display_Value(7,0,(int)pid_integral); } else if(buttons & BUTTON_DOWN) { pid_integral -= 5.0; Display_Value(7,0,(int)pid_integral); } break; } case SH_SET_PROP: { // Desired proportional coefficient if(buttons & BUTTON_RIGHT) { New_State(SH_SET_DIFF, "DIFFL"); lcd.setCursor(7,0); lcd.print(" "); Display_Value(7,0,(int)pid_diff); } else if(buttons & BUTTON_LEFT) { New_State(SH_SET_INTEG, "INTEG"); lcd.setCursor(7,0); lcd.print(" "); Display_Value(7,0,(int)pid_integral); } else if(buttons & BUTTON_SELECT) { lcd.setCursor(7,0); lcd.print(" "); New_State(SH_RUN, " RUN"); } else if(buttons & BUTTON_UP) { pid_proportional += 5.0; Display_Value(7,0,(int)pid_proportional); } else if(buttons & BUTTON_DOWN) { pid_proportional -= 5.0; Display_Value(7,0,(int)pid_proportional); } break; } case SH_SET_DIFF: { // Desired differential coefficient if(buttons & BUTTON_RIGHT) { New_State(SH_SET_MIN, "MIN H"); lcd.setCursor(7,0); lcd.print(" "); Display_Value(7,0,minThrottle); } else if(buttons & BUTTON_LEFT) { New_State(SH_SET_PROP, " PROP"); lcd.setCursor(7,0); lcd.print(" "); Display_Value(7,0,(int)pid_proportional); } else if(buttons & BUTTON_SELECT) { lcd.setCursor(7,0); lcd.print(" "); New_State(SH_RUN, " RUN"); } else if(buttons & BUTTON_UP) { pid_diff += 5.0; Display_Value(7,0,(int)pid_diff); } else if(buttons & BUTTON_DOWN) { pid_diff -= 5.0; Display_Value(7,0,(int)pid_diff); } break; } case SH_SET_MIN: { // Desired minimum Throttle if(buttons & BUTTON_RIGHT) { New_State(SH_SET_TEMP, " TEMP"); lcd.setCursor(7,0); lcd.print(" "); Display_Value(0,1,(int)temp_set); } else if(buttons & BUTTON_LEFT) { New_State(SH_SET_DIFF, " DIFF"); lcd.setCursor(7,0); lcd.print(" "); Display_Value(7,0,(int)pid_diff); } else if(buttons & BUTTON_SELECT) { lcd.setCursor(7,0); lcd.print(" "); New_State(SH_RUN, " RUN"); } else if(buttons & BUTTON_UP) { minThrottle += 1; Display_Value(7,0,minThrottle); } else if(buttons & BUTTON_DOWN) { minThrottle -= 1; Display_Value(7,0,minThrottle); } break; } case SH_SET_TEMP: { // Desired temperature in degrees C if(buttons & BUTTON_RIGHT) { New_State(SH_SET_INTEG, "INTEG"); lcd.setCursor(7,0); lcd.print(" "); Display_Value(7,0,(int)pid_integral); } else if(buttons & BUTTON_LEFT) { New_State(SH_SET_MIN, "MIN H"); lcd.setCursor(7,0); lcd.print(" "); Display_Value(7,0,minThrottle); } else if(buttons & BUTTON_SELECT) { New_State(SH_RUN, " RUN"); } else if(buttons & BUTTON_UP) { temp_set += 1.0; Display_Value(0,1,(int)temp_set); } else if(buttons & BUTTON_DOWN) { temp_set -= 1.0; Display_Value(0,1,(int)temp_set); } break; } case SH_RUN: { // Run state if(buttons & BUTTON_SELECT) { New_State(SH_SET_TEMP, " TEMP"); // temperature setpoint should be displayed already } break; } default: { lcd.clear(); New_State(SH_BEGIN, "BEGIN"); } } } // Read and display the current temperature temperatureRead = P1.readTemperature(1, 1); // Input 1 of slot 1 lcd.setCursor(0, 0); if(temperatureRead < 99.95) lcd.print(" "); // Leading blank, if needed. lcd.print(temperatureRead, 1); // Update PID control and display current throttle value if(PID_exec) { PID_exec = FALSE; Throttle = PID_calc(temperatureRead); Display_Value(6,1,Throttle); } } /* * New_State() * Sets the next state, and updates the state indicator in the display. */ void New_State(int newState, char *stateString) { SH_state = newState; lcd.setCursor(11, 1); lcd.print(stateString); } /* * Display_Value() * Displays a 3-digit integer value at a given column and row */ void Display_Value(int col, int row, int val) { lcd.setCursor(col, row); if(val < 100) lcd.print("0"); if(val < 10) lcd.print("0"); lcd.print(val); } /* * PID_calc() * Returns the new throttle value, based on PID control of the temperature. PID_calc() is called from loop(), but only when Interrupt_60Hz() sets a flag. * This happens every (PID_PERIOD/60) seconds, currently about 2.5 seconds. */ int PID_calc(float currTemperature) { float current_error; // distance from goal, in internal control units (can be a negative number) int control_output; // Throttle value to output float diff_term; // differential term current_error = temp_set - currTemperature; // how far are we from goal (in control units)? integral_term += pid_integral * current_error; // update the integral term. if(integral_term < 0.0) integral_term = 0.0; // don't allow integral_term to suppress heat if(integral_term > INTEG_MAX) integral_term = INTEG_MAX; // don't allow integral_term to grow huge diff_term = pid_diff * (current_error - prev_error); // calculate the differential term prev_error = current_error; control_output = (int) ((pid_proportional * current_error) + integral_term + diff_term); // sum proportional, integral and diff terms if(control_output < minThrottle) return(minThrottle); if((int) control_output > THR_MAX) return(THR_MAX); return((int) control_output); } /* * Interrupt_60Hz() is called once for each cycle of the AC power line. * If the overall control state (SH_state) is SH_RUN, then the heater is operated per Throttle. * In any other state, no action is taken. * * Recent throttle "on/off" history is stored in an array, and a count of the number of "on" intervals is kept. The algorithm * seeks to maintain the "on" to "on" + "off" ratio as close to the desired throttle value as possible. * * A counter is incremented, to pass a flag (PID_exec) to the base level code, to indicate when to run the * PID algorthim. Again, this only takes place in the SH_RUN state. */ void Interrupt_60Hz() { if(SH_state == SH_RUN) { // Operate the heater if(Thr_Sum < Throttle) { digitalWrite(PinHeat, HEAT_ON); // Recent history is less than throttle - Heat for this cycle Thr_Sum -= Thr_Hist[Thr_Hist_Ptr]; // Subtract off the oldest history Thr_Hist[Thr_Hist_Ptr] = 1; // Record the current decision as the newest history Thr_Sum++; // Include this cycle in the sum } else if(Thr_Sum == Throttle) { // Throttle matches history; follow the history if(Thr_Hist[Thr_Hist_Ptr] == 1) digitalWrite(PinHeat, HEAT_ON); else digitalWrite(PinHeat, HEAT_OFF); } else { digitalWrite(PinHeat, HEAT_OFF); // Recent history is equal or greater than throttle - no heat this cycle Thr_Sum -= Thr_Hist[Thr_Hist_Ptr]; // Subtract off the oldest history Thr_Hist[Thr_Hist_Ptr] = 0; // Record the current decision as the newest history } if((Thr_Hist_Ptr + 1) >= THR_MAX) Thr_Hist_Ptr = 0; // Increment Thr_Hist_Ptr else Thr_Hist_Ptr++; // Increment the PID interval counter if((PID_counter + 1) >= PID_PERIOD) { PID_counter = 0; PID_exec = TRUE; // this must be reset at base level, to be meaningful. } else PID_counter++; } else digitalWrite(PinHeat, HEAT_OFF); // Not running - ensure heat is off. return; }