This setup is an AC 220 volts PID control for temperature. In previous tutorials, if you remember, we have made a PID temperature control for DC voltage and a TRIAC AC voltage control. A lot of you asked me for a combination between those two videos.
by: ELECTRONOOBS on 2026-08-16
So, today we will read the temperature with a thermocouple, detect the zero cross of the AC voltage, create the PID control and change the firing angle at the TRIAC gate and by that control the temperature of a 220V heater.
1 x Arduino NANO/UNO LINK eBay
1 x MAX6675 LINK eBay
1 x K-Type thermocouple LINK eBay
1 x i2c LCD LINK eBay
1 x BTA16 TRIAC: LINK eBay
1 x MOC3020: LINK eBay
1 x 220V AC heater: LINK eBay
1 x Full-bridge rectifier: LINK eBay
1 x EL817: LINK eBay
2 x push button: LINK eBay
3 x 1k resistor: LINK eBay
2 x 47k resistor: LINK eBay
1 x 200 resistor: LINK eBay
1 x 100R resistor: LINK eBay
Wires LINK eBay
Wire, soldering iron, solder, etc...
Ok, below you can see the 220V sine wave. As you can see it passes the same amount of time below 0V as above 0V. We need to detect the moment when the wave passes from negative to positive or vice-versa. Why? Well, after the zero-cross, if we send a firing pulse to the TRIAC, we could control the amount of the wave will pas and by that the amount of power as we will se later. So in order to be synchronized, we need to detect the zero-corss.

So, witha microcontroller we need to detect that zero-cross. How we do that? well we use two components and those are a full-bridge rectifier and an photocoupler. We do that because the microcontroller can't work with negative voltages and above 5V. The full-bridge rectifier will give only positive wave and the photocoupler will lower the voltage to 5Vpp. Now we could read that signal with an Arduino. See the signal below.

As you can see in the photo above on the 5Vpp signal, we have a los pulse each zero cros now. With the Arduino is very easy to detect that. We will see later in the code, an interruption made on digital pin D8. To read that pulse, the next scheamtic was used for the full-bridge rectifier and the photocoupler.

ow to read the thermocouple with the amplifier mdoule and the Arduino. make the connections as below.

#include "max6675.h" //INCLUDE THE LIBRARY
int thermoDO = 9;
int thermoCS = 8;
int thermoCLK = 13;
MAX6675 thermocouple(thermoCLK, thermoCS, thermoDO);
void setup() {
Serial.begin(9600);
Serial.println("MAX6675 test");
// wait for MAX chip to stabilize
delay(500);
}
void loop() {
// basic readout test, just print the current temp
Serial.print("C = ");
Serial.println(thermocouple.readCelsius());
Serial.print("F = ");
Serial.println(thermocouple.readFahrenheit());
delay(1000);
}
Ok, see the scheamtic above. Connect the K-type Thermocouple as shown and connect the the SPI pins to D13, D10 and D9. To test the circuit, upload the enxt example code and open the serial monitor at a baud rate of 9600. Heat the thermocouple and see the resuls. Ok, now we know how to measure temperature with the K-type thermocouple. Now we have to see how to control the power applied to the AC heater using the TRIAC.
Ok, see more about TRIAC control on the past tutorial here. Below you have the final schematic for this project. We have the rectifier and photocoupler to detect the zero-cross, then the

We detect the zero-cross, measure the temperature, calculate the delay of the firing pulse with the PID control and create the pulse on the "Firing_pulse" fin connected to the MOC3020 optocoupler that will apply a pulse to the TRIAC. This will control the amount of power that will pass to the AC heater as you can see in the photo below.

