Skip to content

Instantly share code, notes, and snippets.

@nonsintetic
Last active September 9, 2024 09:39
Show Gist options
  • Save nonsintetic/ad13e70f164801325f5f552f84306d6f to your computer and use it in GitHub Desktop.
Save nonsintetic/ad13e70f164801325f5f552f84306d6f to your computer and use it in GitHub Desktop.
SAMD21 Arduino Timer Example
/*
* This sketch illustrates how to set a timer on an SAMD21 based board in Arduino (Feather M0, Arduino Zero should work)
* It should generate a 1Hz square wave as it is (thanks richdrich for the suggestion)
* Some more info about Timer Counter works can be found in this article:
* http://www.lucadavidian.com/2017/08/08/arduino-m0-pro-il-sistema-di-clock/
* and in the datasheet: http://ww1.microchip.com/downloads/en/DeviceDoc/SAM_D21_DA1_Family_DataSheet_DS40001882F.pdf
*/
uint32_t sampleRate = 1000; //sample rate in milliseconds, determines how often TC5_Handler is called
#define LED_PIN 13 //just for an example
bool state = 0; //just for an example
void setup() {
pinMode(LED_PIN,OUTPUT); //this configures the LED pin, you can remove this it's just example code
tcConfigure(sampleRate); //configure the timer to run at <sampleRate>Hertz
tcStartCounter(); //starts the timer
}
void loop() {
//tcDisable(); //This function can be used anywhere if you need to stop/pause the timer
//tcReset(); //This function should be called everytime you stop the timer
}
//this function gets called by the interrupt at <sampleRate>Hertz
void TC5_Handler (void) {
//YOUR CODE HERE
if(state == true) {
digitalWrite(LED_PIN,HIGH);
} else {
digitalWrite(LED_PIN,LOW);
}
state = !state;
// END OF YOUR CODE
TC5->COUNT16.INTFLAG.bit.MC0 = 1; //Writing a 1 to INTFLAG.bit.MC0 clears the interrupt so that it will run again
}
/*
* TIMER SPECIFIC FUNCTIONS FOLLOW
* you shouldn't change these unless you know what you're doing
*/
//Configures the TC to generate output events at the sample frequency.
//Configures the TC in Frequency Generation mode, with an event output once
//each time the audio sample frequency period expires.
void tcConfigure(int sampleRate)
{
// select the generic clock generator used as source to the generic clock multiplexer
GCLK->CLKCTRL.reg = (uint16_t) (GCLK_CLKCTRL_CLKEN | GCLK_CLKCTRL_GEN_GCLK0 | GCLK_CLKCTRL_ID(GCM_TC4_TC5)) ;
while (GCLK->STATUS.bit.SYNCBUSY);
tcReset(); //reset TC5
// Set Timer counter 5 Mode to 16 bits, it will become a 16bit counter ('mode1' in the datasheet)
TC5->COUNT16.CTRLA.reg |= TC_CTRLA_MODE_COUNT16;
// Set TC5 waveform generation mode to 'match frequency'
TC5->COUNT16.CTRLA.reg |= TC_CTRLA_WAVEGEN_MFRQ;
//set prescaler
//the clock normally counts at the GCLK_TC frequency, but we can set it to divide that frequency to slow it down
//you can use different prescaler divisons here like TC_CTRLA_PRESCALER_DIV1 to get a different range
TC5->COUNT16.CTRLA.reg |= TC_CTRLA_PRESCALER_DIV1024 | TC_CTRLA_ENABLE; //it will divide GCLK_TC frequency by 1024
//set the compare-capture register.
//The counter will count up to this value (it's a 16bit counter so we use uint16_t)
//this is how we fine-tune the frequency, make it count to a lower or higher value
//system clock should be 1MHz (8MHz/8) at Reset by default
TC5->COUNT16.CC[0].reg = (uint16_t) (SystemCoreClock / sampleRate);
while (tcIsSyncing());
// Configure interrupt request
NVIC_DisableIRQ(TC5_IRQn);
NVIC_ClearPendingIRQ(TC5_IRQn);
NVIC_SetPriority(TC5_IRQn, 0);
NVIC_EnableIRQ(TC5_IRQn);
// Enable the TC5 interrupt request
TC5->COUNT16.INTENSET.bit.MC0 = 1;
while (tcIsSyncing()); //wait until TC5 is done syncing
}
//Function that is used to check if TC5 is done syncing
//returns true when it is done syncing
bool tcIsSyncing()
{
return TC5->COUNT16.STATUS.reg & TC_STATUS_SYNCBUSY;
}
//This function enables TC5 and waits for it to be ready
void tcStartCounter()
{
TC5->COUNT16.CTRLA.reg |= TC_CTRLA_ENABLE; //set the CTRLA register
while (tcIsSyncing()); //wait until snyc'd
}
//Reset TC5
void tcReset()
{
TC5->COUNT16.CTRLA.reg = TC_CTRLA_SWRST;
while (tcIsSyncing());
while (TC5->COUNT16.CTRLA.bit.SWRST);
}
//disable TC5
void tcDisable()
{
TC5->COUNT16.CTRLA.reg &= ~TC_CTRLA_ENABLE;
while (tcIsSyncing());
}
@lutzray
Copy link

lutzray commented Feb 7, 2021

Thanks for the example! Is this a revised version of this one on svn.larosterna.com with better comments? Just curious.

May I suggest this edit for the first line of code comment: "sample rate in millihertz" rather than "sample rate in milliseconds" .

@netless-ww
Copy link

netless-ww commented Feb 8, 2021

Agree, the variable "sampleRate" is actually a frequency, but not quite exactly in millihertz.

If you want accurate timing in millihertz then you need to account for the 1024 pre scaling factor in GCLK.

The actual frequency in millihertz generated by "sampleRate" is

Freq = sampleRate/ (1000 * 1024) mHz

My suggestion to try and avoid all this pre scaler confusion would be simple modify the first couple of code lines as

=============================================================================

uint32_t samplePeriod = 1000; //Sample period in milliseconds, determines how often TC5_Handler is called
//Note: The maximum samplePeriod = 1400 mS beyond which the TC5 register
//will overflow causing erroneous timing. Shorter sample periods (high frequencies)
//will not cause any problems.
uint32_t sampleRate = (1000 * 1024)/samplePeriod;

=============================================================================

Don't forget that the SAMD21 clock needs to see a USB data connection to give accurate timing. If you start it up without a USB connection, say on battery power, then you will need to find the required sampleRate parameter for your required timing using a CRO.

Those points taken into account, this is a really nice piece of useful code for those of us using the SAMD21

@nonsintetic
Copy link
Author

nonsintetic commented Feb 8, 2021

Thanks for the example! Is this a revised version of this one on svn.larosterna.com with better comments? Just curious.

May I suggest this edit for the first line of code comment: "sample rate in millihertz" rather than "sample rate in milliseconds" .

Think it's more like the other way round. I put this together from some comments on the Arduino forum (I think) ages ago while trying to make a RFM95 walkie talkie :) It's pieced together from several suggestions, but I don't remember what post it was.

@DFMoller
Copy link

DFMoller commented Sep 5, 2022

Thank you for this piece of code, its been really helpful to me to understand the timers on my Nano 33 IoT.

However, I am trying to find out of I can implement an interrupt frequency faster than 1kHz (period less than 1ms) and would really appreciate it if somebody could clarify what the fastest interrupt frequency is that one can implement on the 48MHz Cortex-M0 32-bit SAMD21 used by the Nano 33 IoT. Are there limitations to going faster than 1kHz?

For context, I am sampling AC signals and would like to be able to sample a bit faster than 1kHz.

@nonsintetic
Copy link
Author

nonsintetic commented Sep 5, 2022

Thank you for this piece of code, its been really helpful to me to understand the timers on my Nano 33 IoT.

However, I am trying to find out of I can implement an interrupt frequency faster than 1kHz (period less than 1ms) and would really appreciate it if somebody could clarify what the fastest interrupt frequency is that one can implement on the 48MHz Cortex-M0 32-bit SAMD21 used by the Nano 33 IoT. Are there limitations to going faster than 1kHz?

For context, I am sampling AC signals and would like to be able to sample a bit faster than 1kHz.

On a variant with a different system clock frequency the calculations will be different. This code was devised for an 8Mhz clock. This is the line that would be affected by a different clock speed:

//system clock should be 1MHz (8MHz/8) at Reset by default
 TC5->COUNT16.CC[0].reg = (uint16_t) (SystemCoreClock / sampleRate);

With a faster clock the counter would count faster, so you'd get a bigger maximum frequency. All this does is set a counter to increase by one every clock cycle and set a number as the target. When the counter gets to the set target, the interrupt gets called and the counter starts from zero.

You can also change line 62 to divide the clock by a different number. Dividing by less will count faster. As it is in the gist it divides by 1024.

@DFMoller
Copy link

DFMoller commented Sep 5, 2022

Thanks for the fast reply! I'll do some testing.

@nalex4711
Copy link

nalex4711 commented Feb 8, 2023

Hello there
Actually , there is this more accurate view of the TC5->COUNT16.CC[0].reg setting, i hope more accurate. In the SAMD21 data sheet you can read in the TC section 30.10.13 Channel x Compare/Capture Value, 16-bit Mode, the following sentence: In Match frequency (MFRQ) or Match PWM (MPWM) waveform operation (CTRLA.WAVEGEN), the CC0 register is used as a period register. This is here the case, that means : it counts period units. The default clock generator for the TC peripheral is the XOSC oscillator, i 'm not sure which is that, but it definitely drives the bus with 4MHz (i measured its actual value on a Zero). Let's assign to TC the more accurate oscillator XOSC32K and divide by a DIV1 prescaler, and we have a clock frequency for the TC5 of f = 32768/4 = 8.192 kHz (is divided by 4 by default). The period for that frequency is 1s/32768 = 122.1 us, that means every single count lasts 122.1 us. Now the CC[0] register is a 16bit value, at maximum 65535. Then if we clock pin11 instead of the the LED for 1 count x 122.1 we get a pulsed signal of 50% duty cycle with f=8192 Hz. The declaration of the quotient SystemCoreClock / sampleRate has in my opinion therefore no physical meaning, either the SystemCoreClock value has anything to do with the physical state of the driving clock with the TC5 timer configured in this way. SystemCoreClock is just the constant number 48e6 which is the core clock frequency indeed and the value sampleRate has no possible meaning at all. You can put in the CC[0] register instead any 16bit value between 1 and 65535 and expect theoretically a time interval of 1x 122.1 us up to 65535 x 122.1 us = 8 s. I don't know why the actual period measured is exactly the half of it 4s, i suppose the interrupt mode is CHANGING, not RISING or FALLING. Anyway the code behaves well and solves the service of calling an Interrupt Service Routine ISR by an externally delivered pulse.

I changed the code, corrected it at some placed and added (hopefully) useful comments.

You can fetch the file from my account.

Thanks

@seby42
Copy link

seby42 commented Sep 9, 2024

Doesn't seems to work with a ATSAMD21J18A, strange...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment