Arduino Zero SAM(D21) hardware counter as simple input counter intialization?

Hello everybody,

I am tinkering with some new board with SAMD21G MCUs on Arduino Zero compatible boards. I am trying to use its internal hardware counter in basic counter mode. I somewhere heard that it could work even in power-save mode but thats not important for now.

I need to count absolutely random logical pulses in speed up to 7000 counts per second (so basic 16bit counter should be fine). I found a lot of resources about Timer/Counter hardware in SAM architecture. I guess that basic mechanics are same in whole SAMxxx family.

Sadly all I found are examples/snippets related to slightly different things: - generating PWM on LED:

//source: https://www.digikey.com/eewiki/display/microcontroller/Getting+Started+with+the+SAM+D21+Xplained+Pro+without+ASF
#include "sam.h"

#define LED0 PORT_PB30;

void init_TC3();
void enable_interrupts();

// Global error flag for TC3
volatile uint8_t TC3_error = 0;

int main(void)
{
    SystemInit(); // Initialize the SAM system
    enable_interrupts();
    init_TC3();
     
    // Configure LED0 as output
    REG_PORT_DIRSET1 = LED0;

    while (1)
    {
         
    }
}


void init_TC3()
{
    /* Configure Timer/Counter 3 as a timer to blink LED0 */
    // Configure Clocks
    REG_GCLK_CLKCTRL = GCLK_CLKCTRL_CLKEN | GCLK_CLKCTRL_GEN_GCLK0 | GCLK_CLKCTRL_ID_TCC2_TC3;
    REG_PM_APBCMASK |= PM_APBCMASK_TC3; // Enable TC3 bus clock
     
    // Configure TC3 (16 bit counter by default)
    REG_TC3_CTRLA |= TC_CTRLA_PRESCALER_DIV8;
     
    // Enable interrupts
    REG_TC3_INTENSET = TC_INTENSET_OVF | TC_INTENSET_ERR;
     
    // Enable TC3
    REG_TC3_CTRLA |= TC_CTRLA_ENABLE;
    while ( TC3->COUNT16.STATUS.bit.SYNCBUSY == 1 ){} // wait for TC3 to be enabled
}


void TC3_Handler()
{   
    // Overflow interrupt triggered
    if ( TC3->COUNT16.INTFLAG.bit.OVF == 1 )
    {
        REG_PORT_OUTTGL1 = LED0;
        REG_TC3_INTFLAG = TC_INTFLAG_OVF;
    }
     
    // Error interrupt triggered
    else if ( TC3->COUNT16.INTFLAG.bit.ERR == 1 )
    {
        TC3_error = 1;
        REG_TC3_INTFLAG = TC_INTFLAG_ERR;
    }
}


void enable_interrupts()
{
    NVIC_EnableIRQ( TC3_IRQn );
}

So it seems that I should make the code completely from scratch. Sad is there is no basic draft how to configure everything to work in just simple counting mode.

I also can add some external I2C counter like DS1672, but still it seems to me odd and completely wrong since hardware itself should be able to do it without anything extra.

Can anybody point me to some additional resources or code that could help me? Or at lease some basic outline of sequence of which should be how configured?

Hi Helium328PU,

Your options are to either simply count the number of incoming pulses using the attachInterrupt() function and an interrupt service routine (ISR), or alternatively route the pulses through the SAMD21's External Interrupt Controller (EIC) and on to a TC timer using the Event System. The TC timer is configured to count the incoming pulses. The Event System is a 12-channel highway for peripheral-to-peripheral communications without CPU intervention. The latter option has the advantage that it isn't necessary to call the ISR each time a pulse is received; the whole process occurs without any CPU intervention, apart from reading the timer's COUNT register.

Here's some example code that takes the incoming pulses on D12 (on the Arduino Zero) a.k.a port pin PA19 and routes these through the EIC to timer TC4 via the Event System. TC4 is configured to count the incoming pulses and is set to 32-bit mode in conjunction with TC5. The number or counts is output to the console every second:

