Arduino class to drive an alternating current zero-cross sensing solid state relay using standard or dithered PWM



This library allows accurate control of one or more zero-cross solid state relays tied to the same phase AC power (50 or 60 Hz). It relies on detecting the zero-cross and will work poorly with non zero-cross relays.


A zero-cross sensing solid state relay is useful for driving resistive and/or capacitive loads [1] [2]. There are three main styles of controlling a circuit driven by an AC power source

  1. Random. The device is switched on and off independent of the phase of the driving current. This can lead to high in-rush currents and a noisy load.

  2. Chop/Dimmer. The device is powered on a fixed time after the hot line crosses the zero, and then turned off when zero is crossed again. This is how household dimmer switches behave. The chop leads to high inrush currents and a noisy load.

  3. Zero cross. The device is only switched on or off when the hot line voltage crosses the ground line (120 times per second on 60 Hz AC).

The device [3] is a typical low-cost ZC-SSR. Driven by a dimmer (pot), it behaves like a chop/dimmer driver, but only the zero-cross mode is controllable via a microcontroller.

For accurate power applications, accounting for zero-cross can be difficult because of the following issues

  • The number of cycles in AC (50/60 Hertz) is limited compared to DC PWM. For example a simple analogWrite to the control line for a ZC SSR would be useless.

  • In traditional PWM control (ignoring the zero cross), you are subject to a relatively large "aliasing" error. For example, cycling 10 times per second at 60 Hz can create an 8% error in power depending on where the zero cross falls.

  • To acheive finer control, a large interval is needed. For example, to achieve about 1% accuracy, you would need a pulse width time of about 1 second for enough pulses. For 0.1% accuracy you would need 8.3 second pulses.

This library solves these problems with a simple API to drive multiple ZC-SSR's in an application. Zero cross is detected, and a dithering pattern is used so that very fine average power can be delivered. The period of the PWM cycle can be short (even 1 pulse) and the dither (up to 16 bit) adds a flicker which provides an accurate average power level.


  1. Download [] and unzip into your Arduino libraries folder (so you have th file libraries/ACZCPWM/ in your Arduino directory). For example, on a Mac this creates $HOME/Documents/Arduino/libraries/ACZCPWM/ACZCPWM.h among other files.

  2. Download [] in the same way. ACZCPWM uses the TimerOne library.

  3. Restart your Arduino IDE; you should see ACZCPWM and TimeOne in the File->Examples menu.


  • NOTE You are relying on the internal protection diods of the ATMega chips to use this simple technique. Any board that does not have these diods (and even ones that have them) could be damaged by the +/- 170 volt signal.

  • A pin that supports interrupt-on-change (like pin 2 of the UNO) must be tied (via a 100kΩ resistor) to the hot/phase wire of the mains power to detect the zero cross.

  • A ground (GND) line must be tied (via a 100kΩ resistor) to the neutral write of the mains power for the phase fluxuation to reliably generate the interrupt above.


Near the top of you sketch you must include this and the TimerOne library (in the IDE you can use the sketch->include library) feature.

#include <ACZCPWM.h>
#include <TimerOne.h> // needed by ACZCPWM.h

Now create as many pwm controllers as you need by giving them useful names:

// must support interrupt-on-change
const int zeroCrossDetectPin = 2;

const int heater1Pin = 13; // any digital pin to control relay
const int heater1Period = 120; // default value (1 second in US)
const int heater1Dither = 0; // default value (no flicker)

const int heater2Pin = 12;
const int heater2Period = 1; // smallest value
const int heater2Dither = 16; // max value

ACZCPWM heater1;
ACZCPWM heater2;

In your setup(), set them up:

void setup() {
     Serial.print("heater1 resolution: ");

     Serial.print("heater2 resolution: ");

They are initially off. In your loop() you can then adjust the duty as needed:

void loop() {