Understanding the Mode 0 and Mode 1 Timer on the SAMD51

I've been banging my head against the wall the last few hours trying to adapt some code I'd written for a custom SAMD21 that is now utilizing a SAMD51. What's really giving me trouble is the scheduler that relies on the RTC timer being used in Mode 1 (16-bit periodic counter). It's straight forward enough on the SAMD21 using this as reference, but there have been some changes to the way it works on the SAMD51 that don't carry over with the STATUS registers.

Any help with implementing interrupts on compare values and overflow or just a little help with where to look would be greatly appreciated.

Hi fostac,

Here's an example of how to use the SAMD51's RTC in mode 1, calling the RTC_Handler() interrupt service routine every 10 seconds and toggling the LED on D13 (or LED_BUILTIN). It also puts the microcontrolller into deep sleep during intervening time, this is disables the USB and requires a double press of the reset button to recover for reprogramming.

There's also the option to drive the RTC either from it's internal Ultra Low Power 32kHz oscillator:

OSC32KCTRL->OSCULP32K.bit.EN1K = 1;                           // Enable ULPOSC32K 1KHz clock output
OSC32KCTRL->RTCCTRL.reg |= OSC32KCTRL_RTCCTRL_RTCSEL_ULP1K;   // Select the 1kHz Ultra Low Power (ULP) for the RTC

...or alternatively from its more accurate external 32kHz crystal, (if one's present on your board?):

OSC32KCTRL->XOSC32K.bit.EN1K = 1;                             // Enable XOSC32K 1kHz clock output
OSC32KCTRL->RTCCTRL.reg |= OSC32KCTRL_RTCCTRL_RTCSEL_XOSC1K;  // Select the 1kHz external crystal for the RTC

Here's the code:

// Put SAMD51 into deep sleep and RTC interrupts set to trigger every 10s (PER) 
void setup(){

  // Output to D13 to check that the ISR is being called every 10 seconds
  PORT->Group[g_APinDescription[LED_BUILTIN].ulPort].DIRSET.reg = 1 << g_APinDescription[LED_BUILTIN].ulPin;    

// RTC configuration (rtc.h)----------------------------------------------------                                             
  RTC->MODE1.CTRLA.bit.ENABLE = 0;                     // Disable the RTC
  while (RTC->MODE1.SYNCBUSY.bit.ENABLE);              // Wait for synchronization

  RTC->MODE1.CTRLA.bit.SWRST = 1;                      // Software reset the RTC
  while (RTC->MODE1.SYNCBUSY.bit.SWRST);               // Wait for synchronization
  
  OSC32KCTRL->OSCULP32K.bit.EN1K = 1;                           // Enable ULPOSC32K 1KHz clock output
  OSC32KCTRL->RTCCTRL.reg |= OSC32KCTRL_RTCCTRL_RTCSEL_ULP1K;   // Select the 1kHz Ultra Low Power (ULP) for the RTC
  //OSC32KCTRL->XOSC32K.bit.EN1K = 1;                             // Enable XOSC32K 1kHz clock output
  //OSC32KCTRL->RTCCTRL.reg |= OSC32KCTRL_RTCCTRL_RTCSEL_XOSC1K;  // Select the 1kHz external crystal for the RTC
  
  RTC->MODE1.CTRLA.reg |= RTC_MODE1_CTRLA_PRESCALER_DIV1024 |   // Set prescaler to 1024
                          RTC_MODE1_CTRLA_MODE_COUNT16;         // Set RTC to mode 1, 16-bit timer                         
  
  RTC->MODE1.PER.reg = RTC_MODE1_PER_PER(9);                    // Interrupt time 10s: 1Hz/(9 + 1)
  while (RTC->MODE1.SYNCBUSY.bit.PER);                          // Wait for synchronization

// Configure RTC interrupts ----------------------------------------------------
  NVIC_SetPriority(RTC_IRQn, 0);    // Set the Nested Vector Interrupt Controller (NVIC) priority for RTC
  NVIC_EnableIRQ(RTC_IRQn);         // Connect RTC to Nested Vector Interrupt Controller (NVIC)

  RTC->MODE1.INTENSET.reg = RTC_MODE1_INTENSET_OVF;             // Enable RTC overflow interrupts

// Set-up Deep Sleep Mode--------------------------------------------------------
  PM->SLEEPCFG.reg = PM_SLEEPCFG_SLEEPMODE_STANDBY;                         // Set up the SAMD51 to go into STANDBY mode during sleep
  while(PM->SLEEPCFG.bit.SLEEPMODE != PM_SLEEPCFG_SLEEPMODE_STANDBY_Val);   // Wait the STANDBY bitfield to be set

  // On the SAMD51 it's necessary to deactivate the RUNSTDBY (Run Standby) bit on the native USB
  USB->DEVICE.CTRLA.bit.ENABLE = 0;         // Disable the USB peripheral
  while(USB->DEVICE.SYNCBUSY.bit.ENABLE);   // Wait for synchronization
  USB->DEVICE.CTRLA.bit.RUNSTDBY = 0;       // Deactivate run on standby
  USB->DEVICE.CTRLA.bit.ENABLE = 1;         // Enable the USB peripheral
  while(USB->DEVICE.SYNCBUSY.bit.ENABLE);   // Wait for synchronization

// Enable RTC--------------------------------------------------------------------
  RTC->MODE1.CTRLA.bit.ENABLE = 1;                      // Enable the RTC
  while (RTC->MODE1.SYNCBUSY.bit.ENABLE);               // Wait for synchronization
}

void loop()
{
  __DSB();       // Complete outstanding memory operations - not required for SAMD21 ARM Cortex M0+
  __WFI();       // Put the SAMD51 into deep sleep, Zzzzzzzz...  
}

void RTC_Handler()
{
   // Note that these two lines have to be in this order - clear interrupt flag then toggle LED
   RTC->MODE1.INTFLAG.bit.OVF = 1;                // Reset the overflow interrupt flag
   PORT->Group[g_APinDescription[LED_BUILTIN].ulPort].OUTTGL.reg = 1 << g_APinDescription[LED_BUILTIN].ulPin; // Toggle the LED_BUILTIN
}

Martin, this is exactly what I was looking for! Thank you very much!

MartinL:
Hi fostac,

Here's an example of how to use the SAMD51's RTC in mode 1, calling the RTC_Handler() interrupt service routine every 10 seconds and toggling the LED on D13 (or LED_BUILTIN). It also puts the microcontrolller into deep sleep during intervening time, this is disables the USB and requires a double press of the reset button to recover for reprogramming.

There's also the option to drive the RTC either from it's internal Ultra Low Power 32kHz oscillator:

OSC32KCTRL->OSCULP32K.bit.EN1K = 1;                           // Enable ULPOSC32K 1KHz clock output

OSC32KCTRL->RTCCTRL.reg |= OSC32KCTRL_RTCCTRL_RTCSEL_ULP1K;  // Select the 1kHz Ultra Low Power (ULP) for the RTC



...or alternatively from its more accurate external 32kHz crystal, (if one's present on your board?):



OSC32KCTRL->XOSC32K.bit.EN1K = 1;                            // Enable XOSC32K 1kHz clock output
OSC32KCTRL->RTCCTRL.reg |= OSC32KCTRL_RTCCTRL_RTCSEL_XOSC1K;  // Select the 1kHz external crystal for the RTC



Here's the code:



// Put SAMD51 into deep sleep and RTC interrupts set to trigger every 10s (PER)
void setup(){

// Output to D13 to check that the ISR is being called every 10 seconds
  PORT->Group[g_APinDescription[LED_BUILTIN].ulPort].DIRSET.reg = 1 << g_APinDescription[LED_BUILTIN].ulPin;

// RTC configuration (rtc.h)----------------------------------------------------                                           
  RTC->MODE1.CTRLA.bit.ENABLE = 0;                    // Disable the RTC
  while (RTC->MODE1.SYNCBUSY.bit.ENABLE);              // Wait for synchronization

RTC->MODE1.CTRLA.bit.SWRST = 1;                      // Software reset the RTC
  while (RTC->MODE1.SYNCBUSY.bit.SWRST);              // Wait for synchronization
 
  OSC32KCTRL->OSCULP32K.bit.EN1K = 1;                          // Enable ULPOSC32K 1KHz clock output
  OSC32KCTRL->RTCCTRL.reg |= OSC32KCTRL_RTCCTRL_RTCSEL_ULP1K;  // Select the 1kHz Ultra Low Power (ULP) for the RTC
  //OSC32KCTRL->XOSC32K.bit.EN1K = 1;                            // Enable XOSC32K 1kHz clock output
  //OSC32KCTRL->RTCCTRL.reg |= OSC32KCTRL_RTCCTRL_RTCSEL_XOSC1K;  // Select the 1kHz external crystal for the RTC
 
  RTC->MODE1.CTRLA.reg |= RTC_MODE1_CTRLA_PRESCALER_DIV1024 |  // Set prescaler to 1024
                          RTC_MODE1_CTRLA_MODE_COUNT16;        // Set RTC to mode 1, 16-bit timer                       
 
  RTC->MODE1.PER.reg = RTC_MODE1_PER_PER(9);                    // Interrupt time 10s: 1Hz/(9 + 1)
  while (RTC->MODE1.SYNCBUSY.bit.PER);                          // Wait for synchronization

// Configure RTC interrupts ----------------------------------------------------
  NVIC_SetPriority(RTC_IRQn, 0);    // Set the Nested Vector Interrupt Controller (NVIC) priority for RTC
  NVIC_EnableIRQ(RTC_IRQn);        // Connect RTC to Nested Vector Interrupt Controller (NVIC)

RTC->MODE1.INTENSET.reg = RTC_MODE1_INTENSET_OVF;            // Enable RTC overflow interrupts

// Set-up Deep Sleep Mode--------------------------------------------------------
  PM->SLEEPCFG.reg = PM_SLEEPCFG_SLEEPMODE_STANDBY;                        // Set up the SAMD51 to go into STANDBY mode during sleep
  while(PM->SLEEPCFG.bit.SLEEPMODE != PM_SLEEPCFG_SLEEPMODE_STANDBY_Val);  // Wait the STANDBY bitfield to be set

// On the SAMD51 it's necessary to deactivate the RUNSTDBY (Run Standby) bit on the native USB
  USB->DEVICE.CTRLA.bit.ENABLE = 0;        // Disable the USB peripheral
  while(USB->DEVICE.SYNCBUSY.bit.ENABLE);  // Wait for synchronization
  USB->DEVICE.CTRLA.bit.RUNSTDBY = 0;      // Deactivate run on standby
  USB->DEVICE.CTRLA.bit.ENABLE = 1;        // Enable the USB peripheral
  while(USB->DEVICE.SYNCBUSY.bit.ENABLE);  // Wait for synchronization

// Enable RTC--------------------------------------------------------------------
  RTC->MODE1.CTRLA.bit.ENABLE = 1;                      // Enable the RTC
  while (RTC->MODE1.SYNCBUSY.bit.ENABLE);              // Wait for synchronization
}

void loop()
{
  __DSB();      // Complete outstanding memory operations - not required for SAMD21 ARM Cortex M0+
  __WFI();      // Put the SAMD51 into deep sleep, Zzzzzzzz... 
}

void RTC_Handler()
{
  // Note that these two lines have to be in this order - clear interrupt flag then toggle LED
  RTC->MODE1.INTFLAG.bit.OVF = 1;                // Reset the overflow interrupt flag
  PORT->Group[g_APinDescription[LED_BUILTIN].ulPort].OUTTGL.reg = 1 << g_APinDescription[LED_BUILTIN].ulPin; // Toggle the LED_BUILTIN
}

Martin, when I actually run this on my board the ISR is firing just fine, but at a 500ms period instead of 10s. This is happening with the external and internal clock output.

I also had a question about this RTC_MODE1_PER_PER macro you use to set the period.

RTC->MODE1.PER.reg = RTC_MODE1_PER_PER(9); // Interrupt time 10s: 1Hz/(9 + 1)

Why is the period being increment by 1 to make 10 seconds here?

Ended up being a pretty simple issue. Turns out you have to directly assign the RTCCTRL register as apposed to the OR assignment.

So the code for the internal oscillator now looks something like this:

OSC32KCTRL->OSCULP32K.bit.EN1K = 1;                           // Enable ULPOSC32K 1KHz clock output
OSC32KCTRL->RTCCTRL.reg = OSC32KCTRL_RTCCTRL_RTCSEL_ULP1K;   // Select the 1kHz Ultra Low Power (ULP) for the RTC

while the external looks like this:

OSC32KCTRL->XOSC32K.bit.EN1K = 1;                             // Enable XOSC32K 1kHz clock output
OSC32KCTRL->RTCCTRL.reg = OSC32KCTRL_RTCCTRL_RTCSEL_XOSC1K;  // Select the 1kHz external crystal for the RTC

I'm not familiar enough with Arduino to know what else relies on this clock and what adverse effects this might have on other operations, but for the moment it seems to have solved the problem.

Hi fostac,

I tested the example code on my Adafruit Feather M4 before posting it and it worked just fine, toggling its LED every 10 seconds.

Turns out you have to directly assign the RTCCTRL register as apposed to the OR assignment.

It's only possible to choose either the internal ultra low power 1kHz clock source, or the external crystal 1kHz clock source, but not both at the same time. One of them has to be commented out.

The faster "500ms" LED frequency you're seeing, is because one of the 32kHz RTC clock sources has been accidentally selected instead.

Why is the period being increment by 1 to make 10 seconds here?

The RTC is being clocked from a 1kHz source. The RTC prescaler then further divides signal down by 1024, generating a 1 second tick. The RTC's period (or PER) register is set to 9, because on the next tick after 9, in other words the 10th tick, the RTC returns back to 0 generating an overflow interrupt.

...what else relies on this clock and what adverse effects this might have on other operations...

The selection of the clock sources or the RTC, won't adversly affect any other microcontroller operations.

MartinL:
It's only possible to choose either the internal ultra low power 1kHz clock source, or the external crystal 1kHz clock source, but not both at the same time. One of them has to be commented out.

The faster "500ms" LED frequency you're seeing, is because one of the 32kHz RTC clock sources has been accidentally selected instead.

Right, I have a crystal on my board and was using the XOSC32K with the low power internal clock commented out. It's really strange, when I copy your code directly, switch to the external clock and run it I still get a ~2Hz blink on my board.

Stranger still my board will occasionally get hung up waiting for the SWRST to synchronize on this line.

while (RTC->MODE1.SYNCBUSY.bit.SWRST);

It's spotty whether or not it'll get stuck, are we certain this bit is synchronized? I know there are a few CTRL bits on the SAMD21 that aren't and I'm not sure if that carries over to the 51.

Sorry to keep bothering you with this, I appreciate the help.

Hi fostac

Sorry to keep bothering you with this,...

Your not bothering me at all. Actually, I've made an omission in the example code, as the external oscillator needs to be set to run on standby (RUNSTDBY), to keep it running during sleep mode:

OSC32KCTRL->XOSC32K.bit.RUNSTDBY = 1;    // Set the XOSC32K to run during sleep mode

The internal low power oscillator (OSCULP32K) is by default kept alive during sleep, so it doesn't have a corresponding RUNSTDBY bit.

You're right, there seems to be a problem restarting the RTC after uploading. It's most likely to do with the fact that the RTC is kept running, even during a soft (button) reset. For the moment the workaround is to do a power on reset.

I also tested the current draw on my custom SAMD51 board, (as it's a bit difficult on the Feather M4) and can confirm that the CPU is going into sleep mode.

Hi fostac,

It appears that the sequencing of the registers is important. The SAMD51 datasheet states that the clock must be set-up before the RTC. Moving the RTC disable and software reset commands so that they're executed after the external oscillator set-up, appears to solve the issue.

Here's the revised code:

// Put SAMD51 into deep sleep and RTC interrupts set to trigger every 10s (PER) 
void setup(){
  // Output to D13 to check that the ISR is being called every 10 seconds
  PORT->Group[g_APinDescription[LED_BUILTIN].ulPort].DIRSET.reg = 1 << g_APinDescription[LED_BUILTIN].ulPin;    

// RTC configuration (rtc.h)----------------------------------------------------                                             
         
  //OSC32KCTRL->OSCULP32K.bit.EN1K = 1;                           // Enable ULPOSC32K 1KHz clock output
  //OSC32KCTRL->RTCCTRL.reg = OSC32KCTRL_RTCCTRL_RTCSEL_ULP1K;   // Select the 1kHz Ultra Low Power (ULP) for the RTC  
  OSC32KCTRL->XOSC32K.bit.RUNSTDBY = 1;                         // Set the XOSC32K to run during sleep mode
  OSC32KCTRL->XOSC32K.bit.EN1K = 1;                             // Enable XOSC32K 1kHz clock output 
  OSC32KCTRL->RTCCTRL.reg = OSC32KCTRL_RTCCTRL_RTCSEL_XOSC1K;   // Select the 1kHz external crystal for the RTC

  RTC->MODE1.CTRLA.bit.ENABLE = 0;                     // Disable the RTC
  while (RTC->MODE1.SYNCBUSY.bit.ENABLE);              // Wait for synchronization

  RTC->MODE1.CTRLA.bit.SWRST = 1;                      // Software reset the RTC
  while (RTC->MODE1.SYNCBUSY.bit.SWRST);               // Wait for synchronization
      
  RTC->MODE1.CTRLA.reg |= RTC_MODE1_CTRLA_PRESCALER_DIV1024 |   // Set prescaler to 1024
                          RTC_MODE1_CTRLA_MODE_COUNT16;         // Set RTC to mode 1, 16-bit timer                         
  
  RTC->MODE1.PER.reg = RTC_MODE1_PER_PER(9);                    // Interrupt time 10s: 1Hz/(9 + 1)
  while (RTC->MODE1.SYNCBUSY.bit.PER);                          // Wait for synchronization

// Configure RTC interrupts ----------------------------------------------------
  NVIC_SetPriority(RTC_IRQn, 0);    // Set the Nested Vector Interrupt Controller (NVIC) priority for RTC
  NVIC_EnableIRQ(RTC_IRQn);         // Connect RTC to Nested Vector Interrupt Controller (NVIC)

  RTC->MODE1.INTENSET.reg = RTC_MODE1_INTENSET_OVF;             // Enable RTC overflow interrupts

// Set-up Deep Sleep Mode--------------------------------------------------------
  PM->SLEEPCFG.reg = PM_SLEEPCFG_SLEEPMODE_STANDBY;                         // Set up the SAMD51 to go into STANDBY mode during sleep
  while(PM->SLEEPCFG.bit.SLEEPMODE != PM_SLEEPCFG_SLEEPMODE_STANDBY_Val);   // Wait the STANDBY bitfield to be set

  // On the SAMD51 it's necessary to deactivate the RUNSTDBY (Run Standby) bit on the native USB
  USB->DEVICE.CTRLA.bit.ENABLE = 0;         // Disable the USB peripheral
  while(USB->DEVICE.SYNCBUSY.bit.ENABLE);   // Wait for synchronization
  USB->DEVICE.CTRLA.bit.RUNSTDBY = 0;       // Deactivate run on standby
  USB->DEVICE.CTRLA.bit.ENABLE = 1;         // Enable the USB peripheral
  while(USB->DEVICE.SYNCBUSY.bit.ENABLE);   // Wait for synchronization

// Enable RTC--------------------------------------------------------------------
  RTC->MODE1.CTRLA.bit.ENABLE = 1;                      // Enable the RTC
  while (RTC->MODE1.SYNCBUSY.bit.ENABLE);               // Wait for synchronization
}

void loop()
{
  __DSB();       // Complete outstanding memory operations - not required for SAMD21 ARM Cortex M0+
  __WFI();       // Put the SAMD51 into deep sleep, Zzzzzzzz...  
}

void RTC_Handler()
{
   // Note that these two lines have to be in this order - clear interrupt flag then toggle LED
   RTC->MODE1.INTFLAG.bit.OVF = 1;                // Reset the overflow interrupt flag
   PORT->Group[g_APinDescription[LED_BUILTIN].ulPort].OUTTGL.reg = 1 << g_APinDescription[LED_BUILTIN].ulPin; // Toggle the LED_BUILTIN
}

MartinL:
Hi fostac,

It appears that the sequencing of the registers is important. The SAMD51 datasheet states that the clock must be set-up before the RTC. Moving the RTC disable and software reset commands so that they're executed after the external oscillator set-up, appears to solve the issue.

This seems to have worked perfectly, thanks!