Arduino Nano zero crossing detection

Irving

Joined Jan 30, 2016
5,170
Interesting. Might be worth playing with different values of Kp from, say, 0.5 (should be reduced error but slower response) to, say, 2.5 (less error, faster response, more over/under-shoot).
 

Thread Starter

mike_the_begginer

Joined Dec 7, 2019
137
Kp = 0.5
1. If I set the temp to 150 C -> it increases from minimum temp to 143 C, then it drops to 138 C, then it stabilizes around 141 C.
2. If I set the temp to 250 C -> it increases from 150 C to 235 C, then it drops to 233 C, then it stabilizes around 241 C.
3. If I set the temp to 350 C -> it increases from 250 to 331 C, then it drops to 330 C, then it stabilizes around 335 C.

Kp = 2.5
1. If I set the temp to 150 C -> it increases from minimum temp to 155 C, then it drops to 145 C, then it stabilizes around 148-149 C.
2. If I set the temp to 250 C -> it increases from 150 C to 252 C, then it drops to 244 C, then it stabilizes around 247-249 C.
3. If I set the temp to 350 C -> it increases from 250 to 349 C, then it drops to 344 C, then it stabilizes around 347 C.

The latest code is:
C:
#include <LiquidCrystal.h>
#include <SPI.h>
#include <Wire.h>
#include <max6675.h>

#define thermoDO 12
#define thermoCS 10
#define thermoCLK 13
#define potentiometer A0
#define zerocrossing 2
#define triac 7
#define relay A1

#define test A2
#define test1 A3

int lowError = 0;
int highError = 20;

float temperature, realTemperature;
int pottemperature;
int counter;
int tempError = false;  // global error flag
int shownError = false; //flag to say error shown
int duty = 0; // variable for duty cycle

//PID constants
double Kp = 2.5;
double Ki = 0;
double Kd = 0;

//PID variables
unsigned long currentTime, previousTime;
double elapsedTime;
double error;
double lastError;
double input, output, setPoint;
double cumError, rateError;

byte thermometer[8] = //icon for termometer
{
  B00100,
  B01010,
  B01010,
  B01110,
  B01110,
  B11111,
  B11111,
  B01110
};

byte arrow[8] = //icon for arrow
{
  B11000,
  B01100,
  B00110,
  B00011,
  B00011,
  B00110,
  B01100,
  B11000
};

MAX6675 thermocouple(thermoCLK, thermoCS, thermoDO);

/*  The circuit:
   LCD RS pin to digital pin 12
   LCD Enable pin to digital pin 11
   LCD D4 pin to digital pin 5
   LCD D5 pin to digital pin 4
   LCD D6 pin to digital pin 3
   LCD D7 pin to digital pin 2
   LCD R/W pin to ground
   LCD VSS pin to ground
   LCD VCC pin to 5V
   10K resistor:
   ends to +5V and ground
   wiper to LCD VO pin (pin 3)
*/

LiquidCrystal lcd(3, 4, 5, 6, 8, 9);

void setup() {
  pinMode(test, OUTPUT);
  pinMode(test1, OUTPUT);
  lcd.createChar(0, thermometer);
  lcd.createChar(1, arrow);
  lcd.begin(16, 2);
  lcd.setCursor(0, 0);
  lcd.print("STATIE DE LIPIT");
  output = 0;
  setPoint = 0;
  delay(1200);
  lcd.clear();
  pinMode(relay, OUTPUT);
  pinMode(potentiometer, INPUT);
  pinMode(zerocrossing, INPUT_PULLUP);
  pinMode(triac, OUTPUT);
  digitalWrite(triac, LOW);
  digitalWrite(relay, HIGH);
  realTemperature = thermocouple.readCelsius();
  temperature = 0.779828 * realTemperature - 10.3427;
  //updateDisplay();
  attachInterrupt(digitalPinToInterrupt(2), zero, RISING);
}

