@WatDo
5 days ago
What’s up my friends, welcome back. What I want, is the aluminum block below to have let’s say, exactly 100 degrees. I’ll control the real temperature using a K type thermocouple. To read the data I'll use the MAX66 75 breakout module and control the PID algorithm with and Arduino. Finally, to apply power we will make a small circuit using a MOSFET or maybe a TRIAC in case of high AC voltages. This will be a close loop. The thermocouple measures
by: ELECTRONOOBS on 2026-06-17
1 x Arduino NANO/UNO LINK eBay
1 x K thermocouple LINK eBay
1 x MAX6675: LINK eBay
1 x Rotary encoder: LINK eBay
1 x i2c LCD: LINK eBay
1 x 12V Heater: LINK eBay
1 x IRFZ44N: LINK eBay
1 x S8050: LINK eBay
Resistors: LINK eBay
Wires LINK eBay
Drilled PCB LINK eBay
Wire, soldering iron, solder, etc...
So, the first thing is to understand how a PID temperature controller will work. As in any PID system we need to define a final process, in our case will be the final temperature that we will achieve. In order to control this temperature, we will need a feedback. So, any PID control will have some sort of feedback. In our case, that feedback is made using a K type thermocouple that will measure the real temperature of the system.
This type of control will also need a setpoint, which in our case is the desired temperature. The system will make the difference between the desired value and the feedback from the output (error), and using 3 constants, the proportional, the integral and derivative, we can change the output according to the feedback.

So, if we want the heating block to have exactly 100 degrees what we will do first is apply power to it. This will start heating up. By the time it reaches the setpoint value, which in this case is 100 degrees, the feedback will inform that to the PID control and this will start lowering the power applied to the heating element, and in our example that will be made using a PWM signal applied to a MOSFET that will control the voltage that goes to the heating element inside the block.
So, it is obvious, if the real temperature is higher than the setpoint, we lower the power value, if the real temperature is lower than the setpoint, well, we increase the power till it reaches the desired value.

If we do just that, that is called P control, or proportional control, and will end up in a temperature oscillation between certain values and it will very difficult or never be stable. For that we add the D control, or derivative. This kind of control will react to the speed of temperature change. So, if, for example, if we blow air on the aluminum block, the derivative will fast push the power to maximum in order to keep the temperature. Finally, we have the I, or integral. This will sum the error on each loop getting bigger and bigger with each loop, or, in case of negative error, getting lower and lower. The sum of all these parts, the P, the I and D, makes a PID control. It’s our job to find the correct constants for each of this PID elements.
So, let’s start building this project. The first thing we will do, is see how to read the real temperature. Below we can see a K type thermocouple, and the MAX 66 75 breakout module. This will amplify and compensate the voltage created by the thermocouple. It has an SPI communication so we'll have to connect these pins to the Arduino SPI port.

Use the connections below and let’s test it out. On a breadboard I connect the MAX 66 75 and the thermocouple. Be careful, the thermocouple has polarity so connect positive to positive and negative to negative. Connect the SPI pins to the Arduino and also supply 5V and GND.

Now upload the next code to the Arduino UNO/NANO. This code will just read the SPI data from the module using a function called "readThermocouple" and that gives us the real temperature. We print the value on the LCD screen. Have in mind that if you don't have the LiquidCrystal_I2C library, you'll have to dwonload it and install it to the Arduino IDE. I heat the thermocouple with a lighter and there you go, I have the real value on the LCD screen.
/* Max6675 Module ==> Arduino
* CS ==> D10
* SO ==> D12
* SCK ==> D13
* Vcc ==> Vcc (5v)
* Gnd ==> Gnd */
//LCD config
#include <Wire.h>
#include <LiquidCrystal_I2C.h> //If you don't have the LiquidCrystal_I2C library, download it and install it
LiquidCrystal_I2C lcd(0x3f,20,4); //sometimes the adress is not 0x3f. Change to 0x27 if it dosn't work.
/* i2c LCD Module ==> Arduino
* SCL ==> A5
* SDA ==> A4
* Vcc ==> Vcc (5v)
* Gnd ==> Gnd */
#include <SPI.h>
#define MAX6675_CS 10
#define MAX6675_SO 12
#define MAX6675_SCK 13
void setup() {
lcd.init();
lcd.backlight();
}
void loop() {
float temperature_read = readThermocouple();
lcd.setCursor(0,0);
lcd.print("TEMPERATURE");
lcd.setCursor(7,1);
lcd.print(temperature_read,1);
delay(300);
}
double readThermocouple() {
uint16_t v;
pinMode(MAX6675_CS, OUTPUT);
pinMode(MAX6675_SO, INPUT);
pinMode(MAX6675_SCK, OUTPUT);
digitalWrite(MAX6675_CS, LOW);
delay(1);
// Read in 16 bits,
// 15 = 0 always
// 14..2 = 0.25 degree counts MSB First
// 2 = 1 if thermocouple is open circuit
// 1..0 = uninteresting status
v = shiftIn(MAX6675_SO, MAX6675_SCK, MSBFIRST);
v <<= 8;
v |= shiftIn(MAX6675_SO, MAX6675_SCK, MSBFIRST);
digitalWrite(MAX6675_CS, HIGH);
if (v & 0x4)
{
// Bit 2 indicates if the thermocouple is disconnected
return NAN;
}
// The lower three bits (0,1,2) are discarded status bits
v >>= 3;
// The remaining bits are the number of 0.25 degree (C) counts
return v*0.25;
}

