Search code examples
cavr-gccatmelstudioattiny

Unexpected Timer/Counter B interrupt frequency on ATtiny204


I'm trying to implement a timing system on an ATtiny204 using Timer/Counter B in Microchip studio but I'm getting a very unexpected interrupt frequency based on my register and fuse settings.

I have my OSCCFG.FREQSEL fuse set to the 20MHz clock, and I'm initializing the clock controller with the following two lines which should give me a 20Mhz CLK_CPU and a 312.5kHz CLK_PER

CLKCTRL.MCLKCTRLA = CLKCTRL_CLKSEL_OSC20M_gc; //Select internal 20Mhz oscillator
CLKCTRL.MCLKCTRLB = CLKCTRL_PDIV_64X_gc | CLKCTRL_PEN_bm; //Set peripheral clock to 1/64th of CPU clock

I'm initializing the Timer/Counter B module with the following lines:

TCB0.CCMP = MATCH_VALUE; //Some value such that we get 1000 interrupts per second
TCB0.CTRLB = TCB_CNTMODE_INT_gc; //Periodic interrupt mode
TCB0.EVCTRL = TCB_CAPTEI_bm; //Enable input capture event (probably not needed)
TCB0.CTRLA = TCB_CLKSEL_CLKDIV1_gc | TCB_ENABLE_bm; //Use CLK_PER not CLK_PER/2 and enable timer
TCB0.INTCTRL = TCB_CAPT_bm; //Enable interrupt

From what I read in the datasheet, in periodic interrupt mode, the timer counts until there's a match between its value and TCB0.CCMP then triggers the interrupt, and starts counting again from zero. The interrupt handler just increments a master timestamp uint16_t and flips a pin on and off so I can read the interrupt frequency with a logic analyzer, and clears the interrupt flag.

ISR(TCB0_INT_vect){
    if(++master & 1)
        VPORTA.OUT &= ~_BV(4);
    else
        VPORTA.OUT |= _BV(4);
    TCB0_INTFLAGS = TCB_CAPT_bm; //Clear the flag that triggered this interrupt
}

From my understanding, the proper value for MATCH_VALUE should be 20000000/64/1000 which is about 312, but this value gives me an interrupt frequency of about 10.6kHz.

I've found from experimentation that 3360 (0xD20) gives me almost exactly a 1kHz signal, but I cannot figure out how that makes sense mathematically based on the settings.

I originally thought my microcontroller was damaged and the clock was just going at the wrong speed, but I tried the firmware on a new chip and got the same behavior.

What is wrong in my code or my math? Is it safe to use my value from experimentation or have I done something that will cause other issues?


Solution

  • Some special function registers (SFRs) on Xmega devices have a configuration change protection (CCP). Before writing to such an SFR, one has to write a special pass-byte to the CCP register, think "password".

    Also this is a timed sequence which means that the write to the target SFR has to be carried out within a few CPU cycles after writing the correct byte to CCP. This means

    • C/C++ is unsafe because you have no control over the instructions the compiler generates and over its timings.

    In addition, interrupts have to be disabled. As far as I remember, writing the right pass-byte to CCP starts an atomic sequence for some cycles, but better check against the data sheet. In that case, no explicit disabling IRQs is required even when IRQs might be on.

    AVR-LibC provides an inline assembly macro to facilitate such writes:

    #include <avr/io.h>
    // In some function:
        // Select internal 20 MHz oscillator.
        _PROTECTED_WRITE (CLKCTRL.MCLKCTRLA, CLKCTRL_CLKSEL_OSC20M_gc);
        // Set peripheral clock to 1/64th of CPU clock.
        _PROTECTED_WRITE (CLKCTRL.MCLKCTRLB, CLKCTRL_PDIV_64X_gc | CLKCTRL_PEN_bm);
    

    In addition, there is ccp_write_io() in avr/cpufunc.h which calls a library function (and thus would generate more overhead in general). Also mind AVR-LibC issue #906.