1

I'm using the hardware timers on the 328 for phase angle control of a triac with zero-crossing detection. I am encountering some strange behavior with the "Set on Compare Match" feature not working as expected on Timer1. I am attempting to replicate the red "gate" trace here: https://playground.arduino.cc/Main/ACPhaseControl/, but with my own implementation in code.

Before building the triac and zero-cross detection circuit, I am using another timer (Timer0) to simulate a 120Hz AC square wave (2x the 60Hz frequency because the double crossing pulse will arrive twice a cycle).

The output of the synthesized AC square wave is fed into an external interrupt pin (INT0) with a jumper. The correct functionality of the program is a short pulse to fire the triac at some phase delay relative to the zero crossing interrupt event (another example: https://www.homemade-circuits.com/wp-content/uploads/2020/07/half-phase-control.jpg)

I configure Timer 1 in normal mode, and define compare levels for both OCR1A and OCR1B. OCR1A is the "phase delay" from zero crossing at which to fire the triac, and OCR1B sets the duration of the pulse used to fire the triac. In my code, Method 1 is working and producing the intended output, while Method 2 is not working (output is always ON). For brevity, I am highlighting the differences between the two methods below, and the rest of the code is the same.

Method 1:

  • Set OC1A to do nothing on compare (normal port operation) TCCR1A=0;
  • "Manually" turn on the pin inside of the interrupt handler for output compare A. It will later be turned off in the interrupt handler for output compare B: PORTB=(1<<PORTB1);

This produces the following (correct) output. In the oscilloscope screenshot, the yellow trace is the simulated AC square wave, and the green trace is to be the gate voltage on the triac. https://i.sstatic.net/tpSAw.png

Method 2:

  • Enable the OC1A "Set at compare match" bit in the TCCR1A register: TCCR1A |= (1 << COM1A1) | (1 << COM1A0);
  • In theory, this means that I don't have to manually turn on the pin inside the interrupt handler for compare A, because it's already been turned on for us by the Compare Match Output Unit.

https://i.sstatic.net/45tFA.png

Note that in both methods, I still need to "manually" turn off the pin inside the output compare B interrupt handler because OC1A and OC1B are not the same physical pin: PORTB=0;

In Method 2, the pin is held ON, with no indication that the interrupt handler for output compare B is doing anything. My mental model of the port is that as long as the bit in the data direction register is set for that port, both the output compare unit and CPU can write to the pin. It appears that the pin is always being held ON, even after it is turned OFF by the CPU. Does the compare unit lock/latch the pin until the next clock cycle, or am I missing something else? Some tips here would be much appreciated.

I can prove that the output is indeed connected to OC1A because OC1A is PB1 which is connected to the oscilloscope and is being toggled in Method 1. I have vertically scaled OC1A (green) for readability against OC0A.

I have been referencing Section 13 of https://www.sparkfun.com/datasheets/Components/SMD/ATMega328.pdf for most of this task.

My code below for Method 1 is presented. To get Method 2, toggle the comments on the indicated lines.


#define F_CPU 16000000L

#include <avr/io.h>
#include <avr/interrupt.h>

/*
For reference:
OCR1A = turn_on_timer_level
OCR1B = turn_on_timer_level + pulse_width

*/

int timer0_ac_sim_val = 64;    // Measured in clock ticks with prescale=1024
int turn_on_timer_level = 500;    // Measured in clock ticks with prescale=64
int pulse_width = 200;    // Measured in clock ticks with prescale=64

ISR(INT0_vect){
    // ISR at output compare A of timer 0 turns on timer 1 - simulates what happens when a zero crossing rising edge is detected on a 120Hz AC sine wave
    TCCR1B |= (1<<CS11) | (1<<CS10);
    TCNT1 = 0;
}

ISR(TIMER1_COMPA_vect){
    // ISR at output compare A of timer 1 turns on the pin
    PORTB = (1<<PORTB1);   // Turn on the pin COMMENT FOR METHOD 2
}

ISR(TIMER1_COMPB_vect){
    TCCR1B = 0; // Turn off the timer
    PORTB = 0;  // Turn off the pin
}

int main(){

    // Timer 0 setup for a 120Hz square wave on OC0A
    DDRD |= (1<<PD6);

    TCCR0A = 0x0;
    TCCR0A |= (1<<COM0A0) | (1<<WGM01);  // Set the output to toggle on compare

    TCCR0B = 0x0;
    TCCR0B |= (1<<CS02) | (1<<CS00);     // Configure with a prescale of 1024 and start clock

    OCR0A = timer0_ac_sim_val;  // Calculate this value for a 120Hz square wave, doesn't change.

    // Enable external interrupt 0 to trigger on a rising edge - set the External Interrupt Control Register
    EICRA |= (1<<ISC01) | (1<<ISC00);
    EIMSK |= (1<<INT0);

    // Global interrupt enable
    sei();

    // Timer 1 setup
    DDRB |= (1<<PORTB1);

    TCCR1A = 0x0;   // No pin action on output compare
    // TCCR1A |= (1 << COM1A1) | (1 << COM1A0);    // Set OC1A on compare match UNCOMMENT FOR METHOD 2

    TCCR1B = 0x0;   // Disable clock

    OCR1A = turn_on_timer_level;  // Delay between OC0A rise and OCR1A trigger
    OCR1B = turn_on_timer_level + pulse_width;   // Delay between OC0A rise and OCR1B trigger

    // Enable timer 1 interrupts
    TIMSK1 |= (1<<OCIE1A) | (1<<OCIE1B);

    while(1){
    };
}

2 Answers 2

0

My mental model of the port is that as long as the bit in the data direction register is set for that port, both the output compare unit and CPU can write to the pin.

Your assumption is incorrect. See section 13.11.1 of the datasheet you linked.

If one or both of the COM1A1:0 bits are written to one, the OC1A output overrides the normal port functionality of the I/O pin it is connected to.

Thus, the "normal" functionality being overridden precludes your changing the pin yourself. You would need to set those bits to zero in the ISR to reclaim the use of the port, and set them back to one when required.

1
  • Perfect! I missed that in the datasheet when I was reading. Thanks so much.
    – saustinp
    Feb 22, 2023 at 14:12
0

For those who might be interested, after fixing the problem I decided to implement the same functionality using Fast PWM mode instead of CTC mode. In trying to debug my original issue, I came across a similar post, to which one of the answers was:

https://arduino.stackexchange.com/a/30529/89096

The PWM modes manage pin at both ends of the cycle.

The point of the CTC modes is that they manage the pin only at one end of the >count, but allow you to manage the other end explicitly.

To manage both ends explicitly, you use normal mode.

I didn't fully realize what that meant until now, but it clicked. In PWM mode, the hardware timer takes care of the pin toggling for us, on both sides of the waveform. We would use Normal or CTC modes if we wanted to do manage one or both sides of the waveform differently. But my application fits perfectly in the Fast PWM category, so I decided to implement that as well. This saves an ISR by not making the CPU "manually" toggle the pin on and off each time. The only difference is that the output is on OC1B (PORTB2) now instead of OC1A (PORTB1).

#define F_CPU 16000000L

#include <avr/io.h>
#include <avr/interrupt.h>

int timer0_ac_sim_val = 64;    // Measured in clock ticks with prescale=1024
int turn_on_timer_level = 400;    // Measured in clock ticks with prescale=64
int pulse_width = 100;    // Measured in clock ticks with prescale=64

ISR(INT0_vect){
    // ISR at output compare A of timer 0 turns on timer 1 - simulates what happens when a pulse is received from the zero crossing detector
    TCCR1B |= (1<<CS11) | (1<<CS10) | (1<<WGM13) | (1<<WGM12);
    TCNT1 = 0;
}

ISR(TIMER1_COMPA_vect){
    // ISR at output compare A of timer 1 turns on the pin
    TCCR1B = 0; // Turn off the timer
}

int main(){
    // Timer 0 setup for a 120Hz square wave on OC0A
    DDRD |= (1<<PD6);

    TCCR0A = 0x0;
    TCCR0A |= (1<<COM0A0) | (1<<WGM01);  // Toggle mode - non-PWM

    TCCR0B = 0x0;
    TCCR0B |= (1<<CS02) | (1<<CS00);     // Configure with a prescale of 1024, start clock

    OCR0A = timer0_ac_sim_val;

    // Enable external interrupt 0 to trigger on a rising edge - set the External Interrupt Control Register
    EICRA |= (1<<ISC01) | (1<<ISC00);
    EIMSK |= (1<<INT0);

    // Enable interrupts
    sei();

    // Timer 1 setup - 16 bit resolution with a prescale of 8 gives a max period of 1/(16e6/8/65536) = 32.78ms
    DDRB |= (1<<PORTB2);

    // Set WGM12 (CTC) to 1 and start the timer with a prescaler of 1024   -> This starts the timer in CTC mode
    TCCR1A = (1<<COM1B1) | (1<<COM1B0) | (1<<WGM11) | (1<<WGM10);  // Set COM1B1 and COM1B0 to 1 for inverting mode. For fast PWM set WGM11 and WGM10
    TCCR1B = 0x0;   // Start with clock disabled

    OCR1A = turn_on_timer_level + pulse_width;
    OCR1B = turn_on_timer_level;

    // Enable timer 1 interrupts
    TIMSK1 |= (1<<OCIE1A);

    while(1){

    }

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.