Skip to content

Instantly share code, notes, and snippets.

@nonsintetic
Last active April 1, 2024 11:52
Show Gist options
  • Star 29 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • 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());
}
@csmith105
Copy link

Could you comment on what TC5->COUNT16.INTFLAG.bit.MC0 = 1; //don't change this, it's part of the timer code does?

@richdrich
Copy link

As posted you are setting the counter to e.g. 64M / 10 which is >65535 and hence basically random.

If you set e.g. SampleRate=1000 and TC_CTRLA_PRESCALER_DIV1024 it will (on a Zero) blink at about 1Hz, which is more usable.

@michaelmegliola
Copy link

thanks for the post, this is helpful.

@funwithbots
Copy link

Compiling for a Feather M0, I get a clean compile on your code AS IS but when I try to add it to my own code, the compiler gives me:
'TC5' was not declared in this scope
I moved everything after the loop() function to tc5timer.cpp and created tc5timer.h to predefine the needed functions. tc5timer.h was included in the main sketch. Not sure where I messed this one up.

@danieldeesousa
Copy link

@funwithbots
Make sure that your code before the TC5 deceleration has semi colons. Same thing happened to me.

@gergelytakacs
Copy link

Thank you for this example, helped a lot. Implemented something similar in a timing routine for feedback control, see the AutomationShield project.

@mjunior-fitec
Copy link

As posted you are setting the counter to e.g. 64M / 10 which is >65535 and hence basically random.

If you set e.g. SampleRate=1000 and TC_CTRLA_PRESCALER_DIV1024 it will (on a Zero) blink at about 1Hz, which is more usable.

Very well observed!
It took me quite a while to realize what the actual blinking frequency would be in the example. This idea helps a lot to understand and make the example usable.
I compiled it as an independent module on my MKRWAN1300, and its working fine, so far!

@wbphelps
Copy link

FWIW: Writing a 1 to INTFLAG.bit.MC0 clears the interrupt so that it will run again. This is what is being done in this line:
TC5->COUNT16.INTFLAG.bit.MC0 = 1;
It would be helpful if the comment was changed to indicate this.

@nonsintetic
Copy link
Author

done

FWIW: Writing a 1 to INTFLAG.bit.MC0 clears the interrupt so that it will run again. This is what is being done in this line:
TC5->COUNT16.INTFLAG.bit.MC0 = 1;
It would be helpful if the comment was changed to indicate this.

done

@stokesl
Copy link

stokesl commented Mar 7, 2020

Thank you for this, it is very well done. I am curious though, that when I change the sampleRate either up or down (say to 2000 or 500) my initial thought was it would either half or double the time the light blinks, however I am not finding that to be the case. How exactly could that be accomplished?

@MIKE1106
Copy link

MIKE1106 commented May 9, 2020

Thanks very much. I have been looking to port some code from a Nano to the Nano IoT 33 and want to know if the code could be expected to work on the Nano IoT 33 which is SAMD21G18A microcontroller.

@netless-ww
Copy link

netless-ww commented Jul 4, 2020

Actually

uint32_t sampleRate = 1024;

Will give you exactly a 1 sec interrupt interval generating a 0.5 Hz square wave according to my, admittedly cheap, Chinese CRO.

Not sure why, but 2^10 does have a ring of truth about it - happy for any feedback explanation.

Thanks for a nice piece of code, avoiding the need for an RTC to control accurate 1 Hz sampling.

@steevjo
Copy link

steevjo commented Jul 7, 2020

Just found this example code - very helpful -thank you.

To comment above - yes 1024 generates exactly a 1 hz interrupt - timer is counting at a rate of systemclock/1024(prescaler) counts/sec and interrupt will be generated every 1024 counts - so systemclock counts will trigger the interrupt - to reach systemclock counts takes one second - by definition.

Question - the code uses loads of predefined 'labels', e.g. TC5->COUNT16.CTRLA.reg |= TC_CTRLA_MODE_COUNT16;

Where are all these definitions documented? I'm still finding my way around everything arduino - there's lots - would appreciate being pointed in the right direction. Thanks

@nonsintetic
Copy link
Author

Question - the code uses loads of predefined 'labels', e.g. TC5->COUNT16.CTRLA.reg |= TC_CTRLA_MODE_COUNT16;

Where are all these definitions documented? I'm still finding my way around everything arduino - there's lots - would appreciate being pointed in the right direction. Thanks

The defines are from the Arduino Core written for the SAMD21 (https://github.com/arduino/ArduinoCore-samd). They 'translate' the registers and various combinations of register settings from the datasheet into a more human readable format for programming. Go to that repository and use the search function to find where they are defined, some extra comments might be present.

To see what they actually do you have to check the datasheet for the SAMD21G18. You won't find the EXACT tags most of the time because some are combinations of settings (TC_CTRLA_MODE_COUNT16 means something Timer Counter, for CTRLA (a register), set MODE to COUNT16), check around section 19.8.2 Control - MODE1 (part of the chapter on the real time counter) of the SAMD21 datasheet to see more.

@netless-ww
Copy link

Another SAMD21 Quirk

As I commented above a sample rate of 1024 generates exactly a 1 sec tick - When the SAMD21 is powered from the micro USB connector with a data connection, eg to a PC.

However, when powered with 5V into the BAT pin, or even 5V into the micro USB from a power supply a lower frequency 0.970 Hz [1.031 sec] tic is generated.

Both the USB & BAT connections feed into the same 3V3 voltage regulator with rock solid 3V3 output - so voltage differences do not appear to be the issue - leaving the USB data connection as the issue, as discussed above.

Can anyone confirm my observations above & shed any light on what is causing this timing issue on the SAMD21 between micro USB/with a data connection vs 5V into either the micro USB without a data connection or 5V into the BAT pin?

Note: there is so Serial connection being set up in the user code.

@voloved
Copy link

voloved commented Aug 29, 2020

at when I change the sampleRate either up or down (say to 2000 or 500) my in

TC5->COUNT16.CTRLA.reg |= TC_CTRLA_PRESCALER_DIV1024 | TC_CTRLA_ENABLE; //it will divide GCLK_TC frequency by 1024
TC5->COUNT16.CC[0].reg = (uint16_t) (SystemCoreClock / sampleRate);

In the first line, the GCLK is prescaled by 1024. So say that you have a 48MHz system core clock, it's now counts 48MHz/1024 = 46,875 times per second.
To have this thing trigger every second, you set sampleRate to 1024 so it will count to 46,875, which is the counts per second in the previous sentence.
To trigger every 100ms, you'd want to trigger every 4,688 times, or 48MHz/ (1024 * 10).

So ms wanted to trigger the interrupt is:
(X * SystemCoreClock) / (1,024 * 1000) where
X is the ms wanted
1024 is due to the prescaler in this code
1000 is for 1000ms in a second

@fe64970103
Copy link

Thanks for the example. It's a very concise starting point for custom project alterations.

@netless-ww
Copy link

You need to be a little careful with these timing changes.

A sample rate of 1024 certainly gives you a 1Hz tick - As long as you have a USB data connection back to your PC - see my earlier post relating to this SAMD21 quirk.

I agree with voloved's logic above, the clock count appears to be set to SystemCoreClock/SampleRate. You would imagine that if you halved the sample rate then this would double the count and give a 2 sec tick??

BUT NO My CRO shows a 0.6 sec tick.
THE REASON - 48,000,000/512 = 93,750 causing an overflow in the uint_16 TC5 register.
With the max size of uint_16 at 65,535 the lowest SampleRate you can have is 48,000,000/65,535=733 giving a maximum 1.4 sec tick.

Moving in the other direction, with SampleRate defined as uint_32, there is no problem with higher frequencies - easily works down to 100 uS tick - you will probably run into the digital write speed in your Arduino code.

For those wanting to calculate the required SampleRate [with the specified 1024 pre scaler] the following formula may be handy

SampleRate = (1000 * 1024)/mS

@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

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