Dimming effect by zero crossing detector and a triac

I am trying to have a soft and gradual on/off for my incandescent bulb, say when you switch it on it should go from off mode to 100% brightness in 10 mins and vice versa. the following link contains an interesting code model for that but it's not that clear to me so I hope someone can help me in understating it clearly.
Here is my half-understandings and questions :

FADE_MAX gives the lamp it's dankest brightness and FADE_MIN is its full light? by setting period with them? and fadeAmount is the the step by which the brightness changes?

what is the role of TRIAC_PULSE_MICROS ?

at the void zeroCrossing() first it set the triacOn flag false because we need to turn the triac on after some time, since we don't want to deliver it the full power to have dimming effect? and till I don't know how to bring that 10 mins duration in the game.

Edit 1 :
Adding the sketch and schismatic

#include <TimerOne.h>

const byte INTERRUPT_PIN = 2;
const byte TRIAC_PIN = 4;
const byte TRIAC_PULSE_MICROS = 30;

const int FADE_MAX = 9800;
const int FADE_MIN = 2000;
const int looptime = 100;
int fadeAmount = 10;

volatile bool triacOn;
volatile int period = FADE_MIN; // microseconds cut out from AC pulse

void zeroCrossing() {
  triacOn = false; // triac tuns off self at zero crossing
  Timer1.setPeriod(period); // to call triacPulse() after off period

void triacPulse() {
  if (triacOn) { // stop pulse
    digitalWrite(TRIAC_PIN, LOW);
  } else { // start pulse
    digitalWrite(TRIAC_PIN, HIGH);
    triacOn = true;

void setup() {
  attachInterrupt(digitalPinToInterrupt(INTERRUPT_PIN), zeroCrossing, RISING);

void loop() {
  period = period + fadeAmount;
  if (period <= FADE_MIN || period >= FADE_MAX) {
    fadeAmount = -fadeAmount;

and the input is 220v , 50Hz

a triac switches off at zero crossing (it is a property of a triac and has nothing to do with the zc detector).
the max and min values are there because if the switching of the triac goes very close to the self off of the triac, it can conflict with it and result is flickering of the bulb.

the triac tuns-on on a signal and turns off only at next zero crossing. so it only requires a short pulse to turn on. the length of the pulse is here set with TRIAC_PULSE_MICROS

the other sketch in my SE answer uses a different approach. it starts a PWM signal and synchronizes it with AC by resetting the timer counter in zero crossing interrupt. This is what I use in my heater control project because it is more autonomous so it requires less CPU time.

The 10 min fading would use the same approach as fading the build-in LED.



You're right about FADE_MAX and FADE_MIN.
For an incandascent light keep in mind that they are highly unlinear (they need quite a bit of power until they even start to emit visible light). So you might need to set FADE_MAX quite low to get an effect.

A triac only needs a short pulse to be anabled and it will stay enabled until the next zero crossing. Timer1 interrupt calls the the triacPulse() function. So on detection of a zero crossing we're arming Timer1 to fire an interrupt after period microseconds and make sure that the flag TriacOn is false.
When Timer1 fires an interrupt after period microseconds it calls triacPulse(). If TriacOn is false the else{} is executed. This Enables the output for the Triac, sets TriacOn to true and arms the Timer1 to fire again after TRIAC_PULSE_MICROS (30us in this case).
So after 30us the Timer1 calls triacPulse() again. But this time TriacOn is true, so it will execute the first part of the if{} which disables the output to the triac and stops the Timer1.

Keep in mind that FADE_MAX + TRIAC_PULSE_MICROS must not be >= 10ms (in 50Hz Grids) or 6.6ms (in 60Hz networks).
You might also consider, that in Europe the Grid Frequency is allowed to shift quite a bit over the course of a day. So give it some slack there... If you want do do it cleanly, measure the time between two zero crossings and calculate your desired values from there.


Thanks for the detailed explanations. (I hope) I have better understanding of it now, but again some questions are coming!

void loop() {
  period = period + fadeAmount;
  if ( period >= FADE_MAX) {
      digitalWrite(TRIAC_PIN, LOW);

In the code above my goal is to achieve a gradual off effect(not gradual on effect), when fading period reaches its maximum the Triac should go off, but it doesn't work as I expect. shouldn't it go high instantly and then gradually goes off? that's what I want but it take some times to go high.

BTW what should I consider for selecting the resistors and fuses? my loads are just ac bulb/s. thanks

Yes... this won't work... you don't have to care about interrupts and the thyristor in your loop. This all happens in the functions.

void loop() {
  period = period + fadeAmount;
  if (period <= FADE_MIN || period >= FADE_MAX) {
    fadeAmount = -fadeAmount;

This is the code from the example. Except I've changed the time in the delay by the variable "looptime".
I consider the time the loop takes to execute as negligible in this case!

fadetime = (FADE_MAX - FADE_MIN)/fade_Amount*looptime

Why is this?
(FADE_MAX - FADE_MIN) gives the range in which the value can change
/fade_Amount gives the number of steps it will take to fade trough the whole range
*looptime time it takes for a loop, so multiply by that.

Use the example code and only change the pin numbers if necessary and make the values fir for your usecase.

FADE_MAX is the darkest part. It must be lower than 9970us (so the default 9800 seems reasonable but you might need to change that to a much lower value to have the bulb glow at least a little bit).

So, I'd suggest:
FADE_MAX = 7000
FADE_MIN = 1000
fadeAmount = 10

With this Values you get a difference og 6000 trough which you step with a step with of 10. This makes 600 steps per direction. Now if you want it to take 10 minutes (600s) you just set the looptime to 1s (1000ms) and you should be good to go.

1 Like

Thank you for posting a neat, easy to read schematic.

On the right next to the load you have the AC supply grounded. You must not connect any part of the circuit that is connected to the supply to ground. Doing so is dangerous as it means the whole circuit is connected to the mains supply and possibly live. The whole point of the opto-isolators
is to separate the mains from the low voltage part of the circuit.


Easiest to buy something like this: https://robotdyn.com/ac-light-dimmer-module-1-channel-3-3v-5v-logic-ac-50-60hz-220v-110v.html. You can't beat the price.


It's confusing for me: the period initialized with FADE_MIN , meaning the light should start with the brightest mode, but in the simulation it takes time to maximum brightness.

thanks the formula was helpful.


Right, but the simulation doesn't work that way. Did I miss anything?
talking of safety , do you have any idea on the use of fuses for this circuit?

Thanks, but it isn't available here :sweat_smile:

FADE_MIN is maximum brightness.
What you set with period is the time from zero crossing until the triac is switched on. So the longer the period the longer the triac is off (and therefore the shorter it is on). FADE_MIN is minimum fading (=maximum brightness).
So the example starts with a low value for period which means high brightness. Than every cycle of the loop period is increased (which means it's getting darker) by fadeAmount undil it's >= FADE_MAX. If that happens, fadeAmount is negated (meaning it has its sigh flipped from + to - or the other way around) and so it fades back to the FADE_MIN and fadeAmount is flipped again...

So if you want to start a the darkest setting, initialize period to FADE_MAX. You might also want to set fadeAmount to a negative number but that's just to make it nice :slight_smile:

So with the code with the recommended settings you should get a fade out and fade in effect taking 10 minutes

1 Like

Simulation? What simulation? What do you mean by 'it doesn't work'? What doesn't work? What does it do and how is that different from what it should do?

Measure / work out the maximum current the circuit uses and choose the fuse value closest to and slightly more than that. Fuses should be close to the source of power, which in the UK means in the mains plug. Other countries will differ. Don't make the common mistake of assuming the fuse protects the circuit, it doesn't. It protects the wiring and the supply from overload.

1 Like

I made a "similar" project as a "sunrise dimmer" a long-long time ago with a different microcontroller... I'm still using it...

I started by experimenting with some simple trial-and-error delays. I started somewhere in the "middle" (around 90 degrees). Then I went longer-and-longer to see how dim I could go before it went-into the next half-cycle and just stayed-on at full brightness. I don't remember if there were any issues with too-short of a delay.

I used the "normal" TRIAC-driver opto-isolator but for the zero-crossing isolation I tapped-into the transformer secondary of the power supply that I built-into the gizmo. I don't remember the details of what I did, but I might be looking for some voltage after the zero-crossing and then the "action" happens in the next half-cycle. (That does require trial-and-error.) And if I remember correctly, I just looked for the positive-going zero-crossing (or near-zero crossing)... When you find one zero-crossing you know when the next one is coming, 1/2-cycle later.

In my application it's not a big deal and I simply use "linear" timing... I'm also using a 10-minute fade-up so it's barely noticeable that it's fading-up anyway. Dimmable LED light bulbs have a different brightness curve and I haven't tried an LED yet... And of course the AC sine wave isn't a straight-line either.

1 Like

Sorry for the bad quality!

But it doesn't start with high brightness, it is initially off and then after some times gets its max brightness.

Yes, I am using Proteus 8 . When that GND is there the lamp will work like the above GIF , but if I delete that GND the lamp will be in the off mode for ever.

OK, thanks. I know nothing of Proteus so can't help.

Just make sure you don't connect the mains side to ground if you actually build it.

1 Like

I cleared the wrong statement here.

Why half of the wave is missing?

that is how it works. it switches part of the current wave off. like a fast and good timed on-off switch. but it should look the same in the negative part.

1 Like

Totally, I have difficulty seeing the relevant part of the schematic, the GIF just keeps switching (don't use GIF's please) But i think the rectifier bridge before the opto-coupler is missing. Like this you only detect the zero-crossing in one direction.

1 Like

there some negative DC current

Yes, and I meant the negative side of the input AC by "half of the wave". It sometimes ignores the negative side completely and sometimes cuts it just like the positive side.


The schematic has been provided here , I hoped the GIF help me clarify the behavior.

Doesn't the H11AA1 do the job?

So here is the problem:

The light doesn't start with full brightness and (maybe or?) the waveform problem.

How many zero-crossing detection pulses do you get per AC cycle?

1 Like