SpenceKonde / megaTinyCore

Arduino core for the tinyAVR 0/1/2-series - Ones's digit 2,4,5,7 (pincount, 8,14,20,24), tens digit 0, 1, or 2 (featureset), preceded by flash in kb. Library maintainers: porting help available!
Other
542 stars 140 forks source link

Timers periodic interrupt - no documentation available, help #1095

Open willwaush opened 2 months ago

willwaush commented 2 months ago

Hello there. I am trying to port some code from an Attiny45 over to an Attiny412. The code on the Attiny45 made use of an "Output Compare Match Interrupt" with TCNT0 registers and that's quite well documented on how it works and how to use it.

I haven't found such a clear documentation for such a use on the new Attiny series unfortunately, so I am asking for some help here.

In order to better understand these "new" registers I am trying to build a very basic example: An ISR function is toggling an LED between On/Off periodically. I want to be able to determine and manipulate its rate (period, timing).

Right now, after looking at various documentation and places I laded onto the following code:

#define TCB_TIMEOUT_VALUE 0xffff

void CLOCK_init (void){
    //Mandatory unlock prescaler protection
    CCP = CCP_IOREG_gc;     
    //Setting TCB Clock Prescaler
    CLKCTRL_MCLKCTRLB = CLKCTRL_PDIV_64X_gc | CLKCTRL_PEN_bm; 
    CLKCTRL_MCLKCTRLA = CLKCTRL_CLKSEL_OSCULP32K_gc;

    /* Wait for system oscillator changing to finish */
    while (CLKCTRL_MCLKSTATUS & CLKCTRL_SOSC_bm)
    {
        ;
    }
}

void PORT_init (void){
    // Setting PIN6 as Output and initializing it HIGH
    PORTA_DIR = 0b01000000;
    PORTA_OUT = 0b01000000;
}

void TCB0_init (void){

    /* Load the Compare or Capture register with the timeout value*/
    TCB0_CCMP = TCB_TIMEOUT_VALUE;

    /* Enable TCB and set CLK_PER divider to 1 (No Prescaling) */
    TCB0_CTRLA = TCB_CLKSEL_CLKDIV2_gc | TCB_ENABLE_bm;

    /* Enable Capture or Timeout interrupt */
    TCB0_INTCTRL = TCB_CAPT_bm;
}

ISR(TCB0_INT_vect)
{
    TCB0_INTFLAGS = TCB_CAPT_bm; /* Clear the interrupt flag */
    PORTA_OUT ^= (1 << PIN6_bp); //XOR (exclusive OR) to toggle the LED state
}

int main(void)
{
    CLOCK_init();
    PORT_init();
    TCB0_init();

    //Enable Global Interrupts
    sei(); 

    while (1)
    {
        ;
    }        
}

With the above code I get an LED rate which seems to be around 400ms. Although I'm not getting why! I would like to understand how to manipulate these registers to get precisely to a desired timing (for example, 10ms).

A few things I noticed: • Decreasing the TCB_TIMEOUT_VALUE obviously decreases the time interval at which the ISR gets called; • Assigning a smaller Prescaler at CLKCTRL_MCLKCTRLB = CLKCTRL_PDIV_64X_gc | CLKCTRL_PEN_bm decreases the time interval; • The following instruction seems to do nothing: CLKCTRL_MCLKCTRLA = CLKCTRL_CLKSEL_OSCULP32K_gc; • Using an advised datasheet assignation to get into Periodic Timeout Mode makes the code not work at all anymore: TCB0_CTRLB = TCB_CNTMODE_TIMEOUT_gc;

• Flashing with a lower clock speed (20Mhz is the Default one, I tried decreasing to any sort of value to slow the rate of the LED) doesn't affect the LED timing at all!

• With this instruction I have three clock options, but I don't understand what they really do.

TCB0_CTRLA = TCB_CLKSEL_CLKDIV1_gc | TCB_ENABLE_bm; --> Seems to give no extra CLK "division"
TCB0_CTRLA = TCB_CLKSEL_CLKDIV2_gc | TCB_ENABLE_bm; --> Seems to halve the CLK, so slows it down by a factor of 2
TCB0_CTRLA = TCB_CLKSEL_CLKTCA | TCB_ENABLE_bm;        ---> Should get the CLK from the TCA Timer, but I am not able to make this work in any sort of way.

Thank you guys in advance!

hmeijdam commented 2 months ago

I recognize your confusion, I had exactly the same.

What you need to realize is that when not using Setup and Loop you are skipping the core initialization, which sets the CPU prescaler clock to your desired clock speed. So you have to do the CPU clock prescaling yourself.

Try with adding below F_CPU_init() function to the bottom of your sketch and call it first thing in your main(void)

int main(void) {
 F_CPU_init ();   // reconfigure CPU clock prescaler
  while (1) {
  }
}