void loop() {
  if (!tempError) {  // if no error
    updateDisplay();
  } else // do something on error
  {
    // eg show the word error on the display
    if (!shownError) { // we've not shown error yet, so show it
      displayErrors();
      shownError = true; //set flag so don't show it again
    }
  }
  delay(250); //controls loop timing
}

void zero() {
  counter++;
  if ((counter == 24) || (counter>duty)) { //reach max count or duty cycle limit.
    digitalWrite(triac, LOW);
  }
 // else *** remove this
  if (counter >= 25) {
    counter = 0;
    digitalWrite(test, HIGH); // *** added, this will generate a pulse on test pin (5) every 250mS to prove counter incrementing...
    pottemperature = analogRead(potentiometer);
    pottemperature = map(pottemperature, 0, 1023, 150, 400);
    digitalWrite(test, LOW); // *** put test pin low
    realTemperature = thermocouple.readCelsius();
    temperature = int(0.779828 * realTemperature - 10.3427); // make temperature an integer
    if (tempError || isnan(realTemperature) || temperature >= 432) { // on error kill power & set global error flag
      digitalWrite(relay, LOW); // turn off power to iron
      tempError = true; //set error flag. can only be unset outside ISR. Once set no further action taken till unset in main loop.
    }
    else { //reading valid
      if (temperature < pottemperature) { //*** change, remove =
        digitalWrite(test1, HIGH);  // *** added, generate a pulse on test1 (D6) when pottemp >= temp
        error = pottemperature - temperature;  // don't think you need abs() here as t cannot be > pt and get here...
        cumError += error * 250.0; //*** change - something got lost in one of my earlier edits
        rateError = (error - lastError) / 250.0; //*** change
        output = Kp * error + Ki * cumError + Kd * rateError; //output error needs to be mapped to a number between 0 and 24
        duty = map(output, lowError, highError, 0, 24); // lowError is const >= 0, highError is const for fully on.
        duty = constrain(duty, 0, 24); // keep duty between 0 and 24 (24 = 96%)
        if(duty>0) { digitalWrite(triac, HIGH); } //*** change, added '>0'
        lastError = error;
        digitalWrite(test1, LOW); // *** added
      }
      else {
        duty = 0;
      }//if (temperature
    }//if (tempError
  }//if(counter == 25...
}//zero

void updateDisplay() {
  pottemperature = analogRead(potentiometer);
  pottemperature = map(pottemperature, 0, 1023, 150, 400);
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.write((byte)0);
  lcd.setCursor(2, 0);
  lcd.print((int)pottemperature);
  lcd.setCursor(6, 0);
  lcd.print((char)223); //degree sign
  lcd.setCursor(7, 0);
  lcd.print("C");
  lcd.setCursor(0, 1);
  lcd.write((byte)1);
  if (temperature <= 45) {
    lcd.setCursor(2, 1);
    lcd.print("Lo");
  } else {
    lcd.setCursor(2, 1);
    lcd.print((int)temperature);
  }
  lcd.setCursor(6, 1);
  lcd.print("[");
  lcd.setCursor(7, 1);
  lcd.print((int)realTemperature);
  lcd.setCursor(10, 1);
  lcd.print("]");
  lcd.setCursor(12, 1);
  lcd.print((char)223);
  lcd.setCursor(13, 1);
  lcd.print("C");
}

void displayErrors() {
  digitalWrite(relay, LOW); // the relay will disconnect the power to the soldering iron heating element
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.write((byte)0);
  lcd.setCursor(1, 0);
  lcd.write((byte)0);
  lcd.setCursor(5, 0);
  lcd.print("ERROR!");
  lcd.setCursor(14, 0);
  lcd.write((byte)0);
  lcd.setCursor(15, 0);
  lcd.write((byte)0);
}
 

Irving

Joined Jan 30, 2016
5,170
Hmmm... looks like its struggling to get to high end @ 350C. What voltage transformer and wattage iron are you using?
How long does it take to heat up?

Updated code...
To see temperature logging, turn on serial monitor and set speed to 115200.

C:
#include <LiquidCrystal.h>
#include <SPI.h>
#include <Wire.h>
#include <max6675.h>

#define thermoDO 12
#define thermoCS 10
#define thermoCLK 13
#define potentiometer A0
#define zerocrossing 2
#define triac 7
#define relay A1

#define test A2
#define test1 A3

int lowError = 0;
int highError = 20;

float temperature, realTemperature;
int pottemperature;
int counter;
int tempError = false;  // global error flag
int shownError = false; //flag to say error shown
int duty = 0; // variable for duty cycle

//PID constants
double Kp = 2.5;
double Ki = 0;
double Kd = 0;

//PID variables
unsigned long currentTime, previousTime;
double elapsedTime;
double error;
double lastError;
double input, output, setPoint;
double cumError, rateError;

byte thermometer[8] = //icon for termometer
{
  B00100,
  B01010,
  B01010,
  B01110,
  B01110,
  B11111,
  B11111,
  B01110
};

byte arrow[8] = //icon for arrow
{
  B11000,
  B01100,
  B00110,
  B00011,
  B00011,
  B00110,
  B01100,
  B11000
};

MAX6675 thermocouple(thermoCLK, thermoCS, thermoDO);

/*  The circuit:
   LCD RS pin to digital pin 12
   LCD Enable pin to digital pin 11
   LCD D4 pin to digital pin 5
   LCD D5 pin to digital pin 4
   LCD D6 pin to digital pin 3
   LCD D7 pin to digital pin 2
   LCD R/W pin to ground
   LCD VSS pin to ground
   LCD VCC pin to 5V
   10K resistor:
   ends to +5V and ground
   wiper to LCD VO pin (pin 3)
*/

LiquidCrystal lcd(3, 4, 5, 6, 8, 9);

void setup() {
  Serial.begin(115200); // or faster if your Arduino/PC can handle it...
  pinMode(test, OUTPUT);
  pinMode(test1, OUTPUT);
  lcd.createChar(0, thermometer);
  lcd.createChar(1, arrow);
  lcd.begin(16, 2);
  lcd.setCursor(0, 0);
  lcd.print("STATIE DE LIPIT");
  output = 0;
  setPoint = 0;
  delay(1200);
  lcd.clear();
  pinMode(relay, OUTPUT);
  pinMode(potentiometer, INPUT);
  pinMode(zerocrossing, INPUT_PULLUP);
  pinMode(triac, OUTPUT);
  digitalWrite(triac, LOW);
  digitalWrite(relay, HIGH);
  realTemperature = thermocouple.readCelsius();
  temperature = 0.779828 * realTemperature - 10.3427;
  //updateDisplay();
  attachInterrupt(digitalPinToInterrupt(2), zero, RISING);
}

// added stuff to log temperatures on serial monitor
// change loop time management from simple delay

#define PRINTRATE 100
#define DISPLAYRATE 250

char textbuf[50]; //buffer for data to send
ulong serialTime = millis(); //sending interval for data
ulong displayTime = serialTime;  //display interval for LCD
int pt; //local store for pot and iron temperatures;
int tmp;

void loop() {

  if(millis() >serialTime+PRINTRATE){ //send serial data every PRINTRATE mS
    noInterrupts(); // make sure our local copies are not corrupted while copying them over from ISR
    tmp = temperature;
    pt = pottemperature;
    interrupts();
    sprintf(textbuf, "Time: %lu, Set: %4u, Temp: %4u", millis()/100, pt, tmp);  //format the print string
    Serial.println(textbuf); //send to serial monitor, about 3mS @ 115200
    serialTime += PRINTRATE;
  }
    
  if(millis()>displayTime+DISPLAYRATE) {//update display every DISPLAYRATE mS
    if (!tempError) {  // if no error
      updateDisplay();
    } else // do something on error
    {
    // eg show the word error on the display
      if (!shownError) { // we've not shown error yet, so show it
        displayErrors();
        shownError = true; //set flag so don't show it again
      }
    }
    displayTime += DISPLAYRATE;
  }
}

