AC Phase Control

This sketch uses a ‘Random Phase’ or ‘Non Zero Crossing’ SSR (Im using the Omron G3MC-202PL DC5) to act as an A/C switch and an opto-isolataed AC zero crossing dectector (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

ACPhaseControl.pde

/* Copyright 2011 Lex Talionis

This sketch uses a 'Random Phase' or 'Non Zero Crossing' SSR (Im using 
the Omron G3MC-202PL DC5) to act as an A/C switch and an opto-isolataed 
AC zero crossing dectector (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 <http://www.gnu.org/licenses/>.


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

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

Circut Diagram and more information available at:
http://playground.arduino.cc/Code/ACPhaseControl

*/

#include <TimerOne.h>	// Avaiable from http://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 intrupt 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 Interupt 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 proc
        {
            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; //doubble count
                    // after being turned on by a colon then doubbled 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 isnt anything we want, dump the whole packet
                }
            }
            val = (val*16) + cmd;	
        }

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