void F_CPU_init () {
  // reconfigure CPU clock prescaler
#if (F_CPU == 20000000)
  /* No division on clock */
  _PROTECTED_WRITE(CLKCTRL_MCLKCTRLB, 0x00);
#elif (F_CPU == 16000000)
  /* No division on clock */
  _PROTECTED_WRITE(CLKCTRL_MCLKCTRLB, 0x00);
#elif (F_CPU == 10000000) // 20MHz prescaled by 2
  /* Clock DIV2 */
  _PROTECTED_WRITE(CLKCTRL_MCLKCTRLB, (CLKCTRL_PEN_bm | CLKCTRL_PDIV_2X_gc));
#elif (F_CPU == 8000000) // 16MHz prescaled by 2
  /* Clock DIV2 */
  _PROTECTED_WRITE(CLKCTRL_MCLKCTRLB, (CLKCTRL_PEN_bm | CLKCTRL_PDIV_2X_gc));
#elif (F_CPU == 5000000) // 20MHz prescaled by 4
  /* Clock DIV4 */
  _PROTECTED_WRITE(CLKCTRL_MCLKCTRLB, (CLKCTRL_PEN_bm | CLKCTRL_PDIV_4X_gc));
#elif (F_CPU == 4000000) // 16MHz prescaled by 4
  /* Clock DIV4 */
  _PROTECTED_WRITE(CLKCTRL_MCLKCTRLB, (CLKCTRL_PEN_bm | CLKCTRL_PDIV_4X_gc));
#elif (F_CPU == 2000000) // 16MHz prescaled by 8
  /* Clock DIV8 */
  _PROTECTED_WRITE(CLKCTRL_MCLKCTRLB, (CLKCTRL_PEN_bm | CLKCTRL_PDIV_8X_gc));
#elif (F_CPU == 1000000) // 16MHz prescaled by 16
  /* Clock DIV16 */
  _PROTECTED_WRITE(CLKCTRL_MCLKCTRLB, (CLKCTRL_PEN_bm | CLKCTRL_PDIV_16X_gc));
#else
#ifndef F_CPU
#error "F_CPU not defined"
#else
#error "F_CPU defined as an unsupported value for untuned internal oscillator"
#endif
#endif
}

[edit] O, and for millis(), micros() and delay() functions to work, you also need the core initialization. And in the tools menu of the IDE you can select which timer you want Millis() to use.

willwaush commented 2 months ago

Thank you for your suggestion. I wasn’t particularly worried about the setup you mentioned because I am compiling through PlatformIO, which has already got a way of setting the Clock freq.

I will anyway try your method and report back.

Still, I feel quite confused about what do the instructions I used do really. I would like to find a way to get to an actual formula to estimate precisely the amount of time between each ISR call. Timing is critical in my project , and even if it “seems” to work, I want to be sure I’m estimating time correctly.

hmeijdam commented 2 months ago

Looking again at your code it's probably not going to make a difference, as you don't use the 16/20MHz oscillator

  CLKCTRL_MCLKCTRLB = CLKCTRL_PDIV_64X_gc | CLKCTRL_PEN_bm; 
  CLKCTRL_MCLKCTRLA = CLKCTRL_CLKSEL_OSCULP32K_gc;

Here you select the 32.768 KHz oscillator as the main system clock, and you prescale it with a factor 64. That would mean a 512 Hz clocked CPU. That's very different from the MHz speeds more commonly used.

[edit] The second statement is not being processed, as it would require the CCP = CCP_IOREG_gc; command again just before it

So in fact what you are doing is prescaling the 20 or 16MHz oscillator by 64. so your CPU (and tmer B) is running from a 3.125 MHz or 2.5MHz clock.

Your Clock_init would also override the CPU_init that I provided in my previous post, so that is not working anymore.

hmeijdam commented 2 months ago

So I think your 400ms comes from:

20MHz (oscillator) / 64 (CLKCTRL_PDIV_64X_gc) / 2 (TCB_CLKSEL_CLKDIV2_gc ) / 65535 (TCB_TIMEOUT_VALUE) = 2.3 Hz

willwaush commented 2 months ago

Does CLKCTRL_MCLKCTRLB change the system clock and CLKCTRL_MCLKCTRLA change only the timers clock source?

Because i would like to be able to have control over both the overall system clock (for performance and consumption) and timers clock (to evaluate timing precisely)

Right now I have reached 1 second of delay by changing from the code you have seen above.

    #define TCB_TIMEOUT_VALUE 0x7fff
    ...
    CCP = CCP_IOREG_gc;     
    CLKCTRL_MCLKCTRLB = 0x00;
    CCP = CCP_IOREG_gc;
    CLKCTRL_MCLKCTRLA = CLKCTRL_CLKSEL_OSCULP32K_gc;
    ...
    TCB0_CTRLA = TCB_CLKSEL_CLKDIV1_gc | TCB_ENABLE_bm;

Although now I know how to evaluate timing (kinda of, thanks to you) I am not so sure any longer about the main system clock.

PS: also I see on the datasheet that the clock source for the TCBx Timer can be chosen between two different sources (?)

hmeijdam commented 2 months ago

The Attiny412 has 3 clock sources.

1a) The 16 MHz oscillator 1b) The 20 MHz oscillator 2) The 32.768 KHz Ultra Low Power Oscillator

The choice between 1a) and 1b) is fuse driven, so cannot be selected in software. This is why it is usually called the 16/20 MHz oscillator. You can choose between 1 or 2 in software,

Please study 10.2.1 Block Diagram - CLKCTRL carefully and look at the Other Peripherals, as that's where TimerB also get's it clock source from. From the main clock prescaler.