void zero() {
  counter++;
  if ((counter == 24) || (counter>duty)) { //reach max count or duty cycle limit.
    digitalWrite(triac, LOW);
  }
 // else *** remove this
  if (counter >= 25) {
    counter = 0;
    digitalWrite(test, HIGH); // *** added, this will generate a pulse on test pin (5) every 250mS to prove counter incrementing...
    pottemperature = analogRead(potentiometer);
    pottemperature = map(pottemperature, 0, 1023, 150, 400);
    digitalWrite(test, LOW); // *** put test pin low
    realTemperature = thermocouple.readCelsius();
    temperature = int(0.779828 * realTemperature - 10.3427); // make temperature an integer
    if (tempError || isnan(realTemperature) || temperature >= 432) { // on error kill power & set global error flag
      digitalWrite(relay, LOW); // turn off power to iron
      tempError = true; //set error flag. can only be unset outside ISR. Once set no further action taken till unset in main loop.
    }
    else { //reading valid
      if (temperature < pottemperature) { //*** change, remove =
        digitalWrite(test1, HIGH);  // *** added, generate a pulse on test1 (D6) when pottemp >= temp
        error = pottemperature - temperature;  // don't think you need abs() here as t cannot be > pt and get here...
        cumError += error * 250.0; //*** change - something got lost in one of my earlier edits
        rateError = (error - lastError) / 250.0; //*** change
        output = Kp * error + Ki * cumError + Kd * rateError; //output error needs to be mapped to a number between 0 and 24
        duty = map(output, lowError, highError, 0, 24); // lowError is const >= 0, highError is const for fully on.
        duty = constrain(duty, 0, 24); // keep duty between 0 and 24 (24 = 96%)
        if(duty>0) { digitalWrite(triac, HIGH); } //*** change, added '>0'
        lastError = error;
        digitalWrite(test1, LOW); // *** added
      }
      else {
        duty = 0;
      }//if (temperature
    }//if (tempError
  }//if(counter == 25...
}//zero

void updateDisplay() {
  pottemperature = analogRead(potentiometer);
  pottemperature = map(pottemperature, 0, 1023, 150, 400);
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.write((byte)0);
  lcd.setCursor(2, 0);
  lcd.print((int)pottemperature);
  lcd.setCursor(6, 0);
  lcd.print((char)223); //degree sign
  lcd.setCursor(7, 0);
  lcd.print("C");
  lcd.setCursor(0, 1);
  lcd.write((byte)1);
  if (temperature <= 45) {
    lcd.setCursor(2, 1);
    lcd.print("Lo");
  } else {
    lcd.setCursor(2, 1);
    lcd.print((int)temperature);
  }
  lcd.setCursor(6, 1);
  lcd.print("[");
  lcd.setCursor(7, 1);
  lcd.print((int)realTemperature);
  lcd.setCursor(10, 1);
  lcd.print("]");
  lcd.setCursor(12, 1);
  lcd.print((char)223);
  lcd.setCursor(13, 1);
  lcd.print("C");
}

void displayErrors() {
  digitalWrite(relay, LOW); // the relay will disconnect the power to the soldering iron heating element
  lcd.clear();
  lcd.setCursor(0, 0);
  lcd.write((byte)0);
  lcd.setCursor(1, 0);
  lcd.write((byte)0);
  lcd.setCursor(5, 0);
  lcd.print("ERROR!");
  lcd.setCursor(14, 0);
  lcd.write((byte)0);
  lcd.setCursor(15, 0);
  lcd.write((byte)0);
}
 

Thread Starter

mike_the_begginer