// Setup TC4 in 32-bit mode to count incoming pulses on digital pin D12 using the Event System
void setup()
{
  // Serial Communication /////////////////////////////////////////////////////////////////
  
  SerialUSB.begin(115200);                        // Send data back on the Zero's native port
  while(!SerialUSB);                              // Wait for the SerialUSB port to be ready
  
 // Generic Clock /////////////////////////////////////////////////////////////////////////

  GCLK->CLKCTRL.reg = GCLK_CLKCTRL_CLKEN |        // Enable the generic clock...
                      GCLK_CLKCTRL_GEN_GCLK0 |    // On GCLK0 at 48MHz
                      GCLK_CLKCTRL_ID_TC4_TC5;    // Route GCLK0 to TC4 and TC5
  while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization

  // Port Configuration ///////////////////////////////////////////////////////////////////

  // Enable the port multiplexer on digital pin D12 (port pin PA19)
  PORT->Group[g_APinDescription[12].ulPort].PINCFG[g_APinDescription[12].ulPin].bit.PMUXEN = 1;
  // Set-up the pin as an EIC (interrupt) peripheral on D12
  PORT->Group[g_APinDescription[12].ulPort].PMUX[g_APinDescription[12].ulPin >> 1].reg |= PORT_PMUX_PMUXO_A;

  // External Interrupt Controller (EIC) ///////////////////////////////////////////////////

  EIC->EVCTRL.reg |= EIC_EVCTRL_EXTINTEO3;                                // Enable event output on external interrupt 3 (D12)
  EIC->CONFIG[0].reg |= EIC_CONFIG_SENSE3_HIGH;                           // Set event detecting a HIGH level on interrupt 3
  EIC->INTENCLR.reg = EIC_INTENCLR_EXTINT3;                               // Disable interrupts on interrupt 3
  EIC->CTRL.bit.ENABLE = 1;                                               // Enable the EIC peripheral
  while (EIC->STATUS.bit.SYNCBUSY);                                       // Wait for synchronization

  // Event System //////////////////////////////////////////////////////////////////////////

  PM->APBCMASK.reg |= PM_APBCMASK_EVSYS;                                  // Switch on the event system peripheral

  EVSYS->USER.reg = EVSYS_USER_CHANNEL(1) |                               // Attach the event user (receiver) to channel 0 (n + 1)
                    EVSYS_USER_USER(EVSYS_ID_USER_TC4_EVU);               // Set the event user (receiver) as timer TC4
  
  EVSYS->CHANNEL.reg = EVSYS_CHANNEL_EDGSEL_NO_EVT_OUTPUT |               // No event edge detection
                       EVSYS_CHANNEL_PATH_ASYNCHRONOUS |                  // Set event path as asynchronous
                       EVSYS_CHANNEL_EVGEN(EVSYS_ID_GEN_EIC_EXTINT_3) |   // Set event generator (sender) as external interrupt 3
                       EVSYS_CHANNEL_CHANNEL(0);                          // Attach the generator (sender) to channel 0                                 
  
  // Timer Counter TC4 /////////////////////////////////////////////////////////////////////

  TC4->COUNT32.EVCTRL.reg |= TC_EVCTRL_TCEI |              // Enable asynchronous events on the TC timer
                             TC_EVCTRL_EVACT_COUNT;        // Increment the TC timer each time an event is received

  TC4->COUNT32.CTRLA.reg = TC_CTRLA_MODE_COUNT32;          // Configure TC4 together with TC5 to operate in 32-bit mode
                      
  TC4->COUNT32.CTRLA.bit.ENABLE = 1;                       // Enable TC4
  while (TC4->COUNT32.STATUS.bit.SYNCBUSY);                // Wait for synchronization

  TC4->COUNT32.READREQ.reg = TC_READREQ_RCONT |            // Enable a continuous read request
                             TC_READREQ_ADDR(0x10);        // Offset of the 32-bit COUNT register
  while (TC4->COUNT32.STATUS.bit.SYNCBUSY);                // Wait for synchronization
}

void loop()
{
  SerialUSB.println(TC4->COUNT32.COUNT.reg);               // Output the results
  delay(1000);                                             // Wait for 1 second
}

MartinL: Your options are to either simply count the number of incoming pulses using the attachInterrupt() function and an interrupt service routine (ISR),

