Counting number of pulses in a gate interval on SAMD21

Hi All,
I have found in this Forum a very clever piece of code by MartinL that I have adapted to my purpose of counting the number of pulses of an external signal occurring within a gate interval (https://forum.arduino.cc/t/isr-timer-capture-on-samd21/685456). The original code was referring to an old MKR ZERO schematic, I have changed the PAxx used to match latest v.5 release.
The code, reported at the bottom, performs 3 tasks:

  1. generates a 1s gate signal using the 32768 kHz RTC signal at output pin 8 (PA16)
  2. sets pin 6 as the gate input for counting (therefore to use the gate signal generated at point 1, pin 8 and pin 6 must be wired together; a different external gate signal can be utilized fed to pin 6, voiding the need of the piece of code of point 1 above, for example I am using a 28Hz external square wave)
  3. counts the number of pulses (in the gate interval) of a measuring signal input at pin 10 (PA19)

Can anybody address me on how to modify this code in order to count within the external gate at pin 6 no longer the pulses of a signal applied to pin 10, but rather the internal clock pulses ?
Moreover, since I need extremely precise counting, is it possible to use the doubled 96MHz clock instead of the standard 48MHz ?

Thanks in advance,
Marco

Here is the code:

// Count the number of pulses on pin 10 (PA19) using 1 second test trigger pulse on pin 8 (PA16) 
// for a trigger input on pin 6
// pin 8 (PA16) is the gate output of 1s, pin 6 is the external trigger input, pin 10 (PA19) is the external input of the pulses to measure
// connect pin 8 to pin 6 and the signal to measure to pin 10

#define MAXDATA 20

volatile boolean countRead = false;                // Count read flag
volatile uint32_t count;                           // Variable to hold the TCC0 count value

void setup()
{
  SerialUSB.begin(115200);                         // Initialise the native serial port
  while(!SerialUSB);                               // Wait for the console to open

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

  GCLK->GENDIV.reg =  GCLK_GENDIV_DIV(1) |          // Select clock divisor to 1                     
                      GCLK_GENDIV_ID(4);            // Select GLCK4         
 
  GCLK->GENCTRL.reg = GCLK_GENCTRL_IDC |            // Set the duty cycle to 50/50 HIGH/LOW 
                      GCLK_GENCTRL_GENEN |          // Enable GCLK                   
                      GCLK_GENCTRL_SRC_XOSC32K |    // Select GCLK source as external 32.768kHz crystal (XOSC32K)                          
                      GCLK_GENCTRL_ID(4);           // Select GCLK4             
  while (GCLK->STATUS.bit.SYNCBUSY);                // Wait for synchronization

  GCLK->CLKCTRL.reg = GCLK_CLKCTRL_CLKEN |          // Enable generic clock
                      GCLK_CLKCTRL_GEN_GCLK0 |      // GCLK0 at 48MHz 
                      GCLK_CLKCTRL_ID_TC4_TC5;      // As a clock source for TC4 and TC5
  
  GCLK->CLKCTRL.reg = GCLK_CLKCTRL_CLKEN |          // Enable generic clock
                      GCLK_CLKCTRL_GEN_GCLK4 |      // GCLK4 at 32.768kHz
                      GCLK_CLKCTRL_ID_TCC2_TC3;     // As a clock source for TCC2 and TC3

  //////////////////////////////////////////////////////////////////////////////////////////////
  // TCC2 Initialisation - reference timer: measures a 1s period
  //////////////////////////////////////////////////////////////////////////////////////////////     
                    
  PORT->Group[PORTA].PINCFG[16].bit.PMUXEN = 1;                             // Enable the port multiplexer on port pin PA16 (D11)
  PORT->Group[PORTA].PMUX[16 >> 1].reg |= PORT_PMUX_PMUXE_E;                // Set-up PA16 (D11) as an EIC (interrupt)
  
  TCC2->PER.reg = 32767;                             // Set the period (PER) register for a PWM period of 1s
  while (TCC2->SYNCBUSY.bit.PER);                    // Wait for synchronization

  TCC2->WAVE.reg = TCC_WAVE_WAVEGEN_NPWM;            // Set timer to Normal PWM mode (NPWM)
  while (TCC2->SYNCBUSY.bit.WAVE);                   // Wait for synchronization

  TCC2->CC[0].reg = 16384;                           // Set the duty cycle to 50%
  while (TCC2->SYNCBUSY.bit.CC0);                    // Wait for synchronization

  TCC2->CTRLA.bit.ENABLE = 1;                        // Enable TCC2
  while (TCC2->SYNCBUSY.bit.ENABLE);                 // Wait for synchronization

  ////////////////////////////////////////////////////////////////////////////////////////
  // TC4 Initialisation - measurement counter: counts the number incoming of pulses
  ////////////////////////////////////////////////////////////////////////////////////////

  PORT->Group[PORTA].PINCFG[19].bit.PMUXEN = 1;                            // Enable the port multiplexer on port pin PA19 (D12)
  PORT->Group[PORTA].PMUX[19 >> 1].reg |= PORT_PMUX_PMUXO_A;               // Set-up PA12 (D12) as an EIC (interrupt)
  
  EIC->EVCTRL.reg |= EIC_EVCTRL_EXTINTEO3;                                 // Enable event output on external interrupt 3
  EIC->CONFIG[0].reg |= EIC_CONFIG_SENSE3_HIGH;                            // Set event detecting a HIGH level
  EIC->INTENCLR.reg = EIC_INTENCLR_EXTINT3;                                // Clear the interrupt flag on channel 3
  EIC->CTRL.bit.ENABLE = 1;                                                // Enable EIC peripheral
  while (EIC->STATUS.bit.SYNCBUSY);                                        // Wait for synchronization
  
  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 event 
                    
  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

  TC4->COUNT32.EVCTRL.reg = TC_EVCTRL_TCEI |         // Enable TC4 event input
                            TC_EVCTRL_EVACT_COUNT;   // Increment the TC4 counter upon receiving an event
                                                                
  TC4->COUNT32.CTRLA.reg = TC_CTRLA_MODE_COUNT32;    // Chain TC4 with TC5 to create a 32-bit timer

  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

  attachInterrupt(6, trigger, RISING);              // Initialise the trigger pin 6
}

void loop()
{
  static uint32_t prevcount;
  uint32_t delta;
  uint32_t dta[MAXDATA];
  static int turn = 0;
  int i;

  for(i=0; i < MAXDATA; i++)
  {
    while(!countRead);                                 // Block until the trigger() function returns
    countRead = false;                                 // Reset the count read flag
    dta[i] = count - prevcount;
    prevcount = count;
  }
  for(i=0; i < MAXDATA; i++)
  {
    SerialUSB.print("turn "); 
    SerialUSB.print(turn); 
    SerialUSB.print("    "); 
    SerialUSB.println(dta[i]);                          // Print the result
  }
  turn++;
}

void trigger()
{
  count = TC4->COUNT32.COUNT.reg;                    // Read the COUNT register
  countRead = true;                                  // Set the count read flag
}


Hi @marcosartore

Are you aiming to capture the pulse width of the gate signal with the 96MHz internal clock?

It's possible to connect only the TCC0/TCC1/TCC2 and TC3 timers to the 96MHz internal phase locked loop. However, TCC0 and TCC1 are only 24-bits, unlike the example code, where TC4 and TC5 are chained together to form a 32-bit timer.

Although the higher 96MHz clock rate will result in a higher timing resolution, the trade-off is running at double the speed with a 24-bit timer will reduce the maximum pulse width that can be measured. In this case 0.175 seconds (1 / 96MHz * 2^24).

Hi MartiniL,
thanks for your answer !
In my case the gate frequency is about 20Hz, i.e. 0.05s period, then using a 24 bit Timer is OK.
Yes, I need to target my application for the highest possible resolution, thus using the 96MHz clock would be much better, stated that my max gate interval is within the max allowed with 24 bits.

Sorry if I must base my code on your help, but I wasn't able to find (neither to go close !) any tech document explaining how to low-level program the SAMD21. I've done the very same counting scheme on a dsPic33 using its Input Capture module with success but need to migrate to the MKR ZERO now.

Thanks a lot,
Marco

Hi @marcosartore

The following code receives a pulse on digital 6 (PA20). This signal is passed asynchronously to the TCC0 timer via the External Interrupt Controller (EIC) module and the Event System (a 12-channel peripheral-to-peripheral highway).

The TCC0 timer is clocked by the 96MHz Fractional Digital Phase Locked Loop (FDPLL96M) clock source. The timer is set to capture mode that determines both the pulse-width and period of the incoming signal. The code below simply outputs the number of 96MHz "ticks" for both.

I tested the code against a 20Hz PWM signal at 50% duty-cycle, generated by my Arduino Due. Using a separate board ensures that the input signal is asynchronous with respect to the MKR Zero's internal clocks. The test revealed that there is some jitter of a few timer cycles in the output values, this is mainly due to cycle-to-cycle jitter in the phase locked loops of both boards while counting over the 50ms period, or in other words 4.8 million ticks.

Here's the code:

// Setup TCC0 to capture pulse-width and period on pin D6 (PA20) using the 96MHz (FDPLL96M) clock source

volatile boolean periodComplete;                   // Flag to indicate the end of the timer cycle
volatile uint32_t isrPulsewidth, isrPeriod;        // Variables to store the pulse width and period in the interrupt service routine (ISR)

void setup() 
{
  PM->APBCMASK.reg |= PM_APBCMASK_EVSYS;           // Switch on the event system peripheral
  
  GCLK->CLKCTRL.reg = GCLK_CLKCTRL_CLKEN |         // Enable the generic clock
                      GCLK_CLKCTRL_GEN_GCLK1 |     // Select GCLK1 using either external XOSC32K or internal OSC32K oscillator depending Arduino board
                      //GCLK_CLKCTRL_GEN_GCLK2 |     // Select GCLK2 using the OSCULP32K ultra low power 32k oscillator
                      GCLK_CLKCTRL_ID_FDPLL;       // Connect GCLK2 to GCLK_DPLL input
  
  SYSCTRL->DPLLCTRLB.reg = SYSCTRL_DPLLCTRLB_REFCLK_GCLK;     // Select GCLK_DPLL as the clock source
  
  SYSCTRL->DPLLRATIO.reg = SYSCTRL_DPLLRATIO_LDRFRAC(11) |    // Generate a 96MHz DPLL clock source from the external 32kHz crystal
                           SYSCTRL_DPLLRATIO_LDR(2928);       // Frequency = 32.768kHz * (2928 + 1 + 11/16) = 96MHz
  
  SYSCTRL->DPLLCTRLA.reg = SYSCTRL_DPLLCTRLA_ENABLE;          // Enable the Digital Phase Locked Loop (DPLL)
  while (!SYSCTRL->DPLLSTATUS.bit.LOCK);                      // Wait for the DPLL to achieve lock
  
  GCLK->GENDIV.reg = GCLK_GENDIV_DIV(1) |         // Divide the 96MHz clock source by divisor 1: 96MHz/1=96MHz, Note: must be set to 1
                     GCLK_GENDIV_ID(4);           // Select Generic Clock (GCLK) 4

  GCLK->GENCTRL.reg = GCLK_GENCTRL_IDC |          // Set the duty cycle to 50/50 HIGH/LOW
                      GCLK_GENCTRL_GENEN |        // Enable the generic clock
                      //GCLK_GENCTRL_SRC_DFLL48M |   // Set the clock source to DFLL48M at 48MHz
                      GCLK_GENCTRL_SRC_FDPLL |    // Set the clock source to DPLL96M at 96MHz
                      GCLK_GENCTRL_ID(4);         // Select GCLK4
  while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization

  GCLK->CLKCTRL.reg = GCLK_CLKCTRL_CLKEN |        // Enable the generic clock
                      GCLK_CLKCTRL_GEN_GCLK4 |    // Select GCLK4
                      GCLK_CLKCTRL_ID_TCC0_TCC1;  // Connect GCLK4 to TCC0 and TCC1 
  
  PORT->Group[PORTA].PINCFG[20].bit.PMUXEN = 1;                 // Enable the port multiplexer on digital pin D6 
  PORT->Group[PORTA].PMUX[20 >> 1].reg |= PORT_PMUX_PMUXO_A;    // Set-up the pin as an EIC (interrupt) peripheral on D6

  EIC->EVCTRL.reg |= EIC_EVCTRL_EXTINTEO4;                      // Enable event output on external interrupt 4
  EIC->CONFIG[0].reg |= EIC_CONFIG_SENSE4_HIGH;                 // Set event detecting a HIGH level
  EIC->INTENCLR.reg = EIC_INTENCLR_EXTINT4;                     // Clear the interrupt flag on channel 4
  EIC->CTRL.reg |= EIC_CTRL_ENABLE;                             // Enable EIC peripheral
  while (EIC->STATUS.bit.SYNCBUSY);                             // Wait for synchronization

  EVSYS->USER.reg = EVSYS_USER_CHANNEL(1) |                                // Attach the event user (receiver) to channel 0 (n + 1)
                    EVSYS_USER_USER(EVSYS_ID_USER_TCC0_EV_1);              // Set the event user (receiver) as timer TCC0, event 1

  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_4) |    // Set event generator (sender) as external interrupt 4
                       EVSYS_CHANNEL_CHANNEL(0);                           // Attach the generator (sender) to channel 0

  TCC0->EVCTRL.reg |= TCC_EVCTRL_MCEI1 |           // Enable the match or capture channel 1 event input
                      TCC_EVCTRL_MCEI0 |           //.Enable the match or capture channel 0 event input
                      TCC_EVCTRL_TCEI1 |           // Enable the TCC event 1 input
                      //TCC_EVCTRL_TCINV1            // Invert the event 1 input         
                      TCC_EVCTRL_EVACT1_PPW;       // Set up the timer for capture: CC0 period, CC1 pulsewidth
                      //TCC_EVCTRL_EVACT1_PWP;       // Set up the timer for capture: CC1 period, CC0 pulsewidth
                                       
  NVIC_SetPriority(TCC0_IRQn, 0);      // Set the Nested Vector Interrupt Controller (NVIC) priority for TCC0 to 0 (highest)
  NVIC_EnableIRQ(TCC0_IRQn);           // Connect the TCC0 timer to the Nested Vector Interrupt Controller (NVIC)
 
  TCC0->INTENSET.reg = TCC_INTENSET_MC1 |            // Enable compare channel 1 (CC1) interrupts
                       TCC_INTENSET_MC0;             // Enable compare channel 0 (CC0) interrupts
 
  TCC0->CTRLA.reg |= TCC_CTRLA_CPTEN1 |              // Enable capture on CC1
                     TCC_CTRLA_CPTEN0 |              // Enable capture on CC0
                     TCC_CTRLA_PRESCALER_DIV1;       // Set prescaler to 1 96MHz/1 = 96MHz               
                     
  TCC0->CTRLA.bit.ENABLE = 1;                        // Enable TCC0
  while (TCC0->SYNCBUSY.bit.ENABLE);                 // Wait for synchronization
}

