Phase cutting control based on peak detection

I'm trying to implement AC phase cutting control, but the problem is that instead of the common zero crossing, my circuit is based on peak detection (reason: instead of a big resistor that runs hot, I use a capacitor to reduce current).

This peak detection works fine - I see peaks of 1.8 ms wide and a much sharper falling edge than rising edge, so using the falling edge as timing point. This means that the zero crossing is 4.1 ms past the peak (50 Hz mains AC).

The problem is in the timing of the gate trigger: this is 4.1 ms plus 0-10 ms (half wave). This means that when I'm below about 45% the gate trigger should happen past the next peak, and this is where the common zero crossing code fails.

One solution could be to have the peak detection set a timer interrupt that acts as zero crossing point, and subsequently a new timer is started for the actual phase cutting. But that means I would have to use both timer1 and timer2.

Is there any way to make this happen with just one timer?

Current test code (under development so maybe not all comments make sense). I didn't implement the timer2 part, that should be no problem to do. Mostly wondering if there's a way of doing this with a single timer.

// Pulse length: 1,800 us (according to scope).
// Rising edge is much slower than falling edge (10k too weak a pull-up??) so better use falling edge. Pulse length
// measured by Arduino is about 1480 us.
// Interrupt: should look for FALLING edge, which occurs 4,100 us before the zero crossing.
// Cut-off points for the phase cutting: 4,100 - 14,100 us in timer counts, one count = 4 us.
const uint16_t startACCycle = 1025;
const uint16_t endACCycle = 3525;

const byte ZERO_CROSSING_PIN = 6;
const byte GATE_PIN = 5;

volatile uint8_t pulses;

// Auto/manual control states.
enum controlModes {
  CONTROL_AUTO,
  CONTROL_MANUAL
};
uint8_t controlMode = CONTROL_MANUAL;
uint8_t manualPowerLevel = 0;

void setup() {
  Serial.begin(115200);
  pinMode(ZERO_CROSSING_PIN, INPUT_PULLUP);
  pinMode(GATE_PIN, OUTPUT);

  // set up Timer1
  TIMSK1 = 0x03;                                    // Enable comparator A and overflow interrupts
  TCCR1A = 0x00;                                    // Timer control registers set for
  TCCR1B = 0x00;                                    // normal operation, timer disabled

  *digitalPinToPCMSK(ZERO_CROSSING_PIN) |= bit (digitalPinToPCMSKbit(ZERO_CROSSING_PIN));  // enable pin
  PCIFR  |= bit(digitalPinToPCICRbit(ZERO_CROSSING_PIN)); // clear any outstanding interrupt
  PCICR  |= bit(digitalPinToPCICRbit(ZERO_CROSSING_PIN)); // enable interrupt for the group
}

uint32_t lastChange;
bool rising = true;
void loop() {
  if (millis() - lastChange > 250) {
    lastChange = millis();
    if (rising) {
      manualPowerLevel++;
    }
    else {
      manualPowerLevel--;
    }
    if (manualPowerLevel == 0) {
      rising = true;
    }
    else if (manualPowerLevel == 100) {
      rising = false;
    }
    if (manualPowerLevel % 10 == 0) {
      Serial.print(F("Current level: "));
      Serial.print(manualPowerLevel);
      Serial.print(F(", timer: "));
      Serial.print(startACCycle + (uint32_t)(100 - manualPowerLevel) * (endACCycle - startACCycle) / 100.0);
      Serial.print(F(", OCR1A: "));
      Serial.println(OCR1A);
      pulses = 0;
    }
  }
  handlePump();
}

///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Pump handling.
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void handlePump() {
  OCR1A = startACCycle + (uint32_t)manualPowerLevel * (endACCycle - startACCycle) / 100.0;
}

#define PULSE 63                                    // Trigger pulse width (counts) - 252 us.

// Interrupt Service Routines
ISR (PCINT2_vect) {                                 // Zero crossing is D6, PCINT2.
  if (digitalRead(ZERO_CROSSING_PIN) == LOW) {      // Falling edge.
    TCCR1B = 0x03;                                  // Start timer with divide by 64 input = 4 us per tick at 16 MHz.
    TCNT1 = 0;                                      // Reset timer - count from zero
  }
}

