attachInterrupt(12, NULL, HIGH) for external interrupt on samd21g M0

I am trying to get MartinL's timer setup for the Zero TCC capture to run on my D2 pin (PA10) in the range of 300ns to 2ms. Unfortunately Zero's Pin 12 is not available on my Seeeduino. I have only D0 to D10 inputs.

Seeeduino XIAO pins:

D0 = PA02
D1 = PA04/SCOM0PAD0 /TC0,WO[0]
D2 = PA10/SCOM2PAD3+/TC1,WO[0]/TCC0,WO[2]   (my pref. input pin)
D3 = PA11/EIC/AIN18/SCOM2PAD3+/TC1/TCC0
D4 = PA08/TC0,WO[0]/TCC1,WO[2]     (is in use for SDA)
D5 = PA09/TC0,WO[1]/TCC1,WO[3]     (is in use for SCL)
D6 = PB08    (is used up) 
D7 = PB09
D8 = PA07    (is used up) 
D9 = PA05    (is used up) 
D10 = PA06   (is used up) 

Hi @meso2

The code that you link to, is quite old. Initially, I used the attachInterrupt() function for expediency, as it didn't require any register set-up. However, it eventually dawned on me that this function was unnecessarily calling the EIC_Handler() interrupt service routine for each input pulse, even with the function pointer set to NULL, greatly slowing down the CPU for high frequency signals.

Nowadays, I just set-up the interrupt EIC (External Interrupt Controller) module using registers and thereby allow the input signal to pass to the timer with EIC interrupts turned off.

Anyway, here's the code for timer TC4 pulse width and period capture on PA10 (D2) for the Seeeeeeeduino XIAO:

// Setup TC4 to capture pulse-width and period on digital pin D2 on Seeeduino Xiao
volatile boolean periodComplete;
volatile uint32_t isrPeriod;
volatile uint32_t isrPulsewidth;
uint32_t period;
uint32_t pulsewidth;

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

  GCLK->GENDIV.reg = GCLK_GENDIV_DIV(1) |          // Divide the 48MHz system clock by 1 = 48MHz
                     GCLK_GENDIV_ID(4);            // Set division on Generic Clock Generator (GCLK) 4

  GCLK->GENCTRL.reg = GCLK_GENCTRL_IDC |           // Set the duty cycle to 50/50 HIGH/LOW
                      GCLK_GENCTRL_GENEN |         // Enable GCLK 4
                      GCLK_GENCTRL_SRC_DFLL48M |   // Set the clock source to 48MHz
                      GCLK_GENCTRL_ID(4);          // Set clock source on GCLK 4
  while (GCLK->STATUS.bit.SYNCBUSY);               // Wait for synchronization
  
  GCLK->CLKCTRL.reg = GCLK_CLKCTRL_CLKEN |         // Route GCLK4 to TC4 and TC5
                      GCLK_CLKCTRL_GEN_GCLK4 |     
                      GCLK_CLKCTRL_ID_TC4_TC5;     
  
  // Enable the port multiplexer on port pin PA10
  PORT->Group[PORTA].PINCFG[10].bit.PMUXEN = 1;
  // Set-up the pin as an EIC (interrupt) on port pin PA10
  PORT->Group[PORTA].PMUX[10 >> 1].reg |= PORT_PMUX_PMUXE_A;

  EIC->EVCTRL.reg |= EIC_EVCTRL_EXTINTEO10;                                // Enable event output on external interrupt 10
  EIC->CONFIG[1].reg |= EIC_CONFIG_SENSE2_HIGH;                            // Set event detecting a HIGH level
  EIC->INTENCLR.reg = EIC_INTENCLR_EXTINT10;                               // Disable interrupts on external interrupt 10
  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_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_10) |   // Set event generator (sender) as external interrupt 10
                       EVSYS_CHANNEL_CHANNEL(0);                           // Attach the generator (sender) to channel 0

  TC4->COUNT32.EVCTRL.reg = TC_EVCTRL_TCEI |               // Enable the TC event input
                            //TC_EVCTRL_TCINV |              // Invert the event input
                            TC_EVCTRL_EVACT_PPW;           // Set up the timer for capture: CC0 period, CC1 pulsewidth
                  
  TC4->COUNT32.CTRLC.reg = TC_CTRLC_CPTEN1 |               // Enable capture on CC1
                           TC_CTRLC_CPTEN0;                // Enable capture on CC0
  while (TC4->COUNT32.STATUS.bit.SYNCBUSY);                // Wait for synchronization

  NVIC_SetPriority(TC4_IRQn, 0);      // Set the Nested Vector Interrupt Controller (NVIC) priority for TC4 to 0 (highest)
  NVIC_EnableIRQ(TC4_IRQn);           // Connect the TC4 timer to the Nested Vector Interrupt Controller (NVIC)
 
  TC4->COUNT32.INTENSET.reg = TC_INTENSET_MC1 |            // Enable compare channel 1 (CC1) interrupts
                              TC_INTENSET_MC0;             // Enable compare channel 0 (CC0) interrupts
  
  TC4->COUNT32.CTRLA.reg = //TC_CTRLA_PRESCSYNC_PRESC |      // Overflow on precaler clock, (rather than the GCLK)
                           TC_CTRLA_PRESCALER_DIV1  |      // Set prescaler to 1, 48MHz/1 = 48MHz
                           TC_CTRLA_MODE_COUNT32;          // Set TC4/TC5 to 32-bit timer mode
                          
  TC4->COUNT32.CTRLA.bit.ENABLE = 1;                       // Enable TC4
  while (TC4->COUNT32.STATUS.bit.SYNCBUSY);                // Wait for synchronization
}

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