void loop()
{
  if (periodComplete)                                // Check if the period is complete
  {
    noInterrupts();                                  // Read the new period and pulse width
    uint32_t period = isrPeriod;                   
    uint32_t pulsewidth = isrPulsewidth;
    interrupts();
    SerialUSB.print(F("PW: "));                      // Output the results
    SerialUSB.print(pulsewidth);
    SerialUSB.print(F("   "));
    SerialUSB.print(F("P: " ));
    SerialUSB.println(period);
    periodComplete = false;                          // Start a new period
  }
}

void TCC0_Handler()                                  // Interrupt Service Routine (ISR) for timer TCC0
{     
  // Check for match counter 0 (MC0) interrupt
  if (TCC0->INTFLAG.bit.MC0)             
  {   
    while (TCC0->SYNCBUSY.bit.CC0);                  // Wait for read synchronization
    isrPeriod = TCC0->CC[0].reg;                     // Copy the period
    periodComplete = true;                           // Indicate that the period is complete
  }

  // Check for match counter 1 (MC1) interrupt
  if (TCC0->INTFLAG.bit.MC1)           
  {
    while (TCC0->SYNCBUSY.bit.CC1);                  // Wait for read synchronization
    isrPulsewidth = TCC0->CC[1].reg;                 // Copy the pulse width
  }
}
  

Many thanks for your code, it works like a charm.
I also have evidenced the noisy counts and think that they are not due to jitter but to imprecision of the SAMD21 clock system. I have figured out that an external MHz-range crystal cannot be utilized, therefore this imprecision is quite understandable.

Marco