Smooth dimmer for an AC lightbulb, with PWM and serial communication

Hi everyone…

I have been making a project to have a dimmer system with an AC lightbulb, connected to Arduino, and based on this image:

The proposed circuit of the image is designed to work with a classic potentiometer and the variation of the relation of resistance and voltage, BUT in my case I modify the code to make it work with serial communication, to control it from PC.

Nothing so wrong, but my issue is that I want to make a smooth dimming of the light, actually the light just has 3 phases: one completely off, the second with a strong light, and the third with a light completely on. My goal is not to have 3 phases of light, but seeing the change of light smootly, from the red filament stage, to the completely on stage.

Any ideas or help is welcomed…

#include <ACPWM.h>
#define ZERO_PIN 2 //Input pin from zero cross detector
#define PWM_PIN 9 //Output pin to TRIAC / SSR

void setup(){
// pinMode(ledPin, OUTPUT);
//Initialize PWM operation.
//Mains frequency: 60Hz.
//Zero crossing point reached whenever pulse to PIN2 changes

void loop() {
//Set the duty cycle equal to the value of the serial port.
byte brightness;
if (Serial.available()) {
    // read the most recent byte (which will be from 0 to 255, in ASCII):
    brightness =;
    // set the brightness of the light
    //analogWrite(PWM_PIN, brightness);

Did you realize that every key has its own ASCII code, which you use to control the dimmer? Which keys did you send? For debugging you can add a Serial.println(brightness).

If you have only 3 steps, these may correspond to fully off, one half wave on, and both halves on. To sort out hardware problems, you can use a pot to control the dimmer. If that works, check your code.

The ASCII part of the code I do it from the program where I control the serial (LabVIEW), converting the decimal values from 0 - 255 to its ASCII part.

And about the other part, I tested it with a physical potentiometer, and the code does the same (only 3 stages for the light dimming, and little blinks of the light). Ok, maybe the code is wrong, but does it have something to be with the AC sync of the zero-crossing? Is the ACPWM library?

Right, the correct zero-crossing detection is crucial for driving a triac. Eventually it helps to reverse the interrupt edge (raising/falling)?

I have no experience with that library, but have you tried using the timer based Arduino phase control code that comes with that circuit you presented instead of ACPWM.h? Do you see more continuous variation? Brightness is controlled by the OCR1A value.

// AC Control V1.1
// This Arduino sketch is for use with the heater 
// control circuit board which includes a zero 
// crossing detect function and an opto-isolated TRIAC.
// AC Phase control is accomplished using the internal 
// hardware timer1 in the Arduino
// Timing Sequence
// * timer is set up but disabled
// * zero crossing detected on pin 2
// * timer starts counting from zero
// * comparator set to "delay to on" value
// * counter reaches comparator value
// * comparator ISR turns on TRIAC gate
// * counter set to overflow - pulse width
// * counter reaches overflow
// * overflow ISR turns off TRIAC gate
// * TRIAC stops conducting at next zero cross

// The hardware timer runs at 16MHz. Using a
// divide by 256 on the counter each count is 
// 16 microseconds.  1/2 wave of a 60Hz AC signal
// is about 520 counts (8,333 microseconds).

#include <avr/io.h>
#include <avr/interrupt.h>

#define DETECT 2  //zero cross detect
#define GATE 9    //TRIAC gate
#define PULSE 4   //trigger pulse width (counts)
int i=483;

void setup(){

  // set up pins
  pinMode(DETECT, INPUT);     //zero cross detect
  digitalWrite(DETECT, HIGH); //enable pull-up resistor
  pinMode(GATE, OUTPUT);      //TRIAC gate control

  // set up Timer1 
  //(see ATMEGA 328 data sheet pg 134 for more details)
  OCR1A = 100;      //initialize the comparator
  TIMSK1 = 0x03;    //enable comparator A and overflow interrupts
  TCCR1A = 0x00;    //timer control registers set for
  TCCR1B = 0x00;    //normal operation, timer disabled

  // set up zero crossing interrupt
  attachInterrupt(0,zeroCrossingInterrupt, RISING);    
    //IRQ0 is pin 2. Call zeroCrossingInterrupt 
    //on rising signal


//Interrupt Service Routines

void zeroCrossingInterrupt(){ //zero cross detect   
  TCCR1B=0x04; //start timer with divide by 256 input
  TCNT1 = 0;   //reset timer - count from zero

ISR(TIMER1_COMPA_vect){ //comparator match
  digitalWrite(GATE,HIGH);  //set TRIAC gate to high
  TCNT1 = 65536-PULSE;      //trigger pulse width

ISR(TIMER1_OVF_vect){ //timer1 overflow
  digitalWrite(GATE,LOW); //turn off TRIAC gate
  TCCR1B = 0x00;          //disable timer stopd unintended triggers

void loop(){ // sample code to exercise the circuit

OCR1A = i;     //set the compare register brightness desired.
if (i<65){i=483;}                      


Some nitpicking:

// The hardware timer runs at 16MHz. Using a
// divide by 256 on the counter each count is
// 16 microseconds. 1/2 wave of a 60Hz AC signal
// is about 520 counts (8,333 microseconds).

The latter should read 8.333ms for 60Hz, 10ms for 50Hz.
With an 8 bit counter (T2) the prescaler should divide at least by 512, or it will overflow too early.

Also note that the timer overflow would occur much too late, that's why the counter is set to 4 ticks (PULSE) before overflow, after turning the triac on.