Dimming multiple lights using zero cross signal

Ok, I’m more or less posting this inquiry to avoid experimenting more tomorrow and possibly blowing up some things.

I have built this:

And it works! One of my first major projects and it works, so I was excited of course.

My understanding of this whole circuit is the ZeroCross signal watches as the AC crosses the x-axis. Using this, it triggers the TRIAC. The TRIAC then switches on and off in a frequency that makes the light appear to be dimmed. Somehow I think I’m wrong and I’d like to know what each part does exactly. This would probably help me in understanding my code and how to link the three dimming circuits.

The ultimate idea is to have a single input for AC for the three lamps. My main question is about the ZeroCross signal. I’ve read before that you only need one, but can I control 3 lamps on a single ZeroCross line to the arduino, or do I need to just build the circuit 3 times over? If I can use only one ZeroCross signal for 3 different lights to dim at 3 different levels, then do I still need to duplicate the circuit side with 4N35 and BR or do I combine before the BR and 4N35, or do I still build 3 separate circuits and combine the ZeroCross at the end into the Arduino? Sorry if I seem like I’m taking the cheap way out, but I’m pretty much asking how to put this thing together safely.

If anyone knows a good tutorial for how to code this in applications, that would be awesome as well.


You only need one zero cross signal and that will be sufficient for all the circuits in your house.

As Chagrin says, you only need one zero crossing input to the Arduino. You just need to extend the code to handle multiple outputs. What code are you using?

I’m currently just using an example code I found:


AC Voltage dimmer with Zero cross detection
Author: Charith Fernanado http://www.inmojo.com
Adapted by DIY_bloke
License: Creative Commons Attribution Share-Alike 3.0 License.
Attach the Zero cross pin of the module to Arduino External Interrupt pin
Select the correct Interrupt # from the below table (the Pin numbers are digital pins, NOT physical pins: digital pin 2 [INT0]=physical pin 4 and digital pin 3 [INT1]= physical pin 5)

Pin | Interrrupt # | Arduino Platform

2 | 0 | All
3 | 1 | All
18 | 5 | Arduino Mega Only
19 | 4 | Arduino Mega Only
20 | 3 | Arduino Mega Only
21 | 2 | Arduino Mega Only

In the program pin 2 is chosen
int AC_LOAD = 3; // Output to Opto Triac pin
int dimming = 128; // Dimming level (0-128) 0 = ON, 128 = OFF

void setup()
pinMode(AC_LOAD, OUTPUT);// Set AC Load pin as output
attachInterrupt(0, zero_crosss_int, RISING); // Choose the zero cross interrupt # from the table above

// the interrupt function must take no parameters and return nothing
void zero_crosss_int() // function to be fired at the zero crossing to dim the light
// Firing angle calculation : 1 full 50Hz wave =1/50=20ms
// Every zerocrossing thus: (50Hz)-> 10ms (1/2 Cycle) For 60Hz => 8.33ms (10.000/120)
// 10ms=10000us
// (10000us - 10us) / 128 = 75 (Approx) For 60Hz =>65

int dimtime = (75*dimming); // For 60Hz =>65
delayMicroseconds(dimtime); // Off cycle
digitalWrite(AC_LOAD, HIGH); // triac firing
delayMicroseconds(10); // triac On propogation delay (for 60Hz use 8.33)
digitalWrite(AC_LOAD, LOW); // triac Off

void loop() {
for (int i=5; i <= 128; i++){

I popped that in before I left my lab and didn’t really get time to tinker with it too much. I was just happy to get the light working, though I had no idea how the code told the light what to do. Looking over it now, I still don’t quite understand how the whole thing works. It seems I don’t need a PWM pin as my AC signal? (Correct me if I’m wrong), but I can’t tell how to call the light to power at a certain intensity or pan from one intensity to the next, much less three. To me, it looks like the variable ‘dimming’ controls the light constantly, in which case I just need to assign 2 more that are linked to control pins for my other lights, but I’m not sure. I guess my biggest misunderstanding is seeing how I only need one ZeroCross to control, apparently, as many lights as I want, and how it’s implemented.

And free info, these lamps are halogen heat lamps for a Terrarium. Goal is to simulate sunrise and set as well as adjust during the day for temp, and even maybe a little display for a Thunderstorm.

Thanks again for the help!

  1. Your code tags need to be in square brackets for this forum, not angle brackets.

  2. Doing the timing by using delayMicroseconds calls in the ISR means that when the dimming level is very low or off, the Arduino will not have a chance to do anything else. Better to have the zero crossing interrupt reset a timer (or perhaps more than one timer, so you can use hardware timing instead of software timing) instead.

That makes more sense. Little more research turned up someone else who used a timer, and I'm now working with their code; trying to make it my own.

#define DETECT  0  // interrupt 0 = pin 2
#define GATE_1  8  // triac gate
#define GATE_2  9  // second triac gate

  This is used on 230 volt 50hz ac

// Values determines the level of dimming
#define MAX_LEVEL 128
#define MIN_LEVEL 5

#define FADE_DELAY 20

volatile uint16_t dim_level, light_1 = MAX_LEVEL , light_2 = MAX_LEVEL;

boolean start_low_fix      = true;          // temp fix, because arduino power-on before main voltage is applied.

void setup() 
  attachInterrupt(DETECT, zero_cross_detect, RISING);   // Attach an Interupt to Pin 2 (interupt 0) for Zero Cross Detection
  //moc3022 setup:
  pinMode(GATE_1, OUTPUT);   // triac gate control
  pinMode(GATE_2, OUTPUT);   // triac gate control
  digitalWrite(GATE_1, LOW); // turn off triac gate
  digitalWrite(GATE_2, LOW); // turn off triac gate
 /* Timer Stuff */
  TCCR1A = 0x00;  // clear
  TCCR1B = 0x00;  // clear

  TIMSK1 = 0x02;  // Enable the Output Compare A
  OCR1A  = 1250;  // = 128 steps on 50hz with no timer prescale

  if (light_1 == dim_level || light_1 == dim_level+1) {        // fire first triac
      digitalWrite(GATE_1, HIGH);
  if (light_2 == dim_level || light_2 == dim_level+1) {        // fire second triac
      digitalWrite(GATE_2, HIGH);
   if (dim_level == MAX_LEVEL + 1) { // Shut off gate before ZC
      digitalWrite(GATE_1, LOW);     // set triac gate to high 
      digitalWrite(GATE_2, LOW);     // turn off second triac gate
      TCCR1B = 0;

void zero_cross_detect() {
   TCNT1  = 0;          // reset timer - count from zero
   TCCR1B = 0x09;       // start timer with no prescale and reset tcnt1 on compare
   start_low_fix = false;
   dim_level = 0;

void loop()
    if (start_low_fix) // waiting for first ZC
      // Fade triac 1:
      /*if(fade_down_1) { // Fade-down
        if (light_1 == MAX_LEVEL)
            fade_down_1 = false;
      else { // Fade-up
        if (light_1 == MIN_LEVEL)
            fade_down_1 = true;
      // Fade triac 2:
      if(fade_down_2) { // Fade-down
        if (light_2 == MAX_LEVEL)
            fade_down_2 = false;
      else { // Fade-up
        if (light_2 == MIN_LEVEL)
            fade_down_2 = true;

This is set for 230v on 50Hz, but I need to change it to accommodate 120v on 60Hz. The program as it currently stands works, but I get a lot of flicker (It's either got something to do with cheap cable, or what I think is the OCR1A. It's set for 230v @ 50Hz. What's the math behind that so I can change it to my needs? I'm curious if that's offsetting something.


Try changing the top OCR1A value from 1250 to 1042. That should change it from 50Hz to 60 Hz.

I don't think the software cares about 230V vs 120V.