ISR(TIMER1_COMPA_vect) {                            // Comparator match.
  digitalWrite(GATE_PIN, HIGH);                     // Set TRIAC gate to high.
  TCNT1 = 65536 - PULSE;                            // Trigger pulse width.
}

ISR(TIMER1_OVF_vect) {                              // Timer1 overflow.
  digitalWrite(GATE_PIN, LOW);                      // Turn off TRIAC gate.
  TCCR1B = 0x00;                                    // Disable timer to stop unintended triggers.
}

Circuit used:

This means that the zero crossing is 4.1 ms past the peak (50 Hz mains AC).

You’re ignoring the phase shift created by the capacitor.

How so? That capacitor should introduce a 90° phase shift - the current through the optcoupler drops to 0 as the peak AC voltage (and current) is reached, and is highest at the zero crossing. So with 50 Hz / 20 ms period you're 5 ms off zero crossing at the peak. The actual pulse that I see coming out of the optocoupler of course is not infinitely short, it's about 1.8 ms, and I suppose halfway that pulse is the actual peak.

I don't know how to safely connect my scope to both sides at the same time, so I could see both the 50Hz mains signal and the peak crossing pulse at the same time to verify the phase shift.

Agreed, it should be 90 degrees in theory but it doesn’t appear to work so... just what is the shift?

Yes, you’d need an isolation transformer to safely connect but since I don’t like scoping mains even with one, I would wire up a temporary pure resistive zero crossing opto and compare the outputs.

PS your schematic shows phase and netural swapped between input and output.

avr_fred:
Agreed, it should be 90 degrees in theory but it doesn’t appear to work so... just what is the shift?

Yes, you’d need an isolation transformer to safely connect but since I don’t like scoping mains even with one, I would wire up a temporary pure resistive zero crossing opto and compare the outputs.

PS your schematic shows phase and netural swapped between input and output.

If my memory serves me, the 90 degree phase shift will only happen when the capacitor resonates the circuit with the inductance.

Paul

Zero crossing is used because zero is an absolute point of reference. The capacitor gives a delay but that delay will change with temperature (most capacitors lose significant capacitance as the temp drops) and as the capacitor ages over time.

The AC frequency doesn't change. If you had an accurate clock you could detect just one zero crossing and keep timing events for a week.* You should detect the zero crossing and run your timer to start and stop at whatever point you want, say from 95% of the cycle to 99%.

It should not take 2 timers to do this. Look into the various timer-compare modes available to have one timer do both start and stop.

*An exaggeration. It would not work for more than a few seconds but that is like a week for a 16MHz Arduino.

avr_fred:
Agreed, it should be 90 degrees in theory but it doesn’t appear to work so... just what is the shift?

The circuit works, that's not the problem. Actual phase shift should be about 89 deg due to the 2k2 resistor.

The problem is the code: the timer is started when the peak detection pulse is received, so I add 1/2 phase minus a bit (pulse length) to it for actual start, but as it's based on zero crossing this goes wrong when the TRIAC pulse is in the second half of the phase, i.e. past the next peak detection pulse.

One solution would be to set timer2 when the peak detection pulse is received, it triggering a timer interrupt the moment there's a zero crossing, and then back to the existing timer1 code. I just hope to not need a second timer.

PS your schematic shows phase and netural swapped between input and output.

I know. That's anyway irrelevant here, just doesn't look nice. Forgot to correct that. Maybe best to just remove those markings. It happened during to PCB design, the connectors are back to back so mirrored.

void handlePump() {
  OCR1A = startACCycle + (uint32_t)manualPowerLevel * (endACCycle - startACCycle) / 100.0;
  if(OCR1A > 2500) 
     {OCR1A -= 2500;}
}

I think you can modify the algorithm such that if the calculated OCR1A is greater than 2500 (that is greater than 10000 us and occurring after the next peak detect interrupt) subtract 2500 from the value and fire the trigger on the ac slope right after the interrupt. The next true ac zero crossing will turn the triac off and the triac on time will be short. Otherwise, do as you currently do and fire the trigger after the next ac zero crossing to let the next + 1 turn it off to achieve longer duty cycles.