void TC4_Handler()                                // Interrupt Service Routine (ISR) for timer TC4
{    
  // Check for match counter 0 (MC0) interrupt
  if (TC4->COUNT32.INTFLAG.bit.MC0)            
  {
    TC4->COUNT32.READREQ.reg = TC_READREQ_RREQ |           // Enable a read request
                               TC_READREQ_ADDR(0x18);      // Offset address of the CC0 register
    while (TC4->COUNT32.STATUS.bit.SYNCBUSY);              // Wait for (read) synchronization
    isrPeriod = TC4->COUNT32.CC[0].reg;                    // Copy the period  
    periodComplete = true;                                 // Indicate that the period is complete
  }

  // Check for match counter 1 (MC1) interrupt
  if (TC4->COUNT32.INTFLAG.bit.MC1)          
  {
    TC4->COUNT32.READREQ.reg = TC_READREQ_RREQ |           // Enable a read request
                               TC_READREQ_ADDR(0x1A);      // Offset address of the CC1 register
    while (TC4->COUNT32.STATUS.bit.SYNCBUSY);              // Wait for (read) synchronization
    isrPulsewidth = TC4->COUNT32.CC[1].reg;                // Copy the pulse-width
  }
}
1 Like

I am so glad you helped me with this. Thank you!

I spend a very long time trying to figure this out and searched and searched... but it is just a bit too encrypted and layered for me. It would be nice to have some "website tool" that would just spit out the code.

This is what I am using to produce the signal. The way the code is writing looks very different. Do you see anything wrong with that code? It is working.

void setup()  //TCC1 Timer-Setup AT-SAMD21-G18 ARM Cortex M0
{
  setupTimers();
  changePer(125);
}
void loop() {
  // test period
  for (uint16_t i = 10; i < 256; i++) {
    changePer(i-1);
    delay(20);
  }
  for (uint16_t i = 0; i < 246; i++) {
    changePer(255 - i);
    delay(20);
  }
  /*
  // Or change width
  for (uint16_t i = 0; i < 256; i++) {
    REG_TCC1_CC1 = i;
    //delayMicroseconds(1);
    delay(20);
    while (TCC1->SYNCBUSY.bit.CC1) ;
  }
  for (uint16_t i = 0; i < 256; i++) {
    REG_TCC1_CC1 = 255 - i;
    //delayMicroseconds(10);
    delay(20);
    while (TCC1->SYNCBUSY.bit.CC1) ;
  }
  */
}
void changePer(uint16_t myPer) {
  REG_TCC1_PER = myPer;      // Set the frequency of the PWM on TCC1 to 50kHz
  while (TCC1->SYNCBUSY.bit.PER);

  REG_TCC1_CCB1 = myPer / 2;      // Set the duty cycle of the PWM on TCC0 to 50%
  while (TCC1->SYNCBUSY.bit.CCB0);
}