This is actually the easyest method I know from my AVR times. But even on AVR this makes problem if you have longer SPI transfers (e.g. on SD card) and these uncontrollable interrupts are messing into it.. So thats the reason I have asked. On AVR using hardware counter is pretty easy compared to ARM. Of course I could use even polling MCU to wait for pulses for a certain time but this could be really ridiculous for such powerfull MCU :)

MartinL: Here's some example code that takes the incoming pulses on D12 (on the Arduino Zero) a.k.a port pin PA19 and routes these through the EIC to timer TC4 via the Event System. TC4 is configured to count the incoming pulses and is set to 32-bit mode in conjunction with TC5. The number or counts is output to the console every second:

This is absolutely perfect, thank you a lot ! This works as I desired, completely out of CPU core intervention or MCU interrupts. Added karma to you, really appretiate it because I expected to make whole code by myself!

I guess I can change input to any other pin in these lines (with according MUX) - e.g. A0 , or are there any limitations on which pins it cam be routed?

  // Enable the port multiplexer on digital pin D12 (port pin PA19)
  PORT->Group[g_APinDescription[12].ulPort].PINCFG[g_APinDescription[12].ulPin].bit.PMUXEN = 1;
  // Set-up the pin as an EIC (interrupt) peripheral on D12
  PORT->Group[g_APinDescription[12].ulPort].PMUX[g_APinDescription[12].ulPin >> 1].reg |= PORT_PMUX_PMUXO_A;

And one more tiny question, what is appropriate solution to reset TC4 to zero, to start over and prevent overflow?

Thank you!

I guess I can change input to any other pin in these lines (with according MUX) - e.g. A0 , or are there any limitations on which pins it cam be routed?

Yes that right, all signal pins can be routed with the exception of the Non Maskable Interrupt (NMI) pin on port pin PA08.

The only other thing to bear in mind is that the EIC peripheral has only 16 channels and some of them are shared between pins, but usually this isn't too much of an issue.

And one more tiny question, what is appropriate solution to reset TC4 to zero, to start over and prevent overflow?

The TC4 timer can be controller by issuing commands via its CTRLB register.

To reset/retrigger the TC4 timer:

TC4->COUNT32.CTRLBSET.reg = TC_CTRLBSET_CMD_RETRIGGER;  // Retrigger the TC4 timer
while (TC4->COUNT32.STATUS.bit.SYNCBUSY);               // Wait for synchronization

To stop the TC4 timer:

TC4->COUNT32.CTRLBSET.reg = TC_CTRLBSET_CMD_STOP;    // Stop the TC4 timer
while (TC4->COUNT32.STATUS.bit.SYNCBUSY);            // Wait for synchronization

The SAMD21 datasheet's a little be vague, but writing to the TC4's CTRLB register might disable the continous read synchronization. The datasheet doesn't mention whether this behaviour is register or peripheral wide, anyhow if you can't read the COUNT register afterwards (usually it returns 0), this can be re-established by the lines:

TC4->COUNT32.READREQ.reg = TC_READREQ_RCONT |        // Enable a continuous read request
                           TC_READREQ_ADDR(0x10);    // Offset of the 32-bit COUNT register
while (TC4->COUNT32.STATUS.bit.SYNCBUSY);            // Wait for synchronization

...or to use a basic single read synchronization on the COUNT register:

TC4->COUNT.STATUS.READREQ.reg = TC_READREQ_RREQ;    // Request a read synchronization
while (TC4->COUNT32.STATUS.bit.SYNCBUSY);           // Wait for read synchronization
SerialUSB.println(TC4->COUNT32.COUNT.reg);          // Output the result

OK, perfect - thanks again a lot!

I actually found one drawback when adding this code to my project. After adding to my code it started to freeze/crash randomly. After few hours commenting out several parts of code to find what is causing I found it.

TC4 or TC5 is used by Arduino Core for delay() function. I dont use this often, but 3rd libraries often do. So when TC4 and TC5 are configured in 32bit counter mode, everything works fine until there is delay() in sketch - then it freezes. I tried delayMicroseconds() and this works fine along counter.

According to documentation, there are five timer/counters availiable:

Selectable configuration
Up to five 16-bit TCs, each configurable as:
8-bit TC with two compare/capture channels
16-bit TC with two compare/capture channels
32-bit TC with two compare/capture channels, by using two TCs

So I guess I need to configure single counter in 16bit mode (capacity is enough for my application) and try all of them TC0 - TC4 to find out which are free and not used in Arduino Core.

Or is there any reference to find out which counters are used by IDE? My guess is that there are used two 32bit counters for delay() and delayMicroseconds(), so there should be one 16 bit counter left… At least I hope so :slight_smile:

TC4 or TC5 is used by Arduino Core for delay() function. I dont use this often, but 3rd libraries often do. So when TC4 and TC5 are configured in 32bit counter mode, everything works fine until there is delay() in sketch - then it freezes. I tried delayMicroseconds() and this works fine along counter.

Unlike the AVR Arduinos, the SAMD21 actually uses its systick (system tick) timer for delay(), millis() and micros() timing functions, while delayMicroseconds() uses some assembly code to time clock cycles. These functions are implemented in the SAMD21 Arduino core files "delay.h" and "delay.c".

By default none of the SAMD21's TCC or TC timers are used by the Arduino core code and are available to use as you wish. These timers however are employed by subsequent calls to functions such as analogWrite() and other external Arduino libraries.

Whatever's causing your system to freeze, it isn't an interaction between TC5 and delay(). Not synchronizing the TC timer registers however can cause the microcontroller's peripheral bus to stall.

I am really sorry, delay() function is working fine! It was my mistake that i forgot to uncomment my interrupt function that was pulling my status LEDs HIGH all the time. This caused that code looked frozen on delay() but it wasnt in reality.

MartinL: Unlike the AVR Arduinos, the SAMD21 actually uses its systick (system tick) timer for delay(), millis() and micros() timing functions, while delayMicroseconds() uses some assembly code to time clock cycles. These functions are implemented in the SAMD21 Arduino core files "delay.h" and "delay.c".

I was using Timers/Counters on AVR for a lot of years. That is why my first thing I thought when my code "froze on delay()" was this counter collision :D Looks like ARM is really a LOT more complex than AVR where everything was more "static". I have a feeling that ARM is little bit like FPGA where you have a matrix of peripherials that you can chain up almost freely in many compbinations.

MartinL: By default none of the SAMD21's TCC or TC timers are used by the Arduino core code and are available to use as you wish. These timers however are employed by subsequent calls to functions such as analogWrite() and other external Arduino libraries.

Isnt analogWrite() using different peripherial? SAMD21 has a DAC that is different peripherial. Or it might be utilizing when TC using this function for "legacy" PWM generation. I dont use this anyway, so I dont mind about it too much.

MartinL: Not synchronizing the TC timer registers however can cause the microcontroller's peripheral bus to stall.

So I guess its a good practice to call:

while (TC4->COUNT32.STATUS.bit.SYNCBUSY);                // Wait for synchronization

Once at the beginnig of the loop cycle or before counter reading/clearing function? I am using a lot SERCOM devices (Two as UARTs and one as SPI) so I guess that peripherial bus is quite busy :)

Hi Helium328PU,

The SAMD21's datasheet indicates which registers require read or write synchronization.

The TC's COUNT register is a special case, in that it's necessary to request a continuous or single read synchronization. To read the TC's COUNT register using a single read synchronization:

TC4->COUNT.STATUS.READREQ.reg = TC_READREQ_RREQ;    // Request a read synchronization
while (TC4->COUNT32.STATUS.bit.SYNCBUSY);           // Wait for read synchronization
SerialUSB.println(TC4->COUNT32.COUNT.reg);          // Output the result

The read synchronization request and wait are placed just before the the read from the COUNT register itself.

In the case of write synchronization, it's usually necessary to write to the register first and then wait for synchonization to complete before proceeding:

TC4->COUNT32.COUNT.reg = 0x0000;              // Output the result
while (TC4->COUNT32.STATUS.bit.SYNCBUSY);     // Wait for read synchronization

Write synchronization is automatic and happens anyway, irrespective of whether you wait for it to complete or not. It's just that if you go on to access certain other peripheral registers before it's complete, it has the potential to stall the peripheral bus.

Regarding the analogWrite() function, which TCC or TC timer it uses for PWM output depends on which pin is selected. This function only uses the DAC, if the DAC pin is chosen.

Hello everybody, I see you are really smart in timer and counter handling on SAMD21. Do someone of you have an idea, how I can convert this code (ARM) in SAMD21 code. I want to connect three humidity sensors on the hardware timer/counter inputs. The humidity sensor outputs a frequency signal. Up to 500kHz max. http://fam-haugk.de/giessautomat-fuer-pflanzen

/* 
 * Hardware Counting sketch for Arduino micro
 * 
 * uses pin 12 on 32U4
 */

const int ledPin = 13;
unsigned int freq;

const int selectPin0 = 6;
const int selectPin1 = 7;
const int selectPin2 = 8;

const int maxRead = 25;
int readCount;
int currPort;

void setup() {
  Serial.begin(9600);
  pinMode(ledPin, OUTPUT);
  //hardware counter setup
  TCCR1A = 0; // reset timer/counter control register

  pinMode(selectPin0, OUTPUT);
  pinMode(selectPin1, OUTPUT);
  pinMode(selectPin2, OUTPUT);
  
  readCount = 0;
  currPort = 0;
}

void loop() {
  
  // put your main code here, to run repeatedly:
  freq = 0;

  digitalWrite(ledPin, LOW);
  delay(200);
  digitalWrite(ledPin, HIGH);

  // start the counting
  bitSet(TCCR1B, CS12); // Counter clock source is external pin
  bitSet(TCCR1B, CS11); // Clock on rising edge
  delay(40);  // wait 40ms
  // stop the counting
  TCCR1B = 0;
  freq = TCNT1/4; // devide by 4 to get number of oscillations per 10ms
  TCNT1 = 0; // reset the hardware counter
  
  Serial.println(freq);
  
  // Switch the sensor every 25 cycles
  readCount++;
  if(readCount > maxRead) {
    currPort++;
    if(currPort == 3) currPort = 0;
    readCount = 0;
    selectPort(currPort);
  }
}

// select port in multiplexer
// for simple understanding, the binary pattern is directly entered for the selected port
// and not calculated
void selectPort(int port) {
  if (port==0) {
    digitalWrite(selectPin0, LOW);
    digitalWrite(selectPin1, LOW);
    digitalWrite(selectPin2, LOW);
  }
  else if (port == 1) {
    digitalWrite(selectPin0, HIGH);
    digitalWrite(selectPin1, LOW);
    digitalWrite(selectPin2, LOW);
  }
  else if (port == 2) {
    digitalWrite(selectPin0, LOW);
    digitalWrite(selectPin1, HIGH);
    digitalWrite(selectPin2, LOW);
  }
}

I want to do the same but with MKR WiFi 1010 and 3 hardware counters and without the multiplexer to read more than one sensors on one TC input, if possible. But I have actually not really an idea how this counter config works.

It would be very nice of you if someone could help me with this.

Thanks a lot!

Hi MartinL,

MartinL:
Hi Helium328PU,

Your options are to either simply count the number of incoming pulses using the attachInterrupt() function and an interrupt service routine (ISR), or alternatively route the pulses through the SAMD21’s External Interrupt Controller (EIC) and on to a TC timer using the Event System. The TC timer is configured to count the incoming pulses. The Event System is a 12-channel highway for peripheral-to-peripheral communications without CPU intervention. The latter option has the advantage that it isn’t necessary to call the ISR each time a pulse is received; the whole process occurs without any CPU intervention, apart from reading the timer’s COUNT register.

Here’s some example code that takes the incoming pulses on D12 (on the Arduino Zero) a.k.a port pin PA19 and routes these through the EIC to timer TC4 via the Event System. TC4 is configured to count the incoming pulses and is set to 32-bit mode in conjunction with TC5. The number or counts is output to the console every second:

// Setup TC4 in 32-bit mode to count incoming pulses on digital pin D12 using the Event System