Joined Dec 7, 2019
137
I am using an 2 x 24 Vac / 100 VA toroidal transformer, with the 2 x 24 V ac secondaries connected in parallel.
It takes less than a minute to reach 350 C, about 57 seconds (measured by the smartphone) starting from the room temperature.
Please find attached the temperature logging.
 

Attachments

Last edited:

Thread Starter

mike_the_begginer

Joined Dec 7, 2019
137
New temperature logging attached - this time starting from room temperature.
First I connected the Arduino to the computer, then I set the temp to 350 C and then I powered on the soldering station, and the soldering iron started heating up - this happened around Time: 170.
 

Attachments

Irving

Joined Jan 30, 2016
5,170
That doesn't look too bad at all. What value of Kp?
Leave that as is, and dial in Ki = 1.0, see what happens on the 150 -> 350C

1595696938899.png
1595696988325.png
 

Irving

Joined Jan 30, 2016
5,170
Hmmm, that wasn't what i expected! Especially the non-linearity in the middle there...

Put Ki=0, try Kp = 5, for 150->350
then try Ki = 2.5 and Ki = 5 for 150->350


1595699980871.png
 

Irving

Joined Jan 30, 2016
5,170
Theoretically a larger Ki should speed things up... but then we're not seeing any serious overshoot either....
What concerns me is Kp = 2.5 and Kp = 5 give virtually identical curves, which suggests something is saturating so early that the loop is intrinsically over-damped.

1595703157851.png

Been a long day - I'm going to eat now, I'll look at the other

Here's a demo of how Kp, Ki and Kd should behave...
PID_Compensation_Animated.gifBy Physicsch - Own work, CC0
 

Thread Starter

mike_the_begginer

Joined Dec 7, 2019
137
I was thinking about using some tuning method for the controller, for example Ziegler-Nichols (I read about this method on the internet), but if you say that there is an intrinsically problem then I guess that it is an error somewhere in the code ... And I don't know where to check the code for that problem.
 
Last edited:

Irving

Joined Jan 30, 2016
5,170
Yes... so we need to characterise the system to make sure we're not asking it to do something it can't. As a starter, try running those same tests with the previous version of software for Kp = 1, 2.5 and 5, Ki=0, for 200 and 300degC

I have a feeling 350 is actually outside the range the system can actually operate.

If you're feeling adventurous, try writing 3 new bits of software as follows:

#1 - that just turns the iron ON full (no ISR, etc, just put D7 HIGH) then records temperature every 250mS until it hasn't increased further for, say, 5sec, then turns it OFF and records the temperature every 250mS until it doesn't drop any further for 5sec, then repeats. This will tell us max rate of increase and max temperature achievable and something above thermal constants

#2 - turns D7 HIGH for increasing numbers of zero-crossing counts from 1 to 24 in a total cycle of 25, holding each count till the temperature stabilises at some rolling average over say a 5sec window. This will tell us something about the effectiveness of PWM for maintaining a specific temperature

#3 - extends #2 by increasing the max count and cycle time from a count of 25 to 50, 75 & 100 to see if those are more effective/meaningful rates.

It could be that the current approach, using PWM for temperature control may not be effective, and a better option is simply to use PID to anticipate & inform when to turn iron on and off and just do a more 'informed' version of the original bang-bang control.
 

Thread Starter

mike_the_begginer

Joined Dec 7, 2019
137
I tested for Kp = 5 and 300 C, but it doesn't overshoot. I think that the same thing happens.

I'll do the tests for other settings, if you need them...

It could be that the current approach, using PWM for temperature control may not be effective, and a better option is simply to use PID to anticipate & inform when to turn iron on and off and just do a more 'informed' version of the original bang-bang control.
If this should be better then why not ...
And if it will work correctly ...

LE: added file with Kp = 5 and 200C
 

Attachments

Last edited:

Irving

Joined Jan 30, 2016
5,170
I tested for Kp = 5 and 300 C, but it doesn't overshoot. I think that the same thing happens.

I'll do the tests for other settings, if you need them...