// Output PWM 0.8μs to 4μs square wave on pin D3 (using timer TCC1) 
void setupTimers() {
  REG_GCLK_GENDIV = GCLK_GENDIV_DIV(1) |        // Divide the 48MHz clock source by divisor N=1: 48MHz/1=48MHz
                    GCLK_GENDIV_ID(4);          // Select Generic Clock (GCLK) 4
  while (GCLK->STATUS.bit.SYNCBUSY) ;           // Wait for synchronization

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

  // Enable the port multiplexer for the digital pin D3 and D11  **** g_APinDescription() converts Arduino Pin to SAMD21 pin
  PORT->Group[g_APinDescription[3].ulPort].PINCFG[g_APinDescription[3].ulPin].bit.PMUXEN = 1;
  // PORT->Group[g_APinDescription[11].ulPort].PINCFG[g_APinDescription[11].ulPin].bit.PMUXEN = 1;

  // Connect the TCC1 timer to digital output D3 and D11 - port pins are paired odd PMUO and even PMUXE
  // F & E specify the timers: TCC0, TCC1 and TCC2
  PORT->Group[g_APinDescription[2].ulPort].PMUX[g_APinDescription[2].ulPin >> 1].reg |= PORT_PMUX_PMUXO_E; // D3 is on PA11 = odd, use Device E on TCC1/WO[1]
  // PORT->Group[g_APinDescription[11].ulPort].PMUX[g_APinDescription[11].ulPin >> 1].reg |= PORT_PMUX_PMUXE_F; // D11 is on PA08 = even, use device F on TCC1/WO[0]

  // Feed GCLK4 to TCC0 and TCC1
  REG_GCLK_CLKCTRL = GCLK_CLKCTRL_CLKEN |       // Enable GCLK4 to TCC0 and TCC1
                     GCLK_CLKCTRL_GEN_GCLK4 |   // Select GCLK4
                     GCLK_CLKCTRL_ID_TCC0_TCC1; // Feed GCLK4 to TCC0 and TCC1
  while (GCLK->STATUS.bit.SYNCBUSY) ;           // Wait for synchronization

  // Dual slope PWM operation: timers countinuously count up to PER register value then down 0
  REG_TCC1_WAVE |= TCC_WAVE_POL(0xF) |          // Reverse the output polarity on all TCC0 outputs
                   TCC_WAVE_WAVEGEN_DSBOTH;     // Setup dual slope PWM on TCC0
  while (TCC1->SYNCBUSY.bit.WAVE) ;             // Wait for synchronization

  // Each timer counts up to a maximum or TOP value set by the PER register,
  // this determines the frequency of the PWM operation: Freq = 48Mhz/(2*N*PER)
  REG_TCC1_PER = 256;               // Set the FreqTcc of the PWM on TCC1 to 24Khz
  while (TCC1->SYNCBUSY.bit.PER) ;  // Wait for synchronization

  // Set the PWM signal to output , PWM ds = 2*N(TOP-CCx)/Freqtcc => PWM=0 => CCx=PER, PWM=50% => CCx = PER/2
  // REG_TCC1_CC1 = 128; // TCC1 CC1 - on D3  50%
  // while (TCC1->SYNCBUSY.bit.CC1) ; // Wait for synchronization
  // REG_TCC1_CC0 = 500; // TCC1 CC0 - on D11 50%
  // while (TCC1->SYNCBUSY.bit.CC0) ; // Wait for synchronization

  // Divide the GCLOCK signal by 1 giving  in this case 48MHz (20.83ns) TCC1 timer tick and enable the outputs
  REG_TCC1_CTRLA |= TCC_CTRLA_PRESCALER_DIV1 | // Divide GCLK4 by 1
                    TCC_CTRLA_ENABLE;          // Enable the TCC0 output
  while (TCC1->SYNCBUSY.bit.ENABLE) ; // Wait for synchronization
}

