Arduino Playground is read-only starting December 31st, 2018. For more info please look at this Forum Post

AC Phase Control

This sketch uses a 'Random Phase' or 'Non Zero Crossing' SSR (I'm using the Omron G3MC-202PL DC5) to act as an A/C switch and an opto-isolated AC zero crossing detector (the H11AA1) to give us a zero-crossing reference. This allows the Arduino to dim lights, change the temp of heaters & speed control AC motors.

The software uses dual interrupts (both triggered by Timer1) to control how much of the AC wave the load receives. The first interrupt, zero_cross_detect(), is triggered by the Zero Cross detector on pin 3 (aka IRQ1). It resets Timer1's counter and attaches nowIsTheTime to a new interrupt to be fired midway though the AC cycle. Control flows back to the loop until we have waited the specified time. Then nowIsTheTime pulses the AC_PIN high long enough for the SSR to open, and returns control to the loop.

ACPhaseControl.pde

/* Copyright 2011 Lex Talionis

This sketch uses a 'Random Phase' or 'Non Zero Crossing' SSR (I'm using 
the Omron G3MC-202PL DC5) to act as an A/C switch and an opto-isolated 
AC zero crossing detector (the H11AA1) to give us a zero-crossing 
reference. This allows the Arduino to dim lights, change the temp of 
heaters & speed control AC motors.

The software uses dual interrupts (both triggered by Timer1) to control 
how much of the AC wave the load receives. The first interrupt, 
zero_cross_detect(), is triggered by the Zero Cross detector on pin 3 
(aka IRQ1). It resets Timer1's counter and attaches nowIsTheTime to a 
new interrupt to be fired midway though the AC cycle. Control flows back 
to the loop until we have waited the specified time. Then nowIsTheTime 
pulses the AC_PIN high long enough for the SSR to open, and returns 
control to the loop.


This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.


Based on:
AC Light Control by Ryan McLaughlin <ryanjmclaughlin@gmail.com>
https://web.archive.org/web/20100208075205/https://www.arduino.cc/cgi-bin/yabb2/YaBB.pl?num=1230333861

Thanks to https://www.andrewkilpatrick.org/blog/?page_id=445 
and https://www.hoelscher-hi.de/hendrik/english/dimmer.htm

More information available at:
https://playground.arduino.cc/Code/ACPhaseControl

*/

#include <TimerOne.h>	// Available from https://playground.arduino.cc/Code/Timer1
#define FREQ 60 	// 60Hz power in these parts
#define AC_PIN 9	// Output to Opto Triac
#define LED 13		// builtin LED for testing
#define VERBOSE 1	// can has talk back?

#define DEBUG_PIN 5	//scope this pin to measure the total time for the interrupt to run
int inc=1;

volatile byte state = 255;	// controls what interrupt should be 
                            //attached or detached while in the main loop
double wait = 3276700000;	//find the squareroot of this in your spare time please


char cmd = 0;			//Buffer for serial port commands
unsigned long int period = 1000000 / (2 * FREQ);//The Timerone PWM period in uS, 60Hz = 8333 uS
int hexValue = 0;		// the value from serial a serial port(0-0xFFF)
unsigned int onTime = 0;	// the calculated time the triac is conducting
unsigned int offTime = period-onTime;	//the time to idle low on the AC_PIN
int hexInput(int len);		//interprets a hex packet ":XXX" - len hex digits

void setup()
{
    Serial.begin(115200);	//start the serial port at 115200 baud we want
    Serial.println("AC Motor Control v1");	//the max speed here so any
    #ifdef VERBOSE		//debugging output wont slow down our time sensitive interrupt
    pinMode(DEBUG_PIN, OUTPUT);
    digitalWrite(DEBUG_PIN, LOW);
    Serial.println("----- VERBOSE -----");	// feeling talkative?
    #endif
    pinMode(AC_PIN, OUTPUT);		// Set the Triac pin as output
    pinMode(LED, OUTPUT);
    attachInterrupt(1, zero_cross_detect, RISING); 	// Attach an Interrupt to Pin 3 (interupt 1) for Zero Cross Detection
    Timer1.initialize(period);
    //	Timer1.disablePwm(9);
    Timer1.disablePwm(10);
} 

void zero_cross_detect()	// function to be fired at the zero crossing.  This function
{				// keeps the AC_PIN full on or full off if we are at max or min
    Timer1.restart();	// or attaches nowIsTheTime to fire at the right time.
    state=B00000011;
    #ifdef VERBOSE	
    digitalWrite(DEBUG_PIN, HIGH);
    #endif
    if (offTime<=100)			//if off time is very small
    {
        digitalWrite(AC_PIN, HIGH);	//stay on all the time
        state=0;			// no update this period
        #ifdef VERBOSE
        //Serial.print("Full on\t");
        #endif
    }
    else if (offTime>=8000) {		//if offTime is large
        digitalWrite(AC_PIN, LOW);	//just stay off all the time
        state=0;			//no update this period
        #ifdef VERBOSE
        //Serial.print("Full off\t");
        #endif
    }
    else	//otherwise we want the motor at some middle setting
    {
        Timer1.attachInterrupt(nowIsTheTime,offTime);
    }
    #ifdef VERBOSE
    digitalWrite(DEBUG_PIN, LOW);
    #endif
}		// End zero_cross_detect

void nowIsTheTime ()
{
    #ifdef VERBOSE
    digitalWrite(DEBUG_PIN, LOW);
    #endif
    if (state==1)		//the interrupt has been engaged and we are in the dwell time....
    {
        digitalWrite(AC_PIN,HIGH);
        wait = sqrt(wait);		//delay wont work in an interrupt.
        if (!wait)                      // this takes 80uS or so on a 16Mhz proccessor
        {
            wait = 3276700000;
        }
        digitalWrite(AC_PIN,LOW);
        state = B00000010;
    }
    #ifdef VERBOSE
    digitalWrite(DEBUG_PIN, LOW);
    #endif
}

void loop() {			// Non time sensitive tasks - read the serial port
    /*	offTime = offTime + inc;        //walk up and down debug routine
    if (offTime>=8100)
    {
        inc = -4;
    }
    else if (offTime<=500)
    {
        inc = 4;
    }*/	
    hexValue = hexInput(3);	// Read a 3 digit hex number off the serial
    if (hexValue < 0) {
        //no input, so do nothing
        if(state==B00000011)	//its before the turn on time
        {
            Timer1.attachInterrupt(nowIsTheTime,offTime);
            state=B00000001;	//when it is the time for nowIsTheTime the state will align with unity
        }
        else if(state==B00000010)	//its after turn on time
        {
            Timer1.detachInterrupt();
            attachInterrupt(1, zero_cross_detect, RISING);
            state=B00000000;
        }
    } else {       
        onTime = map(hexValue, 0, 4095, 0, period);	// re scale the value from hex to uSec 
        offTime = period - onTime;			// off is the inverse of on, yay!
        #ifdef VERBOSE
        //Serial.print("In loop:\t");
        //Serial.print("Input Val \t");
        //Serial.print(hexValue);
        //Serial.print("\tperiod:");
        //Serial.print(period);
        //Serial.print("\tonTime:");
        //Serial.print(onTime);
        Serial.print("\toffTime:");
        Serial.println(offTime);
        #endif
    }
}

int hexInput(int len) {		//serial device sends ":XXX" - three hex digits, repeating for ever
    int val = -1;
    if (Serial.available() > len)   {
        int count = 0;		//when count gets to 8 we have a full packet
        #ifdef VERBOSE
        //Serial.println("");
        //Serial.print("Input:");
        #endif

        val = 0;
        while (count != 1<<len)
        {
            cmd = Serial.read();
            switch ( ( ('0'<=cmd) && (cmd<='9') ) //1 if cmd is a ascii numeral
            + (2 * ( ('A'<=cmd) && (cmd<='F') ) ) //2 if cmd is A-F
            + (2 * ( ('a'<=cmd) && (cmd<='f') ) ) //           or a-f
            + (4 * (   cmd==':'             ) ) ) //4 if cmd is a colon - returns 0 for all other chars
            {
            case 1:		//cmd is a numeral
                {
                    Serial.print(cmd);
                    cmd -= '0';
                    count = count<<1; //double count
                    break;
                }	
            case 2:		//cmd is a letter
                {
                    Serial.print(cmd);
                    cmd = (cmd - 'A') + 10;	
                    count = count<<1; //double count
                    // after being turned on by a colon then doubled len times count == 2^len or 1<<len
                    break;
                }
            case 4:		//cmd is a colon - clear the accumulator
                {
                    Serial.print(':');
                    val=0;			//clear the accumulator
                    cmd=0;
                    count=1; //we can start counting now!
                    break;
                }
            case 0: 	//anything else
                {
                    Serial.print('!', DEC);
                    val = -1;		//Set the error condition
                    goto bailout;	//if cmd isn't anything we want, dump the whole packet
                }
            }
            val = (val*16) + cmd;	
        }

        #ifdef VERBOSE
        Serial.print("\tinput val:");
        Serial.println(val);
        #endif
    }
    bailout:
    return val;
}

SimplerACPhaseControl.pde

/*

  By Jonas Ekstrand 2017-04-09

*/
#include <TimerOne.h>
// FOUND THAT USING TRIAL AND ERROR GIVES ME THE BEST VALUES..
#define DIM_MAX_MICROS 9800
#define BRIGHT_MAX_MICROS 5128//3400
#define FULL_ON_MARGIN_MICROS 50
#define INTERRUPT 1
#define AC_PIN 4
int offTime = DIM_MAX_MICROS;
String inputString = "";
boolean stringComplete = false;

void serialEvent() {
  //Excpecting 0-100 from the input to end with new line
  while (Serial.available()) {
    char inChar = (char)Serial.read();
    inputString += inChar;
    if (inChar == '\n') {
      stringComplete = true;
    }
  }
}

void zero_cross_detect() {
  Timer1.restart(); // sync with zero cross detect
  Timer1.attachInterrupt(nowIsTheTime, offTime);
}

void nowIsTheTime() {
  if (offTime < DIM_MAX_MICROS) // FULL OFF?
    digitalWrite(AC_PIN, HIGH);
  if (offTime > (BRIGHT_MAX_MICROS + FULL_ON_MARGIN_MICROS)) // FULL ON?
    digitalWrite(AC_PIN, LOW);
}

void setup() {
  inputString.reserve(100);
  Serial.begin(9600);
  pinMode(AC_PIN, OUTPUT);    // Set the Triac pin as output
  attachInterrupt(INTERRUPT, zero_cross_detect, RISING);
  Timer1.initialize(DIM_MAX_MICROS);
  Timer1.disablePwm(9);
  Timer1.disablePwm(10);
}

void loop() {    // Non time sensitive tasks - ie. read the serial port
  if (stringComplete) {
    Serial.print("dimming to:");
    Serial.println(inputString);
    offTime = map(inputString.toInt(), 0, 100, DIM_MAX_MICROS, BRIGHT_MAX_MICROS);
    // clear the string:
    inputString = "";
    stringComplete = false;
  }
}