If this should be better then why not ...
And if it will work correctly ...
Yes, but it does give a clue...
We know it can go above 300, yet this doesn't, but it is oscillating...

I think there's a problem with if (temperature < pottemperature). At the moment we don't allow the controller to overshoot so its always going to settle low. Duty cycle is always 0 unless the temperature is too low. Maybe it should be 50% when on target, and less when over, more when under, to get more fine grained control?

Let's comment that out, and allow + and - errors, like this.. Changes highlighted by //***

C:
//***in declarations

int lowError = -15; //guessing for now.  The bigger these values the smaller the deadband either side of 0 error that constitues a 50% duty cycle
int highError = +15;

void zero() {
  counter++;
//*** change this line below
  if (counter>duty) { //reach duty cycle limit, unless duty was 25 in which case leave on until next duty calculated later
    digitalWrite(triac, LOW);
  }

  if (counter >= 25) {
    counter = 0;
    digitalWrite(test, HIGH); //this will generate a pulse on test pin (5) every 250mS to prove counter incrementing...
    pottemperature = analogRead(potentiometer);
    pottemperature = map(pottemperature, 0, 1023, 150, 400);
    digitalWrite(test, LOW); // put test pin low
    realTemperature = thermocouple.readCelsius();
    temperature = int(0.779828 * realTemperature - 10.3427); // make temperature an integer
    if (tempError || isnan(realTemperature) || temperature >= 432) { // on error kill power & set global error flag
      digitalWrite(relay, LOW); // turn off power to iron
//*** add this line below just in case
      digitalWrite(triac, LOW);
      tempError = true; //set error flag. can only be unset outside ISR. Once set no further action taken till unset in main loop.
    }
    else { //reading valid
//***      if (temperature < pottemperature) { //*** remove this line and allow errors to be both + and -
        digitalWrite(test1, HIGH);  // *** changed, generate a pulse on test1 (D6) when reading valid
        error = pottemperature - temperature;  // *** +ve error when low = increase duty cycle, -ve error when high = decrease it
        cumError += error * 250.0; //
        rateError = (error - lastError) / 250.0; //
        output = Kp * error + Ki * cumError + Kd * rateError; //output error needs to be mapped to a number between 0 and 24
        duty = map(output, lowError, highError, 0, 25); // *** lowError is const for fully off, highError is const for fully on. zero error maps to 50%
        duty = constrain(duty, 0, 25); // ***keep duty between 0 and 25 (25 = 100%)
//*** re-arrange & add 3 lines
        if(duty>0) {
          digitalWrite(triac, HIGH);
        } else {
          digitalWrite(triac, LOW);
        }
        lastError = error;
        digitalWrite(test1, LOW);
//*** remove 3 lines
//      }
//      else {
//        duty = 0;
//      }//if (temperature
    }//if (tempError
  } //if(counter >= 25
}// zero()
LE - updated chart... shows tracking of temp increase consistent across runs.
Also note
DeltaTemperature v Time
DeltaTemperatureTime
50 - 1003.2sec
100-1504.5sec
150-2006sec
200-2508.5sec
250-30011.2sec
1595766163705.png
 
Last edited:

Irving

Joined Jan 30, 2016
5,170
Yes, but for consistency we need to make the set point a step-change in software from cold, rather than a gradual increase, to get a true measure of thermal rate of change...

I'm off out for a while, back this evening...
 

Thread Starter

mike_the_begginer

Joined Dec 7, 2019
137
Yes, but for consistency we need to make the set point a step-change in software from cold, rather than a gradual increase, to get a true measure of thermal rate of change...
I am sorry, but I don't really understand this. Should it be something like a "compare" between the new set point and minimum temperature ?
 

Irving

Joined Jan 30, 2016
5,170
There is an implied 'compare' in calculating the error:

error = pottemperature - temperature;

If
error < 0, we are too high so reduce heat input
error > 0, we are too low, so increase heat input

If error < +/- a small amount, the deadband, do nothing...
 
Top