Atmega328 fast PWM (mode 7) of timer 0 not working as expected

atmegaavrmicrocontrollerpwmtimer

I am blinking an LED on PORTB5 using timer 0 in mode 7. The time between blinks is computed as $$ \frac{(\text{OCROA}+1) \times \text{prescaler} \times \text{timerCount}}{\text{FCPU}} $$ \$FCPU=16MHz\$. With \$OCROA=124\$, \$prescaler=1024\$, and \$timerCount=125\$, the time turns out to be \$1 \space second\$ and this works as expected. But with \$OCROA=16\$, \$prescaler=1\$, and \$timerCount=2956793\$, I would expect the time to be \$~3.14 \space seconds\$, but I am getting something around \$12 \space seconds\$.

Code that works as expected:

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

volatile uint32_t timerCount = 0;
ISR(TIMER0_COMPA_vect)
{
        ++timerCount;
}

int main(void)
{
        DDRB |= 1 << DDB5;
        TIMSK0 |= 1 << OCIE0A;
        TCCR0A |= 1 << WGM00 | 1 << WGM01;
        TCCR0B |= 1 << WGM02;
        OCR0A = 124;
        sei();
        TCCR0B |= 1 << CS02 | 1 << CS00;
        while(1)
        {
        if(timerCount >= 125)
                {
                        PORTB ^= 1 << PORTB5;
                        timerCount = 0;
                }
        }
        return 0;
}

Code that does not work as expected:

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

volatile uint32_t timerCount = 0;
ISR(TIMER0_COMPA_vect)
{
        ++timerCount;
}

int main(void)
{
        DDRB |= 1 << DDB5;
        TIMSK0 |= 1 << OCIE0A;
        TCCR0A |= 1 << WGM00 | 1 << WGM01;
        TCCR0B |= 1 << WGM02;
        OCR0A = 16;
        sei();
        TCCR0B |= 1 << CS00;
        while(1)
        {
        if(timerCount >= 2956793)
                {
                        PORTB ^= 1 << PORTB5;
                        timerCount = 0;
                }
        }
        return 0;
}

What could be the issue with the latter?

Best Answer

In your second setting the timer is incrementing once for every clock cycle (prescaler is 1), but it only counts to 16. The AVR executes one instruction per clock cycle (except for branches) so there are less than 16 instructions available between each PWM interrupt.

The execution time of the ISR is longer than this (it may be variable as well) and that is causing your code to periodically miss interrupts, extending the apparent time it is taking to count to your target value. To fix the problem you need to either use the prescaler and count to a higher value, or if you can't do that (if you are using the hardware PWM generator), look at using OCR1B with a longer count to generate the interrupts instead.

Also you should disable interrupts when you clear timerCount because this is a 32-bit value that requires multiple instructions to update on an 8-bit platform.