So now that we know how to read the real temperature, let’s mount this next schematic and control the power applied to the heating element with a MOSFET. I mount the circuit on a breadboard once again and upload the next code. This second code has the PID algorithm already created. We read the temperature, calculate the error, sum the PID values and create a PWM signal on digital pin D3 that will be applied to the MOSFET. I set the desired temperature at 100 degrees and use the LCD to print the set value and the real temperture.

Ok so the code below is a bit large. But don't worry. It is very easy. We set a variable setpoint at 100 degrees for this example. Then we read the thermocouple real temperature value as in the past example. Then we use 3 constants and calculate the PID sum. Depending on that value we create a PWM signal on pin D3 and apply it to the MOSFET gate using a BJT driver.
/* Max6675 Module ==> Arduino
* CS ==> D10
* SO ==> D12
* SCK ==> D13
* Vcc ==> Vcc (5v)
* Gnd ==> Gnd */
//LCD config (i2c LCD screen, you need to install the LiquidCrystal_I2C if you don't have it )
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x3f,20,4); //sometimes the adress is not 0x3f. Change to 0x27 if it dosn't work.
/* i2c LCD Module ==> Arduino
* SCL ==> A5
* SDA ==> A4
* Vcc ==> Vcc (5v)
* Gnd ==> Gnd */
#include <SPI.h>
//We define the SPI pìns
#define MAX6675_CS 10
#define MAX6675_SO 12
#define MAX6675_SCK 13
//Pins
int PWM_pin = 3;
//Variables
float temperature_read = 0.0;
float set_temperature = 100;
float PID_error = 0;
float previous_error = 0;
float elapsedTime, Time, timePrev;
int PID_value = 0;
//PID constants
int kp = 9.1; int ki = 0.3; int kd = 1.8;
int PID_p = 0; int PID_i = 0; int PID_d = 0;
void setup() {
pinMode(PWM_pin,OUTPUT);
TCCR2B = TCCR2B & B11111000 | 0x03; // pin 3 and 11 PWM frequency of 980.39 Hz
Time = millis();
lcd.init();
lcd.backlight();
}
void loop() {
// First we read the real value of temperature
temperature_read = readThermocouple();
//Next we calculate the error between the setpoint and the real value
PID_error = set_temperature - temperature_read;
//Calculate the P value
PID_p = kp * PID_error;
//Calculate the I value in a range on +-3
if(-3 < PID_error <3)
{
PID_i = PID_i + (ki * PID_error);
}
//For derivative we need real time to calculate speed change rate
timePrev = Time; // the previous time is stored before the actual time read
Time = millis(); // actual time read
elapsedTime = (Time - timePrev) / 1000;
//Now we can calculate the D calue
PID_d = kd*((PID_error - previous_error)/elapsedTime);
//Final total PID value is the sum of P + I + D
PID_value = PID_p + PID_i + PID_d;
//We define PWM range between 0 and 255
if(PID_value < 0)
{ PID_value = 0; }
if(PID_value > 255)
{ PID_value = 255; }
//Now we can write the PWM signal to the mosfet on digital pin D3
analogWrite(PWM_pin,255-PID_value);
previous_error = PID_error; //Remember to store the previous error for next loop.
delay(300);
//lcd.clear();
lcd.setCursor(0,0);
lcd.print("PID TEMP control");
lcd.setCursor(0,1);
lcd.print("S:");
lcd.setCursor(2,1);
lcd.print(set_temperature,1);
lcd.setCursor(9,1);
lcd.print("R:");
lcd.setCursor(11,1);
lcd.print(temperature_read,1);
}
double readThermocouple() {
uint16_t v;
pinMode(MAX6675_CS, OUTPUT);
pinMode(MAX6675_SO, INPUT);
pinMode(MAX6675_SCK, OUTPUT);
digitalWrite(MAX6675_CS, LOW);
delay(1);
// Read in 16 bits,
// 15 = 0 always
// 14..2 = 0.25 degree counts MSB First
// 2 = 1 if thermocouple is open circuit
// 1..0 = uninteresting status
v = shiftIn(MAX6675_SO, MAX6675_SCK, MSBFIRST);
v <<= 8;
v |= shiftIn(MAX6675_SO, MAX6675_SCK, MSBFIRST);
digitalWrite(MAX6675_CS, HIGH);
if (v & 0x4)
{
// Bit 2 indicates if the thermocouple is disconnected
return NAN;
}
// The lower three bits (0,1,2) are discarded status bits
v >>= 3;
// The remaining bits are the number of 0.25 degree (C) counts
return v*0.25;
}

As you can see the temperature stays at that value. But that is after trying a lot of PID constants and that is the tricky part of this project. So, what you will have to do is try your own values till you get the correct ones. I advise you to start with the I and D values equal 0 and then increase those values slowly till you get good results.
Here on my oscilloscope I have the PWM signal of the MOSFET connected.

At the beginning, till the system reaches the desired value the pulse has a small width since I use a BJT to activathe the N channel gate, so the mosfet is activated with a LOW value in this case. Once the set value is reached it starts to wambble around and by that maintaining the temperature. As you can see, if I try to cool down the heating element by blowing air with this tube, the PWM signal width get’s lower in order to keep the same value. So, the control works.
Now, all this system needs is some sort of control togheter with the LCD screen in order to view and also be able to set the desired temperature value as this commercial PID controller has the set and up and down buttons. For that I'll use the rotary encoder. It has a push button integrated so I can use it to enter the setpoint menu and increase or decrease the value.

This above is the final schematic of this project. We have an LCD screen to print the values, the rotary encoder with push button insede for control, the thermocouple with the MAX 66 75 module, the MOSFET and the BJT as a driver circuit that will control the power and the heating element. Make sure that the thermocouple is touching the heating element in order to know the real value. I mount everything on the breadboard and now let’s test the new code, which by the way, you can also download from a below.
/* Max6675 Module ==> Arduino
* CS ==> D10
* SO ==> D12
* SCK ==> D13
* Vcc ==> Vcc (5v)
* Gnd ==> Gnd */
#include <SPI.h>
//LCD config
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x3f,20,4); //sometimes the adress is not 0x3f. Change to 0x27 if it dosn't work.
/* i2c LCD Module ==> Arduino
* SCL ==> A5
* SDA ==> A4
* Vcc ==> Vcc (5v)
* Gnd ==> Gnd */
//I/O
int PWM_pin = 3; //Pin for PWM signal to the MOSFET driver (the BJT npn with pullup)
int clk = 8; //Pin 1 from rotary encoder
int data = 9; //Pin 2 from rotary encoder
//Variables
float set_temperature = 0; //Default temperature setpoint. Leave it 0 and control it with rotary encoder
float temperature_read = 0.0;
float PID_error = 0;
float previous_error = 0;
float elapsedTime, Time, timePrev;
float PID_value = 0;
int button_pressed = 0;
int menu_activated=0;
float last_set_temperature = 0;
//Vraiables for rotary encoder state detection
int clk_State;
int Last_State;
bool dt_State;
//PID constants
//////////////////////////////////////////////////////////
int kp = 90; int ki = 30; int kd = 80;
//////////////////////////////////////////////////////////
int PID_p = 0; int PID_i = 0; int PID_d = 0;
float last_kp = 0;
float last_ki = 0;
float last_kd = 0;
int PID_values_fixed =0;
//Pins for the SPI with MAX6675
#define MAX6675_CS 10
#define MAX6675_SO 12
#define MAX6675_SCK 13
void setup() {
pinMode(PWM_pin,OUTPUT);
TCCR2B = TCCR2B & B11111000 | 0x03; // pin 3 and 11 PWM frequency of 928.5 Hz
Time = millis();
Last_State = (PINB & B00000001); //Detect first state of the encoder
PCICR |= (1 << PCIE0); //enable PCMSK0 scan
PCMSK0 |= (1 << PCINT0); //Set pin D8 trigger an interrupt on state change.
PCMSK0 |= (1 << PCINT1); //Set pin D9 trigger an interrupt on state change.
PCMSK0 |= (1 << PCINT3); //Set pin D11 trigger an interrupt on state change.
pinMode(11,INPUT);
pinMode(9,INPUT);
pinMode(8,INPUT);
lcd.init();
lcd.backlight();
}
void loop() {
if(menu_activated==0)
{
// First we read the real value of temperature
temperature_read = readThermocouple();
//Next we calculate the error between the setpoint and the real value
PID_error = set_temperature - temperature_read + 3;
//Calculate the P value
PID_p = 0.01*kp * PID_error;
//Calculate the I value in a range on +-3
PID_i = 0.01*PID_i + (ki * PID_error);
//For derivative we need real time to calculate speed change rate
timePrev = Time; // the previous time is stored before the actual time read
Time = millis(); // actual time read
elapsedTime = (Time - timePrev) / 1000;
//Now we can calculate the D calue
PID_d = 0.01*kd*((PID_error - previous_error)/elapsedTime);
//Final total PID value is the sum of P + I + D
PID_value = PID_p + PID_i + PID_d;
//We define PWM range between 0 and 255
if(PID_value < 0)
{ PID_value = 0; }
if(PID_value > 255)
{ PID_value = 255; }
//Now we can write the PWM signal to the mosfet on digital pin D3
//Since we activate the MOSFET with a 0 to the base of the BJT, we write 255-PID value (inverted)
analogWrite(PWM_pin,255-PID_value);
previous_error = PID_error; //Remember to store the previous error for next loop.
delay(250); //Refresh rate + delay of LCD print
//lcd.clear();
lcd.setCursor(0,0);
lcd.print("PID TEMP control");
lcd.setCursor(0,1);
lcd.print("S:");
lcd.setCursor(2,1);
lcd.print(set_temperature,1);
lcd.setCursor(9,1);
lcd.print("R:");
lcd.setCursor(11,1);
lcd.print(temperature_read,1);
}//end of menu 0 (PID control)
//First page of menu (temp setpoint)
if(menu_activated == 1)
{
analogWrite(PWM_pin,255);
if(set_temperature != last_set_temperature)
{
lcd.clear();
lcd.setCursor(0,0);
lcd.print("Set temperature");
lcd.setCursor(0,1);
lcd.print(set_temperature);
}
last_set_temperature = set_temperature;
}//end of menu 1
//Second page of menu (P set)
if(menu_activated == 2)
{
if(kp != last_kp)
{
lcd.clear();
lcd.setCursor(0,0);
lcd.print("Set P value ");
lcd.setCursor(0,1);
lcd.print(kp);
}
last_kp = kp;
}//end of menu 2
//Third page of menu (I set)
if(menu_activated == 3)
{
if(ki != last_ki)
{
lcd.clear();
lcd.setCursor(0,0);
lcd.print("Set I value ");
lcd.setCursor(0,1);
lcd.print(ki);
}
last_ki = ki;
}//end of menu 3
//Forth page of menu (D set)
if(menu_activated == 4)
{
if(kd != last_kd)
{
lcd.clear();
lcd.setCursor(0,0);
lcd.print("Set D value ");
lcd.setCursor(0,1);
lcd.print(kd);
}
last_kd = kd;
}//end of menu 4
}//Loop end
//The function that reads the SPI data from MAX6675
double readThermocouple() {
uint16_t v;
pinMode(MAX6675_CS, OUTPUT);
pinMode(MAX6675_SO, INPUT);
pinMode(MAX6675_SCK, OUTPUT);
digitalWrite(MAX6675_CS, LOW);
delay(1);
// Read in 16 bits,
// 15 = 0 always
// 14..2 = 0.25 degree counts MSB First
// 2 = 1 if thermocouple is open circuit
// 1..0 = uninteresting status
v = shiftIn(MAX6675_SO, MAX6675_SCK, MSBFIRST);
v <<= 8;
v |= shiftIn(MAX6675_SO, MAX6675_SCK, MSBFIRST);
digitalWrite(MAX6675_CS, HIGH);
if (v & 0x4)
{
// Bit 2 indicates if the thermocouple is disconnected
return NAN;
}
// The lower three bits (0,1,2) are discarded status bits
v >>= 3;
// The remaining bits are the number of 0.25 degree (C) counts
return v*0.25;
}
//The interruption vector for push button and rotary encoder
ISR(PCINT0_vect){
if(menu_activated==1)
{
clk_State = (PINB & B00000001); //pin 8 state? It is HIGH?
dt_State = (PINB & B00000010);
if (clk_State != Last_State){
// If the data state is different to the clock state, that means the encoder is rotating clockwise
if (dt_State != clk_State) {
set_temperature = set_temperature+0.5 ;
}
else {
set_temperature = set_temperature-0.5;
}
}
Last_State = clk_State; // Updates the previous state of the clock with the current state
}
if(menu_activated==2)
{
clk_State = (PINB & B00000001); //pin 8 state?
dt_State = (PINB & B00000010);
if (clk_State != Last_State){
// If the data state is different to the clock state, that means the encoder is rotating clockwise
if (dt_State != clk_State) {
kp = kp+1 ;
}
else {
kp = kp-1;
}
}
Last_State = clk_State; // Updates the previous state of the clock with the current state
}
if(menu_activated==3)
{
clk_State = (PINB & B00000001); //pin 8 state?
dt_State = (PINB & B00000010);
if (clk_State != Last_State){
// If the data state is different to the clock state, that means the encoder is rotating clockwise
if (dt_State != clk_State) {
ki = ki+1 ;
}
else {
ki = ki-1;
}
}
Last_State = clk_State; // Updates the previous state of the clock with the current state
}
if(menu_activated==4)
{
clk_State = (PINB & B00000001); //pin 8 state?
dt_State = (PINB & B00000010);
if (clk_State != Last_State){
// If the data state is different to the clock state, that means the encoder is rotating clockwise
if (dt_State != clk_State) {
kd = kd+1 ;
}
else {
kd = kd-1;
}
}
Last_State = clk_State; // Updates the previous state of the clock with the current state
}
//Push button was pressed!
if (PINB & B00001000) //Pin D11 is HIGH?
{
button_pressed = 1;
}
//We navigate through the 4 menus with each button pressed
else if(button_pressed == 1)
{
if(menu_activated==4)
{
menu_activated = 0;
PID_values_fixed=1;
button_pressed=0;
delay(1000);
}
if(menu_activated==3)
{
menu_activated = menu_activated + 1;
button_pressed=0;
kd = kd + 1;
delay(1000);
}
if(menu_activated==2)
{
menu_activated = menu_activated + 1;
button_pressed=0;
ki = ki + 1;
delay(1000);
}
if(menu_activated==1)
{
menu_activated = menu_activated + 1;
button_pressed=0;
kp = kp + 1;
delay(1000);
}
if(menu_activated==0 && PID_values_fixed != 1)
{
menu_activated = menu_activated + 1;
button_pressed=0;
set_temperature = set_temperature+1;
delay(1000);
}
PID_values_fixed = 0;
}
}
The default value is now 0 degrees. Press the set button of the rotary encoderRotate it to the left in order to increase or decrease the temperature value. Press the set button once again and now you can set the P constant for the PID control. Press once again and select the I value. Finally, press the button again and select the D value. Now press the button and exit the menu and the new settings are stored. I set it to 100 degrees and now the real read starts increasing till it reaches the desired value and it stays there. When we reach the desired value you can see the PWM wombelling in order to maintain that value.

There you go our PID of temperature works. I could 3D print a case for this project just as the commercial one has, but, since I’ll use this project for my soldering station in a future tutorial, I won’t do that now. In a future project, I’ll use this 12V soldering iron with a thermocouple inside and make my own soldering station so stay tuned for that guys.
Have in mind that today’s project is for only DC power control, so only DC heating elements will work. I’ll also make a tutorial on a temperature PID control, but for AC 220V voltage using a TRIAC control as in the past TRIAC tutorial.
If my videos/tutorials help you and you would like to help my projects, I have a Patreon campaign. I would really appreciate that guys and it will help my workshop for other new projects and keep this channel/webpage going. BTW, thanks to all my Patreons.
I hope that you’ve enjoyed this small tutorial on PID control. If you have any question about this video or any other, just leave it in the comment section below or on my Q&A page. Also, don’t forget to subscribe and watch all of my other great tutorials. Remember, if you consider helping my projects check my Patreon page as well.
@WatDo
5 days ago
Leave a comment
Please login in order to comment.