Reset PWM to synch with external signal

I'm sorry if this is a dumb question or too general. I've been researching for hours and can't find an answer for the Mega (only for the Due).

I want to reset the PWM pulse stream on an interrupt triggered by a digital input pin, which is driven by a zero crossing detector connected to a full wave rectified 120Hz 12v signal. In other words, I want to start the PWM timer from zero each time there's a zero crossing, so that it produces a pulse with a certain timing during that period of the AC sine wave.

My idea was to set up a PWM output with a lower frequency, say 30Hz and let it produce the pulse I need, and then detect the zero crossing in an interrupt and write zero to the timer registers (TCNT1 and TCNT2?). Since the period of the PWM is longer than the period of the AC sine wave, I should get the beginning of one PWM cycle (one high-low transition) before the timer gets reset.

It doesn't work though. The PWM simply stops outputting.

If it's important, this is meant to control low voltage lighting. The output pulse from the PWM pin will turn a triac on, which then turns off at the next zero crossing. The PWM output will be inverted (low then high) so that I can delay the low-high transition as desired. I've written a solution that works by polling micros() to produce the pulses I need, but using PWM seemed so much more elegant and would involve simpler code.

If anyone has any ideas or insight I would much appreciate it.

TL;RD - I want to generate a pulse from a PWM pin when an interrupt is fired by a digital input pin. I know this will work with the circuitry I'm using because I can generate the needed pulse by polling micros() and it satisfies what I need. I just want to do it with PWM if possible.

The ESP32's MCPWM has the option to sync to an external signal.

https://docs.espressif.com/projects/esp-idf/en/latest/esp32/api-reference/peripherals/mcpwm.html

1 Like

Ah, that looks great. Sadly, I'm using an Arduino Mega 2560. Is there no way to accomplish this onboard with the Mega?

I think the problem is that the PWM only turns on the output pin when it rolls over from TOP to 0. Try setting TCNT1 (Timer 1) or TCNT2 (Timer2) to 255 instead of 0. That should cause an overflow on the next timer clock and turn the PWM pin on.

Oh, cool! I'll try it right now. Thank you.

Nothing yet. I have pin 9 set up for a 30Hz pulse stream. I'm resetting TCNT2 to B11111111 in an interrupt that fires 120 times per second. The PWM on pin 9 never fires though. I'll keep playing with it.

Perhaps it is time to show us your sketch.

Indeed. Here goes.

I'm detecting the 120Hz pulse stream with an interrupt on pin3 on the falling edge of the signal. For testing that signal currently has a 90% duty cycle, so it outputs a ~7.5ms high pulse at the start of each cycle. The interrupt should fire every 8.3ms.

My 30Hz PWM is set to 50 (~20% duty cycle) and should output a 6ms high pulse. The interrupt should detect the 120Hz signal 2.3ms later and reset the timer, starting the PWM over. Its full cycle should never finish and it should end up having an effective frequency of 120Hz.

I have this PWM outputting to an LED. The LED never turns on. I also tried detecting the rising and fulling edge of this signal with another interrupt, and it never fires.

I'm baffled, and I really appreciate the time looking at this.

int pwmPulsePin = 11;
int pwmLEDPin = 12;
int interruptZeroCrossPin = 3;

void setup() {
  TCCR1B = TCCR1B & B11111000 | B00000101;  // set pins 11 and 12 to PWM frequency of 30.64 Hz
  pinMode(pwmPulsePin, OUTPUT);
  pinMode(pwmLEDPin, OUTPUT);
  pinMode(interruptZeroCrossPin, INPUT);
  attachInterrupt(digitalPinToInterrupt(interruptZeroCrossPin), isr_zerocross, FALLING);
}

void isr_zerocross() {
  TCNT1 = B00000001;
  TCNT2 = B00000001;
}

void loop() {
  analogWrite(pwmPulsePin, 50);
  analogWrite(pwmLEDPin, 50);
}

You shouldn't be calling analogWrite 1000's of times a second. Try doing it once, in setup.

By the way, I tried looking at the timer values at the point the interrupt fires. They're just about 128 - halfway through the full count. That corresponds to 60Hz - just about exactly half the frequency of the input signal.

It also shows that the timer is counting, and the PWM should be outputting. But it isn't. I'm out of ideas.

BTW I just realized that I accidentally reverted to an earlier version of the code where I'm not resetting the timer to 255 as suggested. My apologies.

I will try that, thank you.

Ok, I'm calling analogWrite once in setup, and I'm resetting the timer to 255 instead of 0. Nothing

But...I can't see why either of those would matter in terms of the PWM pin never going high. It should turn the LED on every time it rolls over, even if the timing is wrong, and it never does. I even tried feeding the PWM signal into another input pin and using that to trigger another interrupt. It never fires.

You are not setting the WGM (Waveform Generation Mode) so it retains whatever mode was set when the Arduino IDE initialized it.

This is the initialization from "wiring.c"

#if defined(TCCR1B) && defined(CS11) && defined(CS10)
	TCCR1B = 0;

	// set timer 1 prescale factor to 64
	sbi(TCCR1B, CS11);
#if F_CPU >= 8000000L
	sbi(TCCR1B, CS10);
#endif
#elif defined(TCCR1) && defined(CS11) && defined(CS10)
	sbi(TCCR1, CS11);
#if F_CPU >= 8000000L
	sbi(TCCR1, CS10);
#endif
#endif
	// put timer 1 in 8-bit phase correct pwm mode
#if defined(TCCR1A) && defined(WGM10)
	sbi(TCCR1A, WGM10);
#endif

That boils down to:

	TCCR1B = 0b00000011;  // Prescale 64
        TCCR1A = 0b00000001;  // WGM = 1 (Phase-correct, 8-bit)

Warning: "Phase-Correct" PWM alternates between counting up and counting down.

You probably want 8-bit Fast PWM which is WGM 5.

WARNING: The WGM10 and WGM11 bits are in TCCR1A while WGM12 and WGM13 are in TCCR1B.

To get 8-bit Fast PWM:

  TCCR1B = 0;  // Stop the timer/counter
  TCCR1A = 0; // Clear the register
  TCNT1 = 0; // Reset the timer count
  // Turn on the A and B PWM outputs
  OCR1A = 50; // analogWrite(pwmPulsePin , 50);
  OCR1B = 50; // analogWrite(pwmLEDPin , 50);
  TCCR1A |= _BV(COM1A1) | _BV(COM1B1);
  // Set WGM 5 (8-bit Fast PWM):
  TCCR1A |= _BV(WGM10);
  TCCR1B |= _BV(WGM12);
  // Turn on clock at prescale = 1024 (60.04 Hz Fast PWM)
  TCCR1B |= _BV(CS12) | _BV(CS10); // Clock Select = 5

Your full sketch would then be:

const byte pwmPulsePin = 11;
const byte pwmLEDPin = 12;
const byte interruptZeroCrossPin = 3;

void setup()
{
  pinMode(pwmPulsePin, OUTPUT);
  pinMode(pwmLEDPin, OUTPUT);

  TCCR1B = 0;  // Stop the timer/counter
  TCCR1A = 0; // Clear the register
  TCNT1 = 0; // Reset the timer count
  // Turn on the A and B PWM outputs
  OCR1A = 50; // analogWrite(pwmPulsePin , 50);
  OCR1B = 50; // analogWrite(pwmLEDPin , 50);
  TCCR1A |= _BV(COM1A1) | _BV(COM1B1);
  // Set WGM 5 (8-bit Fast PWM):
  TCCR1A |= _BV(WGM10);
  TCCR1B |= _BV(WGM12);
  // Turn on clock at prescale = 1024 (60.04 Hz Fast PWM)
  TCCR1B |= _BV(CS12) | _BV(CS10); // Clock Select = 5

  pinMode(interruptZeroCrossPin, INPUT);
  attachInterrupt(digitalPinToInterrupt(interruptZeroCrossPin), isr_zerocross, FALLING);
}

void isr_zerocross()
{
  TCNT1 = 255;
}

void loop() {}
1 Like

Calling analogWrite() initializes some timer registers. After that, the timer counter runs at some phase. If you call it again, it will change the phase because the counter is reset. So it's not something you want to do often because there is a slight phase shift each time you do it. If you do it at audio frequencies, the effect severely distorts the signal. AFAIK.

Disclaimer: I haven't read the timer code. I'm basing this on instances of people posting code with this possible error, and seeing that changing it fixed the audio problem. It's not hard to see how some other aspect of the initialization might also cause the same problem.

Maybe that wouldn't keep the PWM pin low specifically. But it's best to test with a clean slate, no risk of spill over from other issues.

I did once write some custom code for another processor, it changed duty cycle transparently, because the function to write the PWM value did absolutely no initialization, just wrote the value. As the count register was buffered in that case, the PWM signal phase always remain intact, the same before and after a PWM value update. Then, writing to it at any rate was perfectly safe.

From wiring_analog.c:

			case TIMER1A:
				// connect pwm to pin on timer 1, channel A
				sbi(TCCR1A, COM1A1);
				OCR1A = val; // set pwm duty
				break;
			#endif

All it does is set the COM1A1 bit in TCCR1A (which will already be set) and store a value to the OCR1A buffer which will be moved to OCR1A the next time the timer hits TOP (and that value was already set). I don't think calling analogWrite() thousands of times per second will cause any problems.

Well, that's a good thing. Maybe the sketch in that case was writing values that were causing it. Good to know.

On principle, it's not good to do things redundantly. As you make additions or changes, things like that can be left behind and forgotten, maybe cause problems later...

It's also harmless to write a pin LOW 1000's of times a second, but it's a bad programming practice for the same reason.

Holy Guacamole, Batman! It works!
Truly appreciate it, John. Your reply was generally educational in terms of bitbanging the PWM registers too. I had to take several trips back to the ATMega datasheet to understand what you were doing.

What was getting set wrong in my previous attempts? It would have been something in the default IDE settings, yes?

That's true, but in the final use I will be calling digitalWrite 100s of times per second to change the duty cycle (although I may do it by writing directly to the OCR registers now that I understand how to). This is intended to dim a 12v or 120v light by firing a triac on a delay after the zero crossing of the power signal.

Where does wiring.c come from? Is that part of the Arudino IDE?