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

The MCP41100 uses SPI and the 20x4 LCD uses i2C communication (notice that the schematic shows a 16x2 LCD). The rotary encoder uses the PB3 for CLK, PB4 for DT and PB5 for SW. The wiper of the MCP41100 is connected to the 10 kOhm resistor which is connected to the pin 2-3 of the CV potentiometer on the SZBK07 board. The NTC resistor is in a voltage divider configuration with a 10 kOhm resistor. The input voltage of the divider is +3.3 V and the Vout of the voltage divider is measured on the A0 pin. The MCP41100 gives +5V by default when the bit value is 0. By increasing the bit value, the output voltage of the digital potentiometer drops.



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    
  }  
}

Previous
Previous

Dual Arduino solution for smooth and effortless stepper motor control

Next
Next

Advanced menu system with rotary encoder