SAM D21 lowest clock speed possible

Hello everyone,

I am trying to get the lowest clock speed possible. I tried using the Arduino LowPower library, but this decreases the current draw from 7 mA to 3.5 mA, and i want to get as low as 15 uA in sleep. I am using a standalone SAM D21 which i program with an atmel programmer using Platform.io. I got this code:


GCLK->GENDIV.reg = GCLK_GENDIV_DIV(48) |         // Divide the 48MHz clock source by divisor 48: 48MHz/48=1MHz
                   GCLK_GENDIV_ID(0);            // Select Generic Clock (GCLK) 0

Which from what i've gathered brings the clockspeed to 1MHz, this also lowers the power draw to 1 mA, a lot better.
Now i am trying to use a different internal clock called " OSCULP32K" to get the clockspeed and current draw even lower. But i can't figure out how. i am a little familiar with C, but it feels like my lack of knowledge is limiting me, does anybody have a piece of example code or maybe even a guide which i can use?

The go-to documents would be:
1: The datasheet, which offers a comprehensive overview of the capabilities of the chip. Note chapter 14 in particular. https://ww1.microchip.com/downloads/en/DeviceDoc/SAM_D21_DA1_Family_DataSheet_DS40001882F.pdf
2: Manufacturers usually offer additional help (involving coding examples, libraries etc) in the form of reference manuals, programming guides etc. For the SAMD21, see this one: SAM D21 lowest clock speed possible Specifically chapter 16, although this relies rather heavily on the free-to-use libraries the manufacturer offers for this chip, which are probably not (all) available in your Arduino framework.

In your case, the general course of action is to determine which flags need to be set in which registers to enable the clock you want to use, tell the core to use that clock and what kind of clock divisors/multipliers need to be set. Then either program those registers directly, or through an API of your choice. Your code snippet suggests you're using the former method, which is fine and probably the quickest/easiest way if you just want to reconfigure the system clock.

I imagine the whole thing will become mighty slow at 32kHz!

Hi @saucyboythesecond

Here's how to switch the CPU/main clock on generic clock 0 (GCLK0) from 48MHz over to the 32.768kHz OSC32KULP clock source.

GCLK0 is then divided by 2, to generate 16.384kHz. The code also has the test option to output GCLK0 on the microcontroller's PA14 port pin.

Here's the program:

// Switch main clock on generic clock 0 (GCLK0) over to the 32.768kHz external crystal clock source
// then divide by 2 to get 16.384kHz
void setup() 
{                  
  GCLK->GENCTRL.reg = //GCLK_GENCTRL_OE |            // Test: enable GCLK output (on a selected pin)
                      GCLK_GENCTRL_IDC |           // Set the duty cycle to 50/50 HIGH/LOW
                      GCLK_GENCTRL_GENEN |         // Enable GCLK0
                      GCLK_GENCTRL_SRC_OSCULP32K | // Set the internal ultra low power 32.768kHz clock source (OSCULP32K)
                      GCLK_GENCTRL_ID(0);          // Select GCLK0
  while (GCLK->STATUS.bit.SYNCBUSY);               // Wait for synchronization  

  GCLK->GENDIV.reg = GCLK_GENDIV_DIV(2) |          // Divide the 32.768kHz clock source by divisor 2: 32.768kHz / 2 = 16.384kHz
                     GCLK_GENDIV_ID(0);            // Select Generic Clock (GCLK) 0
  
  // Test: enable the GCLK0 output on PA14
  //PORT->Group[PORTA].PINCFG[14].bit.PMUXEN = 1;
  //PORT->Group[PORTA].PMUX[14 >> 1].reg |= PORT_PMUX_PMUXE_H;
  
  pinMode(LED_BUILTIN, OUTPUT);                    // Configure the built-in LED pin as an OUTPUT
}

void loop() 
{  
  digitalWrite(LED_BUILTIN, HIGH);                 // Switch the built-in LED on
  delay(10);                                       // Wait for (48MHz / 16.384kHz) * 10ms = 29.27 seconds
  digitalWrite(LED_BUILTIN, LOW);                  // Switch the built-in LED off
  delay(10);                                       // Wait for 29.27 seconds
}
2 Likes

thank you very much for your answer! i managed to get the current draw down from 10 mA to 590 uA, which is very nice. However, this is without applying "STANDBY" which, from what I've gathered in the datasheet, puts the DMAC in lowpower mode and would save another 300 uA and also stop other peripherals saving another +- 200 uA. Do you have any idea how i would achieve this? I can't find anything in the datasheet which i understand i can use.

this is the datasheet i am using: https://ww1.microchip.com/downloads/aemDocuments/documents/MCU32/ProductDocuments/DataSheets/SAM-D21-DA1-Family-Data-Sheet-DS40001882H.pdf
and the info i mentioned can be found in tabel 37-12 and tabel 16-4

I have read a ton of forum posts where you have given solutions which helpt me grasp a few basic concepts, but i notice that i still lack a lot of knowledge on this type of embedded programming and would love to learn more. do you have a programming guide of sorts which you use? or is this all from knowledge you have gathered?

Hi @saucyboythesecond

I just use the SAMD21 datasheet (that you linked to) and the CMSIS (Common Microcontroller Software Interface Standard) register definitions, which on my Windows machine are located at:

C:\Users\Computer\AppData\Local\Arduino15\packages\arduino\tools\CMSIS-Atmel\1.2.0\CMSIS\Device\ATMEL\samd21\include\component

The datasheet does explain how each peripheral works in detail, but it's sometimes difficult to get the big picture of how everything fits together as a system.

Anyway, here's some example code that puts the CPU to standby mode. It's awoken by pulling the A1 pin LOW, then half a second later goes back to sleep again. There's also additional code to allow the serial port to recover as well. By the way, depending on your board you might need to change SerialUSB to Serial:

// Program to test COM port receovery after sleep mode

void setup(void) {
  pinMode(A1, INPUT_PULLUP);                      // Intialise button input pin and activate internal pull-up resistor
  pinMode(LED_BUILTIN, OUTPUT);                   // Initialise the LED_BUILTIN output
  attachInterrupt(A1, dummyFunc, LOW);            // Activate a LOW level interrupt on the button pin
  NVMCTRL->CTRLB.bit.SLEEPPRM = NVMCTRL_CTRLB_SLEEPPRM_DISABLED_Val;    // Prevent the flash memory from powering down in sleep mode
  SCB->SCR |= SCB_SCR_SLEEPDEEP_Msk;              // Select standby sleep mode
  SerialUSB.begin(115200);                        // Intialise the native USB port
  while (!SerialUSB);                             // Wait for the console to open
}

void loop() {
  digitalWrite(LED_BUILTIN, LOW);                 // Turn off the LED
  SerialUSB.println(F("Sleeping Zzzz...wait for button to wake"));  // Send sleep message to the console
  USBDevice.detach();                             // Detach the native USB port
  SysTick->CTRL &= ~SysTick_CTRL_TICKINT_Msk;     // Disable SysTick interrupts
  __DSB();                                        // Ensure remaining memory accesses are complete
  __WFI();                                        // Enter sleep mode and Wait For Interrupt (WFI)
  SysTick->CTRL |= SysTick_CTRL_TICKINT_Msk;      // Enable SysTick interrupts
  USBDevice.attach();                             // Re-attach the native USB port
  digitalWrite(LED_BUILTIN, HIGH);                // Turn on the LED
  delay(500);                                     // Wait for half a second (seems to be necessary to give time for the USB port to re-attach)
  while(!SerialUSB);                              // Wait for the console to re-open
  SerialUSB.println();                            // Add a newline
  SerialUSB.println(F("Button depress...waking up")); // Send a wake up message
  delay(500);                                     // Wait half a second
}

void dummyFunc() {}                               // Dummy interrupt service routine
1 Like

okay so I've used the info you have given me and i am trying to figure out how this all works. Here is what i've learned (i think):
So in the datasheet there will be different peripherals mentioned, they usually have a corresponding header file in the location you mentioned. the datasheet mentions the union/structure where you can find the command. Here you can you choose between bit access for using pre defined values or register access for using custom values. for example in the code above you use bit access in the CTRLB structure to use the value stored in SLEEPPRM to send it to the register "NVMCTRL_CTRLB_SLEEPPRM_DISABLED_Val".

if this is all sound logic (which it probably isn't) then i have the following question: how do you use the .reg function exactly? in the code you posted regarding the clock speed, it looks like you separate them using "or" (vertical bar) operators to access multiple registers at once, but what do you do with them ones you have accessed them? or is accessing the register alone enough to cause a reaction? i see a lot of bit shifting when looking at the header file, so i assume this only happens when the register or definition or whatever is called.

Hi @saucyboythesecond

The SAMD21's CMSIS register definintions allow you to access the registers in a number of different ways. For example lets take the TCC0 timer's CTRLA register:

Register ".reg" Suffix

The register definitions allow the entire register to be accessed using the .reg suffix.

For instance, to read the entire TCC0's CTRLA register and display it on the console as a hexidecimal number:

Serial.println(TCC0->CTRLA.reg, HEX);

However, if you wish to set or clear individual bits then it's necessary to use the OR ('|') and AND ('&') bitwise operators.

To set a bit or bitfield (more than one bit), it's necessary to do what's called a read-modify-write operation. Setting a bit/bitfield involves reading the current value of the register, bitwise ORing it with a mask of the bit/bitfield, then writing the result back to the register. This is achieved using the assignment by bitwise OR operator ("|="):

TCC0->CTRLA.reg |= TCC_CTRLA_PRESCALER_DIV16;

Note: this requires all of the TCC_CTRLA_PRESCALER_DIV16 bitfield to be cleared to 0 prior to writing. In other words, if any of the bitfields bits are already set to 1, they won't be cleared to 0.

It's also possible to set multiple bitfields in one go with the bitwise OR:

TCC0->CTRLA.reg |= TCC_CTRLA_RUNSTDBY | TCC_CTRLA_PRESCALER_DIV16;

To clear certain bits/bitfields with a read-modify-write operation, use the assignment by bitwise AND operator ("&=") together with the inverse (a.k.a bitwise NOT operator ('~')) of the bit/bitfield mask. In this case, to disable the TCC0 timer:

TCC0->CTRLA.reg &= ~TCC_CTRLA_ENABLE;  // Disable TCC0

Register ".bit" suffix

The register definitions also allow the individual register bits/bitfields to be accessed using the .bit suffix. This handy notaion dispenses with the assignment by bitwise operators.

For example to enable the TCC0 timer:

TCC0->CTRLA.bit.ENABLE = 1;  // Enable TCC0

or alternatively:

TCC0->CTRLA.bit.ENABLE = TCC_CTRLA_ENABLE_Pos;  // Enable TCC0

Conversely to disable TCC0:

TCC0->CTRLA.bit.ENABLE = 0;  // Disable TCC0

Or set a bitfield:

TCC0->CTRLA.bit.PRESCALER = TCC_CTRLA_PRESCALER_DIV16_Val;

Note: unlike the ".reg" notation, the ".bit" suffix allows the code to both set and clear the relevant bits.

Usually I use the ".reg" notation during initialisation when bit/bitfields are already cleared to 0, as this enables multiple bits/bitfields to be bitwise ORed together in one go. I normally employ the ".bit" suffix during operation, since it automatically set/clears the relevant bit/bitfield bits as required.

Thank you for the clear explanation! Does that mean that for example in:

GCLK->GENCTRL.reg = //GCLK_GENCTRL_OE |          // Test: enable GCLK output (on a selected pin)
                      GCLK_GENCTRL_IDC |           // Set the duty cycle to 50/50 HIGH/LOW
                      GCLK_GENCTRL_GENEN |         // Enable GCLK0
                      GCLK_GENCTRL_SRC_OSCULP32K | // Set the internal ultra low power 32.768kHz clock source (OSCULP32K)
                      GCLK_GENCTRL_ID(0);          // Select GCLK0

all the mentioned register definitions are set to 1 instead of 0? or is it the bit within the definition? and if that's the case, why can you use = instead of |=? It kind of looks like you just read the definitions and that's it as it also looks quite similar to Serial.println(TCC0->CTRLA.reg, HEX);

EDIT: i have experimented a bit and am unable to use a button to interrupt anything.
Here is my current code:

#include <Arduino.h>

#define LEDPIN 12
#define BUTTONPIN 14 //PA02

volatile bool awake = true;

void dummyFunc();

void setup() {
  Serial1.begin(9600);
  pinMode(LEDPIN, OUTPUT);
  pinMode(BUTTONPIN, INPUT);
  digitalWrite(LEDPIN, LOW);
}

void loop() {
  digitalWrite(LEDPIN, LOW);
  Serial1.print(digitalRead(BUTTONPIN));
  attachInterrupt(digitalPinToInterrupt(BUTTONPIN), dummyFunc, CHANGE);
  while (!awake) Serial1.print(digitalRead(BUTTONPIN));
  detachInterrupt(digitalPinToInterrupt(BUTTONPIN));
  digitalWrite(LEDPIN, HIGH);
  Serial1.println("dummyFunc triggered");
  awake = false;
}

void dummyFunc() {
  awake = true;
}

Every post i have come across uses this method to program an interrupt function but i just can't get it to work. i also tried setting EIC->CTRL.bit.ENABLE = 1; but i saw somewhere you need to enable clocks and stuff, so then i managed to make this piece of setup code:

// Initialize EIC and configure pin for interrupt
    PM->APBAMASK.reg |= PM_APBAMASK_EIC; // Enable EIC clock

    // Set up pin multiplexing to connect the pin to the EIC peripheral
    PORT->Group[PORTA].PINCFG[2].bit.PMUXEN = 1; // Enable PMUX for PA02
    PORT->Group[PORTA].PMUX[2 >> 1].reg |= PORT_PMUX_PMUXE(0); // Set PMUX to EIC

    // Configure EIC for the specific pin and trigger condition
    EIC->CONFIG[1].reg = EIC_CONFIG_SENSE1_RISE | EIC_CONFIG_FILTEN1; // Rising edge detection with filter

    // Enable the specific EIC channel for the pin
    EIC->CTRL.reg |= EIC_CTRL_ENABLE; // Enable EIC
    EIC->INTENSET.reg = EIC_INTENSET_EXTINT(1 << 2); // Enable interrupt for EIC channel 2 (PA02)

and now i get this error on one of my chips:

at91samd21g16
Warn : Connecting DP: stalled AP operation, issuing ABORT
Error: Failed to read memory and, additionally, failed to find out where
Warn : target at91samd21g16.cpu examination failed
Warn : Connecting DP: stalled AP operation, issuing ABORT
Warn : Connecting DP: stalled AP operation, issuing ABORT
Warn : Connecting DP: stalled AP operation, issuing ABORT
Warn : Connecting DP: stalled AP operation, issuing ABORT
Warn : Connecting DP: stalled AP operation, issuing ABORT
Warn : Connecting DP: stalled AP operation, issuing ABORT
Error: [at91samd21g16.cpu] DP initialisation failed
embedded:startup.tcl:1516: Error: ** Unable to reset target **
in procedure 'program'
in procedure 'program_error' called at file "embedded:startup.tcl", line 1553
at file "embedded:startup.tcl", line 1516
*** [upload] Error 1

i am using a barebone SAMD21 which i am programming using platform io. the pins i used are defined in a separate variant.cpp file. i am trying to figure out how to get the interrupt working. when i manage that i want to look into the different types of sleepmode. but i can't get the interrupt pins to work. The pinmode serial read gives me a 1 when i press the button and a 0 when i release it, just as it should, but the interrupt isn't triggered.

Hi @saucyboythesecond

The code...

GCLK->GENCTRL.reg = //GCLK_GENCTRL_OE |          // Test: enable GCLK output (on a selected pin)
                      GCLK_GENCTRL_IDC |           // Set the duty cycle to 50/50 HIGH/LOW
                      GCLK_GENCTRL_GENEN |         // Enable GCLK0
                      GCLK_GENCTRL_SRC_OSCULP32K | // Set the internal ultra low power 32.768kHz clock source (OSCULP32K)
                      GCLK_GENCTRL_ID(0);          // Select GCLK0

....takes advantage of the fact that this register is reset to 0 during power-up. The binary value of the bitfields are bitwise ORed together and applied/written to the GENCTRL register.

When I uses a button, I normally set the button input pin to INPUT_PULLUP with pinMode() and the set attachInterrupt() function edge triggering to LOW. Both these lines are placed in the setup() portion of the code, (rather than loop()). It shouldn't be necessary to continuously attach and detach the interrupt each time.

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.