I'm not sure how the triac behaves if the trigger is high but the triac may not be locked on, when the zero crossing occurs.

I'm not sure it makes a difference if the TCNT1 adjustment and OVF interrupt is used to turn the trigger pin off, the timer off, and reset TCNT1 or if its better to follow Mark's suggestion and use OCR1A to turn the trigger pulse on, and a COMPB interrupt on OCR1B when OCR1B = OCR1A + 63 to turn the trigger pin off, the timer off, and reset TCNT1.

The trigger time of 252 us seems quite long. Most of the example trigger pulse lengths are closer to 50 us.

Thanks, going to think about how to implement this.

The gate trigger time I got from the [url=https://playground.arduino.cc/Main/ACPhaseControlAC Phase Control tutorial[/url]. Indeed the spec sheet gives a time of 2 us for the trigger so that can be shortened to about 50 us.

cattledog:

void handlePump() {

OCR1A = startACCycle + (uint32_t)manualPowerLevel * (endACCycle - startACCycle) / 100.0;
  if(OCR1A > 2500)
    {OCR1A -= 2500;}
}

Took me a while (just can't wake up today) but I get what you mean. You basically move forward the second half.

I'm afraid that will give erratic behaviour when the required duty cycle means firing very close to the peak detection point. In normal zero crossing that's no problem (no-one will ever notice that you never reach 100% but just 98% or so of the full wave, and you could anyway still get 100% by simply keeping the gate high), for me that switch-over point is somewhere in the middle. That's going to give a small zone where it just won't fire reliably.

interrupt. The next true ac zero crossing will turn the triac off and the triac on time will be short. Otherwise, do as you currently do and fire the trigger after the next ac zero crossing to let the next + 1 turn it off to achieve longer duty cycles.

This may be a better approach. It's a bit more programming complexity as you have to keep track of cycles, and when the previous peak was detected. As my brain doesn't work today that's not for now to figure out, really :slight_smile:

Almost there!
This code works with only timer1, and looks good on the scope. Haven't been able to test with a motor as it makes way too much noise for the dead of night :slight_smile:

One issue: if pulseDelay is less than or equal to PULSE, the TRIAC gate remains on for long times; as if it's on for one full cycle and then off for another cycle. Can't see that clearly on the scope, but there is definitely this glitch and it goes away the moment pulseDelay is greater than PULSE by 1 or more. I just don't see how that happens. The workaround is simple (make sure pulseDelay > PULSE) but it shouldn't happen in the first place.

I'm not resetting the timer, instead making use of the two comparators and unsigned integer overflows to simply set the next interrupt to "now + delay time". Comparator A for the zero crossing, comparator B for setting the gate on and off.

// Pulse length: 1,800 us (according to scope).
// Rising edge is much slower than falling edge so better use falling edge. Pulse length
// measured by Arduino is about 1480 us.
// Interrupt: should look for FALLING edge, which occurs 4,100 us before the zero crossing.
// Cut-off points for the phase cutting: 4,100 - 14,100 us in timer counts, one count = 4 us.

const byte SYNC_PIN = 6;                            // The synchronisation signal: peak detection.
const byte GATE_PIN = 5;                            // The TRIAC gate.
const uint16_t ZC_DELAY = 1025;                     // 4 us per clock tick; 4100 us delay between sync signal and zero crossing.
const byte PULSE = 12;                              // Trigger pulse width (counts) - 48 us.
volatile bool peakDetected = false;
volatile bool gateOn = false;
uint32_t lastChange;
bool rising = true;
uint8_t manualPowerLevel;
uint16_t pulseDelay;

void setup() {
  Serial.begin(115200);
  pinMode(SYNC_PIN, INPUT_PULLUP);
  pinMode(GATE_PIN, OUTPUT);

  // set up Timer1
  TIMSK1 = 0x06;                                    // Enable comparator A and overflow interrupts.
  TCCR1A = 0x00;                                    // Timer control registers set for
  TCCR1B = 0x00;                                    // normal operation, timer disabled.

  *digitalPinToPCMSK(SYNC_PIN) |= bit(digitalPinToPCMSKbit(SYNC_PIN)); // enable pin change interrupt
  PCIFR |= bit(digitalPinToPCICRbit(SYNC_PIN));     // clear any outstanding interrupt
  PCICR |= bit(digitalPinToPCICRbit(SYNC_PIN));     // enable interrupt for the group
}


void loop() {
  handlePump();
  if (millis() - lastChange > 250) {
    lastChange = millis();
    if (rising) {
      manualPowerLevel++;
    }
    else {
      manualPowerLevel--;
    }
    if (manualPowerLevel == 0) {
      rising = true;
    }
    else if (manualPowerLevel == 100) {
      rising = false;
    }
    if (manualPowerLevel % 10 == 0) {
      Serial.print(F("Current level: "));
      Serial.print(manualPowerLevel);
      Serial.print(F(", timer: "));
      Serial.print((uint16_t)(100 - manualPowerLevel) * 25);
      Serial.print(F(", OCR1A: "));
      Serial.println(OCR1A);
    }
  }
}

///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Pump handling.
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void handlePump() {
  pulseDelay = (uint16_t)(100 - manualPowerLevel) * 25;
  if (pulseDelay < 5) {
    pulseDelay = 5;
  }
}


// Interrupt Service Routines
ISR (PCINT2_vect) {                                 // Zero crossing is D6, PCINT2.
  if (digitalRead(SYNC_PIN) == LOW) {               // Falling edge.
    OCR1A = TCNT1 + ZC_DELAY;                       // Set the comparator A to ZC_DELAY from the current reading of the counter.
    TCCR1B = 0x03;                                  // Start timer with divide by 64 input = 4 us per tick at 16 MHz.
    peakDetected = true;                            // We just detected a peak.
  }
}

ISR(TIMER1_COMPA_vect) {                            // Comparator A match: zero crossing is now.
  OCR1B = TCNT1 + pulseDelay;                       // Set the comparator B to start the TRIAC pulse at pulseDelay from now.
  peakDetected = false;                             // No peak yet within this switching cycle.
}

ISR(TIMER1_COMPB_vect) {                            // Comparator B match: time to switch the TRIAC (on or off).
  if (gateOn == false) {                            // Gate is off, so we have to switch on the TRIAC now.
    digitalWrite(GATE_PIN, HIGH);                   // Set TRIAC gate to high.
    gateOn = true;                                  // Record TRIAC state.
    OCR1B += PULSE;                                 // When PULSE ticks passed, we have to come back and switch off the gate again.
  }
  else {
    digitalWrite(GATE_PIN, LOW);                    // Set TRIAC gate to low.
    gateOn = false;                                 // Record TRIAC state.
    if (peakDetected ==  false) {                   // No peak detected yet, so we switched before the next peak.
      TCCR1B = 0x00;                                // Disable timer to stop unintended triggers.
    }
  }
}

It sounds like you are on a good path now.

Got it.
Mixing up directions again... it's when the pulseDelay reaches it's maximum that the glitches happen - and that makes sense: if you start the pulse too late, it extends past the next zero crossing. That's bad of course. So the pulse should start early enough, to allow for it to finish.
On top of that there's an error in the peak detection, that is not perfectly stable. There appears to be a variation of 100-150µs in measured cycle length, so we have to allow for 200µs plus gate pulse length for it to fire reliably. This effectively means that the minimum on is 2.5% of the cycle in time (much less in power of course).
Looking good, tomorrow connect my angle grinder to test it better.

So posting this here, hoping it's useful. This works with the above circuit. I have a few completed boards on hand that I'm happy to sell, PM if interested. Not planning actual production at this stage, built them as I couldn't find any AC phase control board that could 1) handle 4A, 2) had a snubber circuit to switch inductive loads such as motors rather than resistive ones such as lamps, and 3) had the zero detection on board (which I turned in to peak detection as I was not interested in bulky, hot resistors).

Over the next few days I'll probably turn it into a proper library and post it with schematics and so on Github.

// Pulse length: 1,800 us (according to scope).
// Rising edge is much slower than falling edge (10k too weak a pull-up??) so better use falling edge. Pulse length
// measured by Arduino is about 1480 us.
// Interrupt: should look for FALLING edge, which occurs 4,100 us before the zero crossing.
// Cut-off points for the phase cutting: 4,100 - 14,100 us in timer counts, one count = 4 us.

const byte SYNC_PIN = 6;                            // The synchronisation signal: peak detection.
const byte GATE_PIN = 5;                            // The TRIAC gate.
const uint16_t ZC_DELAY = 1025;                     // 4 us per clock tick; 4100 us delay between sync signal and zero crossing.
const byte PULSE = 12;                              // Trigger pulse width (counts) - 48 us.
const uint16_t MAX_PULSE_DELAY = 2480 - PULSE;      // 200 us before the next expected ZC the TRIAC pulse must be finished.
volatile bool peakDetected = false;
volatile bool gateOn = false;
uint32_t lastChange;
bool rising = true;
uint8_t manualPowerLevel;
uint16_t pulseDelay;

void setup() {
  Serial.begin(115200);
  pinMode(SYNC_PIN, INPUT_PULLUP);
  pinMode(GATE_PIN, OUTPUT);

  // set up Timer1
  TIMSK1 = 0x06;                                    // Enable comparator A and overflow interrupts.
  TCCR1A = 0x00;                                    // Timer control registers set for
  TCCR1B = 0x00;                                    // normal operation, timer disabled.

  *digitalPinToPCMSK(SYNC_PIN) |= bit(digitalPinToPCMSKbit(SYNC_PIN)); // enable pin change interrupt
  PCIFR |= bit(digitalPinToPCICRbit(SYNC_PIN));     // clear any outstanding interrupt
  PCICR |= bit(digitalPinToPCICRbit(SYNC_PIN));     // enable interrupt for the group
}


void loop() {
  handlePump();
  if (millis() - lastChange > 250) {
    lastChange = millis();
    if (rising) {
      manualPowerLevel++;
    }
    else {
      manualPowerLevel--;
    }
    if (manualPowerLevel == 0) {
      rising = true;
    }
    else if (manualPowerLevel == 10) {
      rising = false;
    }
    if (manualPowerLevel % 10 == 0) {
      Serial.print(F("Current level: "));
      Serial.print(manualPowerLevel);
      Serial.print(F(", timer: "));
      Serial.print((uint16_t)(100 - manualPowerLevel) * 25);
      Serial.print(F(", OCR1A: "));
      Serial.println(OCR1A);
    }
  }
}

///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Pump handling.
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
void handlePump() {
  pulseDelay = (uint16_t)(100 - manualPowerLevel) * 25;
  if (pulseDelay > MAX_PULSE_DELAY) {
    pulseDelay = MAX_PULSE_DELAY;
  }
}


// Interrupt Service Routines
ISR (PCINT2_vect) {                                 // Zero crossing is D6, PCINT2.
  if (digitalRead(SYNC_PIN) == LOW) {               // Falling edge.
    OCR1A = TCNT1 + ZC_DELAY;                       // Set the comparator A to ZC_DELAY from the current reading of the counter.
    TCCR1B = 0x03;                                  // Start timer with divide by 64 input = 4 us per tick at 16 MHz.
    peakDetected = true;                            // We just detected a peak.
  }
}

ISR(TIMER1_COMPA_vect) {                            // Comparator A match: zero crossing is now.
  OCR1B = TCNT1 + pulseDelay;                       // Set the comparator B to start the TRIAC pulse at pulseDelay from now.
  peakDetected = false;                             // No peak yet within this switching cycle.
}

ISR(TIMER1_COMPB_vect) {                            // Comparator B match: time to switch the TRIAC (on or off).
  if (gateOn == false) {                            // Gate is off, so we have to switch on the TRIAC now.
    digitalWrite(GATE_PIN, HIGH);                   // Set TRIAC gate to high.
    gateOn = true;                                  // Record TRIAC state.
    OCR1B += PULSE;                                 // When PULSE ticks passed, we have to come back and switch off the gate again.
  }
  else {
    digitalWrite(GATE_PIN, LOW);                    // Set TRIAC gate to low.
    gateOn = false;                                 // Record TRIAC state.
    if (peakDetected ==  false) {                   // No peak detected yet, so we switched before the next peak.
      TCCR1B = 0x00;                                // Disable timer to stop unintended triggers.
    }
  }
}