I just connected a couple dots....
that_chap kept talking about SSRs (Solid State Relays) and I kept reading it as SCRs (Silicon Controlled Rectifiers).
To clear things up a little: SSRs are made from a back-to-back pair of opto-controlled triacs, which means they are functionally the same as the triacs on Ryan's board. So yes, you could use SSRs to do the load switching. However, when I look up the cost of SSRs vs. the cost of triacs with similar specs, the SSRs are an order of magnitude more expensive and considerably larger.
SCRs are sort of similar to triacs, but only conduct on one half of the wave, so you need two of them in inverse parallel to work like a triac. But that would be pointless since you can just use a triac.
EDIT: NEW CODE IS FINISHED!!
OK, it works great at 50/60Hz but it's range isn't all that good at 400Hz. The problem is that it takes about 500us to do all the timing calculations on four channels, which is about 2/5 of the half-period at 400Hz. If you wanted just one channel, you could scale down the code and 400Hz would work fine with about 125us minimum dim. But the dimming is smooth and more-or-less frequency agnostic. The lower the frequency, the better.
I intentionally did not use loops to cycle through the channels. Loops added processing overhead and with four channels, it really wasn't that hard to keep track of. Another weird-but-intentional thing is the placement of the OKToFire=0; lines. I did this because it really didn't matter where these lines went in each section and it adds a little time delay between the digitalWrite lines without using delayMicroseconds()
You can see the code in action if you have a dual trace oscilloscope. Attach one input to the interrupt pin and another to an output pin. Sync to the interrupt pin and you will see how all the timing works together.
I fought with this code for days until I re-read the attachInterrupt() doc. It states that millis() and delay() don't work in the ISR function. That was key to success since I was trying to calculate all the timing in the ISR function, but was getting weird and inconsistent results. Once I took all that out and put it in void loop(), all was better.
/*
AC Light Control
Ryan McLaughlin <ryanjmclaughlin@gmail.com>
The hardware consists of an Triac to act as an A/C switch and
an opto-isolator to give us a zero-crossing reference.
The software uses two interrupts to control dimming of the light.
The first is a hardware interrupt to detect the zero-cross of
the AC sine wave, the second is software based and always running
at 1/128 of the AC wave speed. After the zero-cross is detected
the function check to make sure the proper dimming level has been
reached and the light is turned on mid-wave, only providing
partial current and therefore dimming our AC load.
Thanks to http://www.andrewkilpatrick.org/blog/?page_id=445
and http://www.hoelscher-hi.de/hendrik/english/dimmer.htm
*/
/*
Modified by Mark Chester <mark@chesterfamily.org>
to use the AC line frequency (half-period) as a reference point
and fire the triacs based on that plus a dimmer delay value.
I removed the second timer-based interrupt and replaced it with a
means to reference the zero-crossing point as per interrupt 0.
*/
// General
unsigned long int ZeroXTime1 = 0; // Timestamp in micros() of the latest zero crossing interrupt
unsigned long int ZeroXTime2 = 0; // Timestamp in micros() of the previous zero crossing interrupt
unsigned long int NextTriacFire[4]; // Timestamp in micros() when it's OK to fire the triacs again.
unsigned long int DimStep; // How many micros() in each step of dimming
int Dimmer[4]; // The dimmer input variable. One for each channel
byte TriacPin[4] = {4,5,6,7}; // Which digital IO pins to use
boolean OKToFire[4]; // Bit to say it's OK for the triacs to fire
volatile boolean zero_cross = 0; // Boolean to store a "switch" to tell us if we have crossed zero
void setup() { // Begin setup
pinMode(TriacPin[0], OUTPUT); // Set the Triac pin as output
pinMode(TriacPin[1], OUTPUT); // Set the Triac pin as output
pinMode(TriacPin[2], OUTPUT); // Set the Triac pin as output
pinMode(TriacPin[3], OUTPUT); // Set the Triac pin as output
attachInterrupt(0, zero_cross_detect, FALLING); // Attach an Interupt to Pin 2 (interupt 0) for Zero Cross Detection
delay(50); // Give the interrupt time to capture a few AC cycles
} // End setup
void zero_cross_detect() { // function to be fired at the zero crossing
zero_cross = 1; // All we do is set a variable that's picked up later in the code
}
void loop() { // Main Loop
if ( zero_cross ) { // Did we detect a zero cross?
ZeroXTime2 = ZeroXTime1; // shift the current zero cross value to the previous
ZeroXTime1 = micros(); // set the new current zero cross time in micros()
DimStep = (ZeroXTime1 - ZeroXTime2)/1024; // Calc the duration of each dimming step
Dimmer[0] = analogRead(0); // Read in a dimmer value (change to suit needs)
Dimmer[1] = analogRead(1);
Dimmer[2] = analogRead(2);
Dimmer[3] = analogRead(3);
NextTriacFire[0] = ZeroXTime1 + (Dimmer[0] * DimStep); // Calc the next triac fire time
NextTriacFire[1] = ZeroXTime1 + (Dimmer[1] * DimStep);
NextTriacFire[2] = ZeroXTime1 + (Dimmer[2] * DimStep);
NextTriacFire[3] = ZeroXTime1 + (Dimmer[3] * DimStep);
OKToFire[0] = 1; // Tell us it's OK to fire the triacs
OKToFire[1] = 1;
OKToFire[2] = 1;
OKToFire[3] = 1;
zero_cross = 0; // Done. Don't try again until we cross zero again
}
if ( OKToFire[0] && micros() >= NextTriacFire[0] ) { // Are we OK and past the delay time?
digitalWrite(TriacPin[0], HIGH); // Fire the Triac mid-phase
OKToFire[0] = 0; // We fired - no longer OK to fire
digitalWrite(TriacPin[0], LOW); // Turn off the Triac gate (Triac will not turn off until next zero cross)
}
if ( OKToFire[1] && micros() >= NextTriacFire[1] ) { // Are we OK and past the delay time?
digitalWrite(TriacPin[1], HIGH); // Fire the Triac mid-phase
OKToFire[1] = 0; // We fired - no longer OK to fire
digitalWrite(TriacPin[1], LOW); // Turn off the Triac gate (Triac will not turn off until next zero cross)
}
if ( OKToFire[2] && micros() >= NextTriacFire[2] ) { // Are we OK and past the delay time?
digitalWrite(TriacPin[2], HIGH); // Fire the Triac mid-phase
OKToFire[2] = 0; // We fired - no longer OK to fire
digitalWrite(TriacPin[2], LOW); // Turn off the Triac gate (Triac will not turn off until next zero cross)
}
if ( OKToFire[3] && micros() >= NextTriacFire[3] ) { // Are we OK and past the delay time?
digitalWrite(TriacPin[3], HIGH); // Fire the Triac mid-phase
OKToFire[3] = 0; // We fired - no longer OK to fire
digitalWrite(TriacPin[3], LOW); // Turn off the Triac gate (Triac will not turn off until next zero cross)
}
}