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.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;
}

