AC Phase Control By Tweaking PWM Output to trigger Triac

Please take a look and help me improve this code.
The code uses PWM and attempts to match it to the AC frequency it is receiving I've reversed the pwm signal so it starts off then turns on after a duration to trigger the triac.

this is decently functioning code but I need to revise and improve it.
What I would like to do is enhance it to do things like Auto detect AC frequency and
fix the glitch when it gets close to the 100% point (1024).
Any advice is appreciated.

Advantages: Non Blocking low overhead control as the PWM handles everything.

/* AC Phase Dimmer Control using PWM
 *  By ZHomeSlice 
 *  I don't think I've seen this done before anywhere
*/

#define RESOLUTION 65536    // Timer1 is 16 bit
#define PHASEDURATION 16666
unsigned int pwmPeriod;
unsigned char clockSelectBits;


void ZeroCross(){
  shiftedStart(PHASEDURATION * 0.5);     //and start it up with timer at zero
}

void setPeriod(long microseconds){
  long cycles = (F_CPU / 2000000) * microseconds;                                // the counter runs backwards after TOP, interrupt is at BOTTOM so divide microseconds by 2
  if (cycles < RESOLUTION)              clockSelectBits = _BV(CS10);             // no prescale, full xtal
  else if ((cycles >>= 3) < RESOLUTION) clockSelectBits = _BV(CS11);             // prescale by /8
  else if ((cycles >>= 3) < RESOLUTION) clockSelectBits = _BV(CS11) | _BV(CS10); // prescale by /64
  else if ((cycles >>= 2) < RESOLUTION) clockSelectBits = _BV(CS12);             // prescale by /256
  else if ((cycles >>= 2) < RESOLUTION) clockSelectBits = _BV(CS12) | _BV(CS10); // prescale by /1024
  else         cycles = RESOLUTION - 1, clockSelectBits = _BV(CS12) | _BV(CS10); // request was out of bounds, set as maximum

  char oldSREG = SREG;
  cli();                            // Disable interrupts for 16 bit register access
  ICR1 = pwmPeriod = cycles;        // ICR1 is TOP in p & f correct pwm mode
  SREG = oldSREG;
  TCCR1B &= ~(_BV(CS10) | _BV(CS11) | _BV(CS12));
  TCCR1B |= clockSelectBits;       // reset clock select register, and starts the clock

}

void shiftedStart(long microseconds){
  long cycles = (F_CPU / 2000000) * microseconds;
  unsigned int tcnt1;
  TIMSK1 &= ~_BV(TOIE1);        
  GTCCR |= _BV(PSRSYNC);        // reset prescaler (NB: shared with all 16 bit timers);
  char oldSREG = SREG;          // save status register
  cli();                        // Disable interrupts
  TCNT1 = cycles;
  SREG = oldSREG;               // Restore status register
  TCCR1B |= clockSelectBits;
}

void setPwmDuty(int duty){
  unsigned long dutyCycle = pwmPeriod;
  dutyCycle *= duty;
  dutyCycle >>= 10;
  char oldSREG = SREG;
  cli();
  OCR1A = OCR1A = ICR1 - dutyCycle;      // invert 0-1024 range 
  SREG = oldSREG;
}

void disablePwm(char pin){
  TCCR1A &= ~_BV(COM1A1);                // clear the bit that enables pwm on PB1
}

void setup(){
  pinMode(2,INPUT_PULLUP);
  TCCR1A = 0;                            // clear control register A
  TCCR1A = _BV(COM1A0) | _BV(COM1A1)     // Clear OC1A on Compare Match, set
                                         // OC1A at BOTTOM (Inverting mode)
           | _BV(WGM11);                 // Fast PWM, top at ICR1
  TCCR1B = _BV(WGM12)  | _BV(WGM13);     //       ditto
  setPeriod(PHASEDURATION);
  TCCR1B &= ~(_BV(CS10) | _BV(CS11) | _BV(CS12));        // clears all clock selects bits (stop the counter)
  DDRB |= _BV(PORTB1);                                   // sets data direction register for pwm output pin
  TCCR1A |= _BV(COM1A1);                                 // activates the output pin
  setPwmDuty(0);
  attachInterrupt(0, ZeroCross, FALLING);
}

void loop(){
  static int Direction = 1;
  static int Duty = 0;
  static unsigned long ATimer; 
  if ((unsigned long)(millis() - ATimer) >= (10)){
    ATimer = millis();
    Duty += Direction;
    if (Duty >= 1024) Direction = -1; 
    if (Duty <= 1) Direction = 1;
    setPwmDuty(Duty);
  }
}


Thanks in advanced.
Z

Hello there,
While I was having a hard time controlling the phase shift between PWM and AC while working on the AC Triac trigger circuit with PWM control, I saw this code and saw it working with close to 100% performance.
I think it is much more performance than alternative solutions.
Thank you zhomeslice

pusent:
Hello there,
While I was having a hard time controlling the phase shift between PWM and AC while working on the AC Triac trigger circuit with PWM control, I saw this code and saw it working with close to 100% performance.
I think it is much more performance than alternative solutions.
Thank you zhomeslice

Your welcome,
Keep me updated on any breakthroughs or suggestions you find
Thank you,
Z

why are you using a PWM signal? how do you synchronize it to the zero crossing?

knowing when there is a zero crossing, why don't you simply generate a short (1 usec) pulse after an appropriate delay to trigger the Triac?

// trigger line power triac with 25% duty cycle

#define ZeroCrPin     A1
#define TriacPin      10

#if 1
# define HalfPeriodUsec  (1000000/120)

#else   // values to test at human speeds with LED
# define PulseWidthMsec     250
# define HalfPeriodUsec     1000000
#endif
#define TriacDutyCycle(x)   (HalfPeriodUsec * (100-x)/100)


byte          zc;
byte          zcLst;
unsigned long zcUsec    = 0;
unsigned long triacUsec;

char s [90];

// -----------------------------------------------------------------------------
void loop (void)
{
    unsigned long usec = micros();

    // capture zero crossing
    zc = digitalRead (ZeroCrPin);
    if (zcLst != zc)  {
        zcLst = zc;

        if (LOW == zc)  {
            zcUsec = usec;
        }
    }

    // wait for triac delay
    if (zcUsec && (usec - zcUsec) > triacUsec)  {
        zcUsec = 0;

        digitalWrite (TriacPin, HIGH);
#ifdef PulseWidthMsec           // no need to ~usec pulse
        delay (PulseWidthMsec);
#endif
        digitalWrite (TriacPin, LOW);
    }
}

// -----------------------------------------------------------------------------
void setup (void)
{
    Serial.begin (9600);

    digitalWrite (TriacPin, LOW);
    pinMode (TriacPin, OUTPUT);

    pinMode (ZeroCrPin, INPUT_PULLUP);
    zcLst = digitalRead (ZeroCrPin);

    triacUsec = TriacDutyCycle(25);

    sprintf (s, "%s: HalfPeriodUsec %ld, triacUsec %ld",
            __func__, HalfPeriodUsec, triacUsec);
    Serial.println (s);
}

gcjr:
why are you using a PWM signal? how do you synchronize it to the zero crossing?

knowing when there is a zero crossing, why don't you simply generate a short (1 usec) pulse after an appropriate delay to trigger the Triac?

I appreciate your approach and I truly understand the simplicity of it.
Unfortunately, there are some major drawbacks to it.

as for your questions they are absolutely valid and I hope to answer them.

why are you using a PWM signal?

because I can literally ignore it for the most part. I can potentially skip sensing zero crossings and still, the track will be triggered properly. PWM does not require lines of code to traverse rapidly to generate the signals Literally all that is needed is to mess with the counter to synchronize it. Pulse goes high and triac turns on turn be sure that the pulse is low before the zero-crossings and the triac will be ready for the next pulse. set the pulse cycle to match the AC frequency of the location and start the timer on just prior to zero crossing and all works like a champ. I don't even need to be cautious about delays in my code and other blocking code will not affect the resulting output.

If the only thing I needed to do was to control the triac I would consider just use your method. With that said my method is so lightweight accurate and fast, it would be hard for me to not implement my method in everything I use it for in the future.

how do you synchronize it to the zero crossing?

Well, that's the magic.

in the startup, I have the following line lining the interrupt to the function ZeroCrossing

attachInterrupt(0, ZeroCross, FALLING);



easy to read function which also allows me to pass a parameter to the shifted Start function

void ZeroCross(){
  shiftedStart(PHASEDURATION * 0.5);     //and start it up with timer at zero
}


now for the hard work
We calibrate PWM timer to the exact moment of zero crossing 


void shiftedStart(long microseconds){
  long cycles = (F_CPU / 2000000) * microseconds;
  unsigned int tcnt1;
  TIMSK1 &= ~_BV(TOIE1);        
  GTCCR |= _BV(PSRSYNC);        // reset prescaler (NB: shared with all 16 bit timers);
  char oldSREG = SREG;          // save status register
  cli();                        // Disable interrupts
  TCNT1 = cycles;
  SREG = oldSREG;               // Restore status register
  TCCR1B |= clockSelectBits;
}

I can see it you might say wow that's quite a bit to de every time we cross zero. but remember we only trigger this as an interrupt once at the exact time we cross zero. we don't have to continually poll an input.

So with that said you could do the same with your code and trigger zcUsec to match the time

a one-liner could be

#define ZeroCrPin   2// interrupt 0
volatile unsigned long zcUsec    = 0;  // must be set to volatile when used within an interrupt.
attachInterrupt(0, []{zcUsec = micros()}, FALLING); // used an anonomous function (lambda funciton) to set zcUsec to micros()

now in your code, you wouldn't have to continually check the pin for zero-crossing by pulling it. This would capture the time at that moment using an interrupt.

With that improvement, you could save time and find that your triggering of the triac to be more accurate.

The next and biggest difference is that I don't need to be concerned when the trigger event must occur. loop function has no responsibility ore requirement to be fast

example: I can set the cycle once and it will stay that way. no additional code is required in the loop function.

void setup(){

... 

// add this
   setPwmDuty(512);  // set the triac output to 50%  0 ~ 1024
}
void loop(){
// nothing here
}

I can be careless in my loop() with blocking code and never have to worry that the output will be affected.

Why I went with this method occurred as I was testing with an LCD display and Serial output, reading several sensors, and responding to requests. I noticed that the incandescent light I was using to visualize my output flickered when events occurred I concluded that I needed to be more precise with this output without having to rework all the other code. this not so simple solution was the result and I shared it with the community.

Z

P.S.
At this time I have not made any modifications to my original posted code that would alter this method of trial control. I am open to any feedback and even improvements you might find and would be willing to share.

I like your approach that keeps this as non-blocking and free running as possible. Haven't looked closely at your code yet, but I suspect there could be some improvement(s) to be had in the circuit.

I guess you're using a 5V MCU to drive the PWM dimmer circuit (because of the total VF of both LEDs)
What color is the visible LED?

What I would like to do is enhance it to do things like Auto detect AC frequency and
fix the glitch when it gets close to the 100% point (1024).

With regards to the glitch, I wonder if this is a hardware or software issue.
Do you have a scope trace or any more information?
What type of AC load are you using?
Perhaps the AC load needs a snubber circuit, as not having one on an inductive (or capacitive) load might me the cause, or contribute to the glitch.
Maybe a resistive load (for testing) would make a difference?

EDIT: Software

Re: datasheet 14.7.3 Fast PWM Mode

The extreme values for the OCR0A register represents special cases when generating a PWM waveform output in the fast PWM mode. If the OCR0A is set equal to BOTTOM, the output will be a narrow spike for each MAX+1 timer clock cycle. Setting the OCR0A equal to MAX will result in a constantly high or low output (depending on the polarity of the output set by the COM0A1:0 bits.

OCR1A = OCR1A = ICR1 - dutyCycle;      // invert 0-1024 range

If you want 10-bit range, wouldn't the max setting be 1023? (0-1023 = 1024 values)

EDIT: back to Hardware

There's room for improved accuracy of the zero-cross circuit. The H11L1 looks interesting.

Another EDIT:

...I noticed that the incandescent light I was using to visualize my output flickered when events occurred...

There's significant EMI when switching an incandescent load (filtering is needed). Really, a lot depends on the type of load being switched. If this is intended for use as an LED driver, the circuit would need to be more complex.