The small bit of code below is the one taht creates the small firing pulse of 100us. With the delayMicroseconds(maximum_firing_delay - PID_value); we control the delay between the zero-cross and the firing pulse and by that the amount of the wave that will pass. In the photo below the amout of wave is more or less 50%.
//If the zero cross interruption was detected we create the 100us firing pulse
if (zero_cross_detected)
{
delayMicroseconds(maximum_firing_delay - PID_value); //This delay controls the power
digitalWrite(firing_pin,HIGH);
delayMicroseconds(100);
digitalWrite(firing_pin,LOW);
zero_cross_detected = false;
}
Make sure you mount the schenatic as before. Downlaod the code and uplaod it to the Arduino. Read all the comments in the code to understand more. The code is simple. Remember to install the i2c liquid crystal library and the MAX 66 75 in order to be able to compile this code.
In short words, it goes like this. The interruption on digital pin D8 will detect the zero cross of the main AC signal. Next, we create the PID value and map that from 1 microsecondsv to 7.4 milliseconds and that will be the delay between the zero-cross detection to the firing angle pulse. The rest of the code is for the push buttons read, measure the temperature and fix the setpoint with the buttons. Compile and upload.
/* Max6675 Module ==> Arduino
* CS ==> D10
* SO ==> D9
* SCK ==> D13
* Vcc ==> Vcc (5v)
* Gnd ==> Gnd */
//LCD config
#include "max6675.h"
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
LiquidCrystal_I2C lcd(0x27,20,4); //sometimes the adress is not 0x27. Change to 0x3f if it dosn't work.
/* i2c LCD Module ==> Arduino
* SCL ==> A5
* SDA ==> A4
* Vcc ==> Vcc (5v)
* Gnd ==> Gnd */
//Inputs and outputs
int firing_pin = 3;
int increase_pin = 11;
int decrease_pin = 12;
int zero_cross = 8;
int thermoDO = 9;
int thermoCS = 10;
int thermoCLK = 13;
//Start a MAX6675 communication with the selected pins
MAX6675 thermocouple(thermoCLK, thermoCS, thermoDO);
//Variables
int last_CH1_state = 0;
bool zero_cross_detected = false;
int firing_delay = 7400;
//////////////////////////////////////////////////////
int maximum_firing_delay = 7400;
/*Later in the code you will se that the maximum delay after the zero detection
* is 7400. Why? Well, we know that the 220V AC voltage has a frequency of around 50-60HZ so
* the period is between 20ms and 16ms, depending on the country. We control the firing
* delay each half period so each 10ms or 8 ms. To amke sure we wont pass thsoe 10ms, I've made tests
* and the 7400us or 7.4ms was a good value. Measure your frequency and chande that value later */
//////////////////////////////////////////////////////
unsigned long previousMillis = 0;
unsigned long currentMillis = 0;
int temp_read_Delay = 500;
int real_temperature = 0;
int setpoint = 100;
bool pressed_1 = false;
bool pressed_2 = false;
//PID variables
float PID_error = 0;
float previous_error = 0;
float elapsedTime, Time, timePrev;
int PID_value = 0;
//PID constants
int kp = 203; int ki= 7.2; int kd = 1.04;
int PID_p = 0; int PID_i = 0; int PID_d = 0;
void setup() {
//Define the pins
pinMode (firing_pin,OUTPUT);
pinMode (zero_cross,INPUT);
pinMode (increase_pin,INPUT);
pinMode (decrease_pin,INPUT);
PCICR |= (1 << PCIE0); //enable PCMSK0 scan
PCMSK0 |= (1 << PCINT0); //Set pin D8 (zero cross input) trigger an interrupt on state change.
PCMSK0 |= (1 << PCINT3); //Set pin D11 (increase button) trigger an interrupt on state change.
PCMSK0 |= (1 << PCINT4); //Set pin D12 (decrease button) trigger an interrupt on state change.
lcd.init(); //Start the LC communication
lcd.backlight(); //Turn on backlight for LCD
}
void loop() {
currentMillis = millis(); //Save the value of time before the loop
/* We create this if so we will read the temperature and change values each "temp_read_Delay"
* value. Change that value above iv you want. The MAX6675 read is slow. Tha will affect the
* PID control. I've tried reading the temp each 100ms but it didn't work. With 500ms worked ok.*/
if(currentMillis - previousMillis >= temp_read_Delay){
previousMillis += temp_read_Delay; //Increase the previous time for next loop
real_temperature = thermocouple.readCelsius(); //get the real temperature in Celsius degrees
PID_error = setpoint - real_temperature; //Calculate the pid ERROR
if(PID_error > 30) //integral constant will only affect errors below 30ºC
{PID_i = 0;}
PID_p = kp * PID_error; //Calculate the P value
PID_i = PID_i + (ki * PID_error); //Calculate the I value
timePrev = Time; // the previous time is stored before the actual time read
Time = millis(); // actual time read
elapsedTime = (Time - timePrev) / 1000;
PID_d = kd*((PID_error - previous_error)/elapsedTime); //Calculate the D value
PID_value = PID_p + PID_i + PID_d; //Calculate total PID value
//We define firing delay range between 0 and 7400. Read above why 7400!!!!!!!
if(PID_value < 0)
{ PID_value = 0; }
if(PID_value > 7400)
{ PID_value = 7400; }
//Printe the values on the LCD
lcd.clear();
lcd.setCursor(0,0);
lcd.print("Set: ");
lcd.setCursor(5,0);
lcd.print(setpoint);
lcd.setCursor(0,1);
lcd.print("Real temp: ");
lcd.setCursor(11,1);
lcd.print(real_temperature);
previous_error = PID_error; //Remember to store the previous error.
}
//If the zero cross interruption was detected we create the 100us firing pulse
if (zero_cross_detected)
{
delayMicroseconds(maximum_firing_delay - PID_value); //This delay controls the power
digitalWrite(firing_pin,HIGH);
delayMicroseconds(100);
digitalWrite(firing_pin,LOW);
zero_cross_detected = false;
}
}
//End of void loop
// |
// |
// |
// v
//See the interruption vector
//This is the interruption routine (pind D8(zero cross), D11(increase) and D12(decrease))
//----------------------------------------------
ISR(PCINT0_vect){
///////////////////////////////////////Input from optocoupler
if(PINB & B00000001){ //We make an AND with the state register, We verify if pin D8 is HIGH???
if(last_CH1_state == 0){ //If the last state was 0, then we have a state change...
zero_cross_detected = true; //We have detected a state change! We need both falling and rising edges
}
}
else if(last_CH1_state == 1){ //If pin 8 is LOW and the last state was HIGH then we have a state change
zero_cross_detected = true; //We haev detected a state change! We need both falling and rising edges.
last_CH1_state = 0; //Store the current state into the last state for the next loop
}
if(PINB & B00001000){ //We make an AND with the state register, We verify if pin D11 is HIGH???
if (!pressed_1)
{
setpoint = setpoint + 5; //Increase the temperature by 5. Change this with your value if you want.
delay(20);
pressed_1 = true;
}
}
else if (pressed_1)
{
pressed_1 = false;
}
if(PINB & B00010000){ //We make an AND with the state register, We verify if pin D12 is HIGH???
if (!pressed_2)
{
setpoint = setpoint - 5; //Decrease the temperature by 5. Change this with your value if you want.
delay(20);
pressed_2 = true;
}
}
else if (pressed_2)
{
pressed_2 = false;
}
}
//End of interruption vector for pins on port B: D8-D13
So, there you have it. I could now set the temperature and it will stay there thanks to the PID control. The temperature sometimes has 2 or 3 degrees error but for now that is a good result. In my case, the setpoint step is 5 degrees but you could change that in the code of you want.

Leave a comment
Please login in order to comment.