void setup()
{
  // Serial Communication /////////////////////////////////////////////////////////////////
 
  SerialUSB.begin(115200);                        // Send data back on the Zero’s native port
  while(!SerialUSB);                              // Wait for the SerialUSB port to be ready
 
// Generic Clock /////////////////////////////////////////////////////////////////////////

GCLK->CLKCTRL.reg = GCLK_CLKCTRL_CLKEN |        // Enable the generic clock…
                      GCLK_CLKCTRL_GEN_GCLK0 |    // On GCLK0 at 48MHz
                      GCLK_CLKCTRL_ID_TC4_TC5;    // Route GCLK0 to TC4 and TC5
  while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization

// Port Configuration ///////////////////////////////////////////////////////////////////

// Enable the port multiplexer on digital pin D12 (port pin PA19)
  PORT->Group[g_APinDescription[12].ulPort].PINCFG[g_APinDescription[12].ulPin].bit.PMUXEN = 1;
  // Set-up the pin as an EIC (interrupt) peripheral on D12
  PORT->Group[g_APinDescription[12].ulPort].PMUX[g_APinDescription[12].ulPin >> 1].reg |= PORT_PMUX_PMUXO_A;

// External Interrupt Controller (EIC) ///////////////////////////////////////////////////

EIC->EVCTRL.reg |= EIC_EVCTRL_EXTINTEO3;                                // Enable event output on external interrupt 3 (D12)
  EIC->CONFIG[0].reg |= EIC_CONFIG_SENSE3_HIGH;                          // Set event detecting a HIGH level on interrupt 3
  EIC->INTENCLR.reg = EIC_INTENCLR_EXTINT3;                              // Disable interrupts on interrupt 3
  EIC->CTRL.bit.ENABLE = 1;                                              // Enable the EIC peripheral
  while (EIC->STATUS.bit.SYNCBUSY);                                      // Wait for synchronization

// Event System //////////////////////////////////////////////////////////////////////////

PM->APBCMASK.reg |= PM_APBCMASK_EVSYS;                                  // Switch on the event system peripheral

EVSYS->USER.reg = EVSYS_USER_CHANNEL(1) |                              // Attach the event user (receiver) to channel 0 (n + 1)
                    EVSYS_USER_USER(EVSYS_ID_USER_TC4_EVU);              // Set the event user (receiver) as timer TC4
 
  EVSYS->CHANNEL.reg = EVSYS_CHANNEL_EDGSEL_NO_EVT_OUTPUT |              // No event edge detection
                      EVSYS_CHANNEL_PATH_ASYNCHRONOUS |                  // Set event path as asynchronous
                      EVSYS_CHANNEL_EVGEN(EVSYS_ID_GEN_EIC_EXTINT_3) |  // Set event generator (sender) as external interrupt 3
                      EVSYS_CHANNEL_CHANNEL(0);                          // Attach the generator (sender) to channel 0                               
 
  // Timer Counter TC4 /////////////////////////////////////////////////////////////////////

TC4->COUNT32.EVCTRL.reg |= TC_EVCTRL_TCEI |              // Enable asynchronous events on the TC timer
                            TC_EVCTRL_EVACT_COUNT;        // Increment the TC timer each time an event is received

TC4->COUNT32.CTRLA.reg = TC_CTRLA_MODE_COUNT32;          // Configure TC4 together with TC5 to operate in 32-bit mode
                     
  TC4->COUNT32.CTRLA.bit.ENABLE = 1;                      // Enable TC4
  while (TC4->COUNT32.STATUS.bit.SYNCBUSY);                // Wait for synchronization

TC4->COUNT32.READREQ.reg = TC_READREQ_RCONT |            // Enable a continuous read request
                            TC_READREQ_ADDR(0x10);        // Offset of the 32-bit COUNT register
  while (TC4->COUNT32.STATUS.bit.SYNCBUSY);                // Wait for synchronization
}

void loop()
{
  SerialUSB.println(TC4->COUNT32.COUNT.reg);              // Output the results
  delay(1000);                                            // Wait for 1 second
}

I’m wondering what I did wrong, code flashed on MKR 1400, wire connected to D12 and other end toggeling between GND and Vcc gives me all time “0” in terminal, any tip?

Take 2, I've connected 1 kHz generator to this pin D12 and still nothing :(