Microcontroller controlled digital thermostat for Peltier coolers
In this video I show you the final version of my digital thermostat for Peltier coolers. This thermostat does not only switches the power on and off, but it precisely controls the power for the Peltier cooler. The temperature of the cold side is measured via an NTC thermistor, then a logic decides whether we should stay at the current power level or increase/decrease the power for the Peltier cooler. The change of the power is done by injecting a some voltage to the feedback pin of the SZBK07 DC-DC converter by using an MCP41100 digital potentiometer. The resolution of the DC-DC converter's output voltage is limited by the 8 bit resolution of the digital potentiometer. The temperature resolution is also limited by this. Assuming that the temperature range is 60°C (+30 to -30°C), the step size is about 0.23°C.
Check out this playlist for previous iterations and more details.
Wiring diagram
Arduino/STM32 source code
#include <SPI.h> //SPI comunication //SPI on STM32: MOSI: PA7, SCK: PA5 //SPI on Arduino: MOSI: 11, SCK: 13 //20x4 LCD #include <LiquidCrystal_I2C.h> //SDA = B7[A4], SCL = B6[A5] STM32/[Arduino] LiquidCrystal_I2C lcd(0x27, 20, 4); int menuCounter = 0; //counts the clicks of the rotary encoder between menu items (0-3 in this case) float menu2_Value = 30; //value within menu 2 - Goal temperature / default = 30°C int menu3_Value = 500; //value within menu 3 - Update interval / default = 500 ms float menu4_Value = 1; //value within menu 4 - Tolerance / default = 1°C bool menu1_selected = false; //enable/disable to change the value of menu item bool menu2_selected = false; bool menu3_selected = false; bool menu4_selected = false; //Note: if a menu is selected ">" becomes "X". bool coolingIsOn = false; //Boolean for checking if the cooling is ON or OFF int PowerPercent = 0; int coolingPower = 0; //0: no power, 256: full power //no power: max output on the MCP41100 //full power: zero output on the MCP41100 //Defining pins //Arduino interrupt pins: 2, 3. const int RotaryCLK = PB3; //CLK pin on the rotary encoder const int RotaryDT = PB4; //DT pin on the rotary encoder const int PushButton = PB5; //Button to enter/exit menu const int VoutPin = PA0; //ADC0 pin of STM32 const byte CS1_pin = PA4; //CS pin for potmeter (Pick any on Arduino, Pin 10 is the "default") //Statuses for the rotary encoder int CLKNow; int CLKPrevious; int DTNow; int DTPrevious; bool refreshLCD = true; //refreshes values bool refreshSelection = false; //refreshes selection (> / X) float TimeNow1 = 0; float TimeNow2 = 0; //For the LCD //Thermometer-related variables float Vsupply = 3.3; //power supply voltage (3.3 V rail) - STM32 ADC pin is NOT 5 V tolerant float Vout; //Voltage divider output float R_NTC; //NTC thermistor resistance in Ohms float R_10k = 9840; //10k resistor measured resistance in Ohms (other element in the voltage divider) float B_param = 3700; //B-coefficient of the thermistor float T0 = 298.15; //25°C in Kelvin float Temp_K; //Temperature measured by the thermistor (Kelvin) float Temp_C; //Temperature measured by the thermistor (Celsius) void setup() { pinMode(PB3, INPUT_PULLUP); //RotaryCLK pinMode(PB4, INPUT_PULLUP); //RotaryDT pinMode(PB5, INPUT_PULLUP); //Button pinMode(CS1_pin, OUTPUT); //Chip select is an output pinMode(VoutPin, INPUT_ANALOG); //A0 pin for measuring the voltage of the NTC //------------------------------------------------------ lcd.begin(); // initialize the lcd lcd.backlight(); //------------------------------------------------------ lcd.setCursor(0,0); //Defining positon to write from first row, first column . lcd.print("SZBK07 300W 15A"); lcd.setCursor(0,1); //Second row, first column lcd.print("Rotary encoder"); lcd.setCursor(0,2); //Second row, first column lcd.print("Peltier controller"); lcd.setCursor(0,3); //Second row, first column lcd.print("Improved version"); delay(5000); //wait 2 sec lcd.clear(); //clear the whole LCD printLCD(); //print the stationary parts on the screen //------------------------------------------------------ //Store states of the rotary encoder CLKPrevious = digitalRead(RotaryCLK); DTPrevious = digitalRead(RotaryDT); attachInterrupt(digitalPinToInterrupt(RotaryCLK), rotate, CHANGE); //CLK pin is an interrupt pin attachInterrupt(digitalPinToInterrupt(PushButton), pushButton, FALLING); //PushButton pin is an interrupt pin //Writing the default (0) value on the digital potentiometer //--MCP41100--------------- digitalWrite(CS1_pin, HIGH); //Select potmeter SPI.begin(); //start SPI for the digital potmeter digitalWrite(CS1_pin, LOW); //Default value for the pot should be zero (otherwise, Vout will be 2.5 V by default) SPI.transfer(0x11); //command 00010001 [00][01][00][11] SPI.transfer(0); //transfer the integer value of the potmeter (0-255 value) delayMicroseconds(100); //wait digitalWrite(CS1_pin, HIGH); //------------------------- } void loop() { TimeNow1 = millis(); //Update time if(TimeNow1 - TimeNow2 > (menu3_Value)) //update PSU according to the update interval { ConvertToTemperature(); //Measure the temperature if(coolingIsOn == true) //if cooling is enabled, we run the PSU { adjustTemperature(); //Adjust PSU according to the above measured temperature writePotmeter(); //write the potmeter value in according to the power adjustments } TimeNow2 = millis(); //Update time } if(refreshLCD == true) //If we are allowed to update the LCD ... { updateLCD(); // ... we update the LCD ... //... also, if one of the menus are already selected... if(menu1_selected == true || menu2_selected == true || menu3_selected == true || menu4_selected == true) { // do nothing - i.e. we do not move the cursor } else { updateCursorPosition(); //update the position of the cursor } refreshLCD = false; //reset the variable - wait for a new trigger } if(refreshSelection == true) //if the selection is changed { updateSelection(); //update the selection on the LCD refreshSelection = false; // reset the variable - wait for a new trigger } } void rotate() { //Power ON/OFF if(menu1_selected == true) { //do nothing - menu 1 is the power ON/OFF } //---MENU2--------------------------------------------------------------- //Goal temperature else if(menu2_selected == true) { CLKNow = digitalRead(RotaryCLK); //Read the state of the CLK pin // If last and current state of CLK are different, then a pulse occurred if (CLKNow != CLKPrevious && CLKNow == 1) { // If the DT state is different than the CLK state then // the encoder is rotating in A direction, so we increase if (digitalRead(RotaryDT) != CLKNow) { if(menu2_Value < 30) //we do not go above 30 { menu2_Value = menu2_Value + 0.1; //0.1°C increment } else { menu2_Value = 30; //the value is not allowed to increase above 30 } } else { if(menu2_Value < -30) //we do not go below 0 { menu2_Value = -30; //the value is not allowed to decrease below -30 } else { // Encoder is rotating in B direction, so decrease menu2_Value = menu2_Value - 0.1; //0.1°C decrement } } } CLKPrevious = CLKNow; // Store last CLK state } //---MENU3--------------------------------------------------------------- //Update interval else if(menu3_selected == true) { CLKNow = digitalRead(RotaryCLK); //Read the state of the CLK pin // If last and current state of CLK are different, then a pulse occurred if (CLKNow != CLKPrevious && CLKNow == 1) { // If the DT state is different than the CLK state then // the encoder is rotating in A direction, so we increase if (digitalRead(RotaryDT) != CLKNow) { if(menu3_Value < 60000) //we do not go above 60000 ms (60 s = 1 min) { menu3_Value = menu3_Value + 100; //100 ms increment } else { menu3_Value = 60000; //the value is not allowed to increase above 60000 } } else { if(menu3_Value < 100) //we do not go below 100 ms { menu3_Value = 100; //the value is not allowed to decrease below 100 } else { // Encoder is rotating B direction so decrease menu3_Value = menu3_Value - 100; //100 ms decrement } } } CLKPrevious = CLKNow; // Store last CLK state } //---MENU4---------------------------------------------------------------- // Tolerance else if(menu4_selected == true) { CLKNow = digitalRead(RotaryCLK); //Read the state of the CLK pin // If last and current state of CLK are different, then a pulse occurred if (CLKNow != CLKPrevious && CLKNow == 1) { // If the DT state is different than the CLK state then // the encoder is rotating in A direction, so we increase if (digitalRead(RotaryDT) != CLKNow) { if(menu4_Value < 10) //we do not go above 10 { menu4_Value = menu4_Value + 0.1; //0.1°C increment } else { menu4_Value = 10; //the value is not allowed to increase above 10 } } else { if(menu4_Value < 0.1) //we do not go below 0.1 { menu4_Value = 0.1; //the value is not allowed to decrease below 0 } else { // Encoder is rotating in B direction, so decrease menu4_Value = menu4_Value - 0.1; //0.1°C decrement } } } CLKPrevious = CLKNow; // Store last CLK state } else //MENU COUNTER---------------------------------------------------------------------------- { CLKNow = digitalRead(RotaryCLK); //Read the state of the CLK pin // If last and current state of CLK are different, then a pulse occurred if (CLKNow != CLKPrevious && CLKNow == 1) { // If the DT state is different than the CLK state then // the encoder is rotating in A direction, so we increase if (digitalRead(RotaryDT) != CLKNow) { if(menuCounter < 3) //we do not go above 3 { menuCounter++; } else { menuCounter = 0; } } else { if(menuCounter < 1) //we do not go below 0 { menuCounter = 3; } else { // Encoder is rotating CW so decrease menuCounter--; } } } CLKPrevious = CLKNow; // Store last CLK state } //Refresh LCD after changing the counter's value refreshLCD = true; } void pushButton() { switch(menuCounter) { case 0: menu1_selected = !menu1_selected; //we change the status of the variable to the opposite break; case 1: menu2_selected = !menu2_selected; break; case 2: menu3_selected = !menu3_selected; break; case 3: menu4_selected = !menu4_selected; break; } refreshLCD = true; //Refresh LCD after changing the value of the menu refreshSelection = true; //refresh the selection ("X") } void printLCD() { //These are the values which are not changing the operation lcd.setCursor(1,0); //1st line, 2nd block lcd.print("OFF"); //text //---------------------- lcd.setCursor(8,0); //1st line, 9th block lcd.print("TEMP: "); //text //---------------------- lcd.setCursor(1,1); //2nd line, 2nd block lcd.print("GOAL: "); //text //---------------------- lcd.setCursor(13,1); //2nd line, 14th block lcd.print("P: "); //text lcd.setCursor(19,1); //2nd line, 20th block lcd.print("%"); //text //---------------------- lcd.setCursor(1,2); //3rd line, 2nd block lcd.print("UPD: "); //text lcd.setCursor(12,2); //3rd line, 13th block lcd.print("ms"); //text //---------------------- lcd.setCursor(1,3); //4th line, 2nd block lcd.print("TOL: "); //text lcd.setCursor(12,3); //4th line, 13th block lcd.print("C"); //text //---------------------- } void updateLCD() //update upon rotating the encoder { //GOAL TEMPERATURE lcd.setCursor(6,1); lcd.print(" "); lcd.setCursor(6,1); lcd.print(menu2_Value,1); //UPDATE/REFRESH INTERVAL lcd.setCursor(5,2); lcd.print(" "); lcd.setCursor(5,2); lcd.print(menu3_Value); //TOLERANCE lcd.setCursor(5,3); lcd.print(" "); lcd.setCursor(5,3); lcd.print(menu4_Value,1); } void updateCursorPosition() { //Clear display's ">" parts lcd.setCursor(0,0); //1st line, 1st block lcd.print(" "); //erase by printing a space lcd.setCursor(0,1); lcd.print(" "); lcd.setCursor(0,2); lcd.print(" "); lcd.setCursor(0,3); lcd.print(" "); //Place cursor to the new position switch(menuCounter) //this checks the value of the counter (0, 1, 2 or 3) { case 0: lcd.setCursor(0,0); //1st line, 1st block lcd.print(">"); break; //------------------------------- case 1: lcd.setCursor(0,1); //2nd line, 1st block lcd.print(">"); break; //------------------------------- case 2: lcd.setCursor(0,2); //3th line, 1st block lcd.print(">"); break; //------------------------------- case 3: lcd.setCursor(0,3); //4th line, 1st block lcd.print(">"); break; } } void updateSelection() { //When a menu is selected ">" becomes "X" if(menu1_selected == true) { lcd.setCursor(1,0); //1st line, 1st block coolingIsOn = !coolingIsOn; //Flip the state of the cooling if(coolingIsOn == true) { lcd.print(" "); lcd.setCursor(1,0); lcd.print("ON"); //If we switched ON the cooling, we print ON } else { lcd.print(" "); lcd.setCursor(1,0); lcd.print("OFF"); //If we switched OFF the cooling, we print OFF } } //------------------- if(menu2_selected == true) { lcd.setCursor(0,1); //2nd line, 1st block lcd.print("X"); } //------------------- if(menu3_selected == true) { lcd.setCursor(0,2); //3rd line, 1st block lcd.print("X"); } //------------------- if(menu4_selected == true) { lcd.setCursor(0,3); //4th line, 1st block lcd.print("X"); } //------------------- } void ConvertToTemperature() { Vout = analogRead(VoutPin)* (3.3/4095); //4095 - 12 bit resolution of the STM32 blue pill //For Arduino users: (5.0 / 1023) R_NTC = (Vout * R_10k) /(Vsupply - Vout); //calculating the resistance of the thermistor Temp_K = (T0*B_param)/(T0*log(R_NTC/R_10k)+B_param); //Temperature in Kelvin Temp_C = Temp_K - 273.15; //converting into Celsius //Update LCD lcd.setCursor(13,0); //1st line, 14th block lcd.print(" "); //erase the content by printing space over it lcd.setCursor(13,0); //1st line, 14th block lcd.print(Temp_C); //print the value of the measured temperature } void writePotmeter() { //CS goes low digitalWrite(CS1_pin, LOW); SPI.transfer(0x11); //command 00010001 [00][01][00][11] SPI.transfer(coolingPower); //transfer the integer value of the potmeter (0-255 value) delayMicroseconds(100); //wait //CS goes high digitalWrite(CS1_pin, HIGH); //nominalVoltage = 5 - (coolingPower * 5.0 / 256.0); //5 V might not be 5.000 V exactly } void adjustTemperature() { if(abs(Temp_C - menu2_Value) < menu4_Value) //Diff is less than tolerance { //If we are within the tolerance, the coolingPower variable is not changed anymore. } else { if(Temp_C > menu2_Value) //if the measured T > set temperature (goal temp) { if(coolingPower < 256) { coolingPower++; //this increases the power to the Peltier - more cooling } else { //do nothing after reaching the max. value of the digital potentiometer } } else //the measured T < set temperature (overcooling) { if(coolingPower > 0) { coolingPower--; //this decreases the power to the Peltier - less cooling } else { //do nothing after reaching the min. value of the digital potentiometer } } //Recalculate PowerPercent PowerPercent = coolingPower /256.0 * 100.0; //POWER lcd.setCursor(15,1); lcd.print(" "); lcd.setCursor(15,1); lcd.print(PowerPercent); //print the power value } }