Hi @meso2

Your code looks good to me. The only thing I'd suggest is using the buffered PERB register rather than the unbuffered PER. It just requires adding a "B" to the end of the register name.

The reason for using the buffered CCBx and PERB registers, is that they only take effect at the end of the timer cycle, whereas changes to the unbuffered CCx and PER register take effect immediately at the timer output. This prevents glitches from occuring on the timer output waveform.

To prevent slight possibility that CCBx and PERBx register updates occur either side of a timer update, it's possible to temporarily block updates using the Lock Update bit in the timer's CTRLB register like this:

  TCC0->CTRLBSET.reg = TCC_CTRLBSET_LUPD;         // Set the Lock Update bit
  while (TCC0->SYNCBUSY.bit.CTRLB);               // Wait for synchronization
  TCC0->PERB.reg = 47999;                         // Set the frequency of the PWM on TCC0 to 1000Hz
  while(TCC0->SYNCBUSY.bit.PERB);                 // Wait for synchronization
  TCC0->CCB[3].reg = 24000;                       // Set the duty-cycle to 50%
  while(TCC0->SYNCBUSY.bit.CCB3);                 // Wait for synchronization
  TCC0->CTRLBCLR.reg = TCC_CTRLBCLR_LUPD;         // Clear the Lock Update bit
  while (TCC0->SYNCBUSY.bit.CTRLB);               // Wait for synchronization

I am trying to make sense of your suggestion. At first I got lost in the details but got it now working.

As I understand it I am using TCC1 with CCB1 in the above code (not TCC0 with CCB3.)


So instead of updating the pulse like this:

void changePer(uint16_t myPer) {
  REG_TCC1_PER = myPer;      // Set the frequency of the PWM on TCC1 to 50kHz
  while (TCC1->SYNCBUSY.bit.PER);
  REG_TCC1_CCB1 = myPer / 2;      // Set the duty cycle of the PWM on TCC0 to 50%
  while (TCC1->SYNCBUSY.bit.CCB0);
}

you suggest I should be using this?

void changePer(uint16_t myPer) {
  TCC1->CTRLBSET.reg = TCC_CTRLBSET_LUPD;     // Set the Lock Update bit
  while (TCC1->SYNCBUSY.bit.CTRLB);           // Wait for synchronization
  TCC1->PERB.reg = myPer;                     // Set period
  while(TCC1->SYNCBUSY.bit.PERB);             // Wait for synchronization
  TCC1->CCB[1].reg = myPer/2;                 // Set duty-cycle to 50%
  while(TCC1->SYNCBUSY.bit.CCB1);             // Wait for synchronization
  TCC1->CTRLBCLR.reg = TCC_CTRLBCLR_LUPD;     // Clear the Lock Update bit
  while (TCC1->SYNCBUSY.bit.CTRLB);           // Wait for synchronization
}

My apologies, I just cut 'n' pasted this code from another forum thread.

you suggest I should be using this?

Yes, it effectively makes sure changes to the PERB and the CCB1 registers won't occur on separate timer cycles, (even though the chances of this happening is quite small).

I finally can comunicate between Seeeduino's!

Thank you so very much for helping me with this.

Implementing this functionality I've noticed that reading the register value from a period is not always possible when the periods are very short. At first I had the shortest periods set to 0.25us. But reading such short pulses just did not produce any read. It looks like it need a minimum in duration to start to show a response. For now it looks like 6.5us to 10.3us is my range I have to work with; which is plenty fast for now.

Hi @meso2

I think that the limiting factor is the TC4's interrupt service routine. At a waveform period of 6.5us, the interrupt is being called 153846 times a second and double that if you include pulse width measurement.

I've got some pulse width and period timer capture code that uses a DMA rather than an ISR. I'm just wondering if this would increase the speed of meaurement? :thinking:

Or ... maybe using another timer to update another register less frequently with the pulse period value read.

Timers require some real time to understand. :grinning: