Arduino Zero TCC Capture

I've been reading over documentation and Atmel studio examples all day for SAMD processors trying to figure out how to setup the TCC module in capture mode. So far the vast majority of examples that I have found involve operating the timers for generating waveform outputs.

Some of the helpful code examples taht I've found are:

and

Then there are docs which explain operating the module in PWP mode. However they stop short of providing a readable example code. Then there is this app note for D20 that doesn't compile in Atmel Studio:

So I'm just curious if there is anyone out there who has had success with the capture mode of SAMD?

What I really want to do is detect pulse width and period of a signal that is connected to one of my Arduino Zero pins. The receive an interrupt each time that new pulses arrive.

Ok so here is an update as to where I am at. first here is the code:

uint32_t last = 0;
uint32_t now = 0;
uint32_t width = 0;
uint32_t period = 0;
uint32_t irqs = 0;

// the setup function runs once when you press reset or power the board
void setup() {
  // initialize digital pin 13 as an output.
  pinMode(27, INPUT_PULLUP);
  pinMode(LED_BUILTIN, OUTPUT);
  Serial.begin(115200);
  while (!Serial);
  Serial.println("Hello!");
  
  // Configure the Clock
  GCLK->CLKCTRL.reg = (uint16_t) (GCLK_CLKCTRL_CLKEN | GCLK_CLKCTRL_GEN_GCLK0 | GCLK_CLKCTRL_ID( GCM_TC1_TC2)) ;
  while ( GCLK->STATUS.bit.SYNCBUSY == 1 ); // wait for sync 

  // Diable the Timer
  REG_TC1_CTRLA &= ~TC_CTRLA_ENABLE;

  // Set prescaler and make timer a 32bit counter
  REG_TC1_CTRLA |= TC_CTRLA_PRESCALER_DIV1 | TC_CTRLA_MODE(TC_CTRLA_MODE_COUNT32_Val);   // Set perscaler

  // Connect the MCE0 capture channel to the Event control
  REG_TC1_EVCTRL |= TC_EVCTRL_MCEO0 | TC_EVCTRL_TCEI | TC_EVCTRL_EVACT_PWP;

  // Setup Interrupts
  REG_TC1_INTENSET = 0;              // disable all interrupts
  REG_TC1_INTENSET |= TC_INTENSET_OVF | TC_INTENSET_MC0;  // Overflow and MCE0 capture
  NVIC_EnableIRQ(TC1_IRQn);
  
  // Enable TC
  REG_TC1_CTRLA |= TC_CTRLA_ENABLE ;
  Serial.println("TC Enabled!"); 

  attachInterrupt(27,pin_irq,CHANGE);
  EIC->EVCTRL.bit.EXTINTEO7 = 1;
  PORT->Group[0].PMUX[27/2].bit.PMUXO = PINMUX_PA27A_EIC_EXTINT7 & 0xFFFF;

  PM->APBCMASK.reg |= PM_APBCMASK_EVSYS;
  GCLK->CLKCTRL.reg = (uint16_t) (GCLK_CLKCTRL_CLKEN | GCLK_CLKCTRL_GEN_GCLK0 | GCLK_CLKCTRL_ID_EVSYS_0);
  while (GCLK->STATUS.bit.SYNCBUSY);

  EVSYS->CTRL.reg = EVSYS_CTRL_SWRST;
  EVSYS->CHANNEL.reg = EVSYS_CHANNEL_PATH_ASYNCHRONOUS | EVSYS_CHANNEL_EVGEN(EVSYS_ID_GEN_EIC_EXTINT_7) | EVSYS_CHANNEL_CHANNEL(0); // Maybe CH+1 but why?
  EVSYS->USER.reg = (uint16_t) (EVSYS_USER_USER(EVSYS_ID_USER_TC1_EVU) | EVSYS_USER_CHANNEL(0));
  EVSYS->INTENSET.bit.EVD0 = 1;
  
}

// the loop function runs over and over again forever
void loop() {
  now = REG_TC1_COUNT32_COUNT;
  width = REG_TC1_COUNT32_CC0;
  period = REG_TC1_COUNT32_CC1;
  Serial.print("Count: ");
  Serial.println(now);
  Serial.print("Delta: ");
  Serial.println(now-last);
  Serial.print("Width: ");
  Serial.println(width);
  Serial.print("Period: ");
  Serial.println(period);
  Serial.print("IRQs: ");
  Serial.println(irqs);
  irqs = 0;
  last = now;
  digitalWrite(LED_BUILTIN, HIGH);   // turn the LED on (HIGH is the voltage level)
  delay(1000);
  //Serial.println("low");// wait for a second
  digitalWrite(LED_BUILTIN, LOW);    // turn the LED off by making the voltage LOW
  delay(1000);              // wait for a second
}

void pin_irq() {
  Serial.println("pin_irq");
}

void TC1_Handler()
{
  irqs++;
  //Serial.println(irqs);
  //REG_TC1_INTFLAG = 0x00;
  //Serial.println(REG_TC1_INTFLAG);
  TcCount32* TC = (TcCount32*) TC1;       // get timer struct
  if (TC->INTFLAG.bit.OVF == 1) {  // A overflow caused the interrupt
    //digitalWrite(pin_ovf_led, irq_ovf_count % 2); // for debug leds
    //digitalWrite(pin_mc0_led, HIGH); // for debug leds
    Serial.println("OVF!");
    TC->INTFLAG.bit.OVF = 1;    // writing a one clears the flag ovf flag
    //irq_ovf_count++;                 // for debug leds
  }
  
  if (TC->INTFLAG.bit.MC0 == 1) {  // A compare to cc0 caused the interrupt
    //digitalWrite(pin_mc0_led, LOW);  // for debug leds
    Serial.println("MC0!");
    TC->INTFLAG.bit.MC0 = 1;    // writing a one clears the flag ovf flag
  }
}

void EVSYS_Handler() {
  Serial.println("EVSYS_IRQ");
  Serial.println( EVSYS->INTENSET.reg);
  EVSYS->INTFLAG.bit.EVD0 = 1;
}

So what is working:
The Timer is running in 32bit mode
The Timer overflow interrupt does work
The Pin interrupt does work

What I'm not sure about:
Is pin PA27 properly routed to the Event system?

  • Port PUX properly configured? It should be since this is done via "pinMode(" and "attachInterrupt("
  • Eventsys properly configured?

Hi electro_95,

I'm also trying to get this to work, but there appears to be a mismatch between the "Period and Pulse-Width Capture Action" mode and how the events system works.

I understand that the "Period and Pulse-Width Capture Action" captures the waveform's pulse-width and period in the CC0 and CC1 registers (or vice-versa), but the problem is that the TC timer counters on the SAMD21 have no inputs and they're only able to capture input events from an event channel.

That leaves the question of how to get the input waveform to the TC timer counter? As the PORTs don't have any event functionality, that just leaves the EIC (External Interrupt Controller).

The EIC allows to you activate events, that as far as I can gather (as the datasheet doesn't explain) corresponds to the EIC interrupt flag. I believe the EIC will send out event pulses on one of the specified event channels, as the given interrupt flag is set and cleared.

So the EIC is generating a short positive pulses on the event channel for each transition of the input waveform, however the "Period and Pulse-Width Capture Action" mode by default is using the rising pulses to capture the period and the time to the following trailing pulse to capture the pulsewidth. As far as I can see the two just don't match up.

It almost appears as if the "Period and Pulse-Width Capture Action" mode was designed for a timer counter input pin, rather than an event channel.

Anyone got any ideas?

I'm beginning to think that the event channel just toggles with each event, as I consider is probably only way interrupt events could work in conjunction with the timer.

Hi electro_95,

I finally solved it.

The code automatically captures an incoming digital waveform's period and pulse-width.

The incoming waveform is detected by enabling interrupts for level detection on the input pin. The event system is sort of a 12 channel highway between peripherals that allows them to communicate without CPU intervention. By enabling an output event for the interrupt pin (generator/sender) and the input event for a timer (user/receiver) and connecting them on a given channel, we can pass infomation about changes on the input pin to the timer using the event system. With the timer set to "Period and Pulse-Width Capture Action" mode, we can then automatically capture the period and pulse-width of the waveform.

The timer starts on the rising edge of the event line. The number of counter ticks to the subsequent trailing edge gives us the pulse-width, which is stored in the counter compare register 1 (CC1). The following rising edge gives us the period, which is stored in the counter compare register 0 (CC0). At this point the counter is reset back to 0 to start the cycle over again.

The following code outputs on the Zero's native port, the period and pulse-width of a radio controlled receiver channel in microseconds, (pulse-width: 1-2ms, period: ~22ms) using capture on TC3, at 1MHz (1us tick):

// Setup TC3 to capture pulse-width and period
volatile boolean periodComplete;
volatile uint16_t isrPeriod;
volatile uint16_t isrPulsewidth;
uint16_t period;
uint16_t pulsewidth;

void setup() 
{ 
  SerialUSB.begin(115200);                  // Send data back on the Zero's native port
  while(!SerialUSB);                        // Wait for the SerialUSB port to be ready
  
  REG_PM_APBCMASK |= PM_APBCMASK_EVSYS;     // Switch on the event system peripheral
  
  REG_GCLK_GENDIV = GCLK_GENDIV_DIV(3) |    // Divide the 48MHz system clock by 3 = 16MHz
                    GCLK_GENDIV_ID(5);      // Set division on Generic Clock Generator (GCLK) 5
  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 GCLK 5
                     GCLK_GENCTRL_SRC_DFLL48M |   // Set the clock source to 48MHz 
                     GCLK_GENCTRL_ID(5);          // Set clock source on GCLK 5
  while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization*/

  REG_GCLK_CLKCTRL = GCLK_CLKCTRL_CLKEN |         // Enable the generic clock...
                     GCLK_CLKCTRL_GEN_GCLK5 |     // ....on GCLK5
                     GCLK_CLKCTRL_ID_TCC2_TC3;    // Feed the GCLK5 to TCC2 and TC3
  while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization

  REG_EIC_EVCTRL |= EIC_EVCTRL_EXTINTEO3;                                 // Enable event output on external interrupt 3
  attachInterrupt(12, NULL, HIGH);                                        // Attach interrupts to digital pin 12 (external interrupt 3)
  
  REG_EVSYS_USER = EVSYS_USER_CHANNEL(1) |                                // Attach the event user (receiver) to channel 0 (n + 1)
                   EVSYS_USER_USER(EVSYS_ID_USER_TC3_EVU);                // Set the event user (receiver) as timer TC3

  REG_EVSYS_CHANNEL = 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

  REG_TC3_EVCTRL |= 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
                   
  REG_TC3_READREQ = TC_READREQ_RREQ |             // Enable a read request
                    TC_READREQ_ADDR(0x06);        // Offset of the CTRLC register
  while (TC3->COUNT16.STATUS.bit.SYNCBUSY);       // Wait for (read) synchronization
  REG_TC3_CTRLC |= TC_CTRLC_CPTEN1 |              // Enable capture on CC1
                   TC_CTRLC_CPTEN0;               // Enable capture on CC0
  while (TC3->COUNT16.STATUS.bit.SYNCBUSY);       // Wait for (write) synchronization

  //NVIC_DisableIRQ(TC3_IRQn);
  //NVIC_ClearPendingIRQ(TC3_IRQn);
  NVIC_SetPriority(TC3_IRQn, 0);      // Set the Nested Vector Interrupt Controller (NVIC) priority for TC3 to 0 (highest)
  NVIC_EnableIRQ(TC3_IRQn);           // Connect the TC3 timer to the Nested Vector Interrupt Controller (NVIC)
 
  REG_TC3_INTENSET = TC_INTENSET_MC1 |            // Enable compare channel 1 (CC1) interrupts
                     TC_INTENSET_MC0;             // Enable compare channel 0 (CC0) interrupts
  
  REG_TC3_CTRLA |= TC_CTRLA_PRESCALER_DIV16 |     // Set prescaler to 16, 16MHz/16 = 1MHz
                   TC_CTRLA_ENABLE;               // Enable TC3
  while (TC3->COUNT16.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(period);                      // Output the results
    SerialUSB.print(F("   "));
    SerialUSB.println(pulsewidth);
    periodComplete = false;                       // Start a new period
  }
}

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

  // Check for match counter 1 (MC1) interrupt
  if (TC3->COUNT16.INTFLAG.bit.MC1)           
  {
    REG_TC3_READREQ = TC_READREQ_RREQ |           // Enable a read request
                      TC_READREQ_ADDR(0x1A);      // Offset address of the CC1 register
    while (TC3->COUNT16.STATUS.bit.SYNCBUSY);     // Wait for (read) synchronization
    isrPulsewidth = REG_TC3_COUNT16_CC1;          // Copy the pulse-width
  }
}

Hi,

I was trying to port MartinL's (big thanks to you, otherwise it will take me a lot of time to figure out these things basing from the SAMD21 datasheet alone!) code to the Adafruit Feather M0 board. Everything compiles except that when I try to run the code, it gets stuck at this line:

attachInterrupt(12, NULL, HIGH);

I think this has something to do with clock synchronization stuff, but I have no idea what happens internally when I call the attachInterrupt function.

Any help to solve this issue is much appreciated.

Thanks a lot!

-Ruch

Hi Rucus,

I've checked the pinout diagram of the Adafruit Feather M0 and it's the same as the Zero, so there shouldn't be any difference. The sketch should wait for pulses on digital pin 12 on either board.

Are you sure that it isn't getting stuck at the line?:

while(!SerialUSB);                        // Wait for the SerialUSB port to be ready

This will wait until the console is opened before proceeding with the rest of the sketch. You can just comment out this line, if you prefer to execute the sketch without waiting.

Hi MartinL,

Thanks for your quick response.

I am not sure though if they have the same "pinouts", if indeed we are referring to the same thing. For example in Zero, pin 12 is referring to the MISO pin, while pin 12 of the Adafruit M0 adalogger is referring to a generic IO and the MISO is actually on pin 22.

I am not using the SerialUSB for printing texts. I have pasted the code below, which is basically your code that I changed a little bit to use the Serial object of printing texts over the terminal. :slight_smile:

void setup()   {                
  Serial.begin(115200);
  
  delay(7000);  //have ample time to catch text in serial monitor
  
  Serial.println("My Project");
  
  REG_PM_APBCMASK |= PM_APBCMASK_EVSYS;     // Switch on the event system peripheral
  
  REG_GCLK_GENDIV = GCLK_GENDIV_DIV(3) |    // Divide the 48MHz system clock by 3 = 16MHz
                    GCLK_GENDIV_ID(5);      // Set division on Generic Clock Generator (GCLK) 5
  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 GCLK 5
                     GCLK_GENCTRL_SRC_DFLL48M |   // Set the clock source to 48MHz 
                     GCLK_GENCTRL_ID(5);          // Set clock source on GCLK 5
  while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization*/

  REG_GCLK_CLKCTRL = GCLK_CLKCTRL_CLKEN |         // Enable the generic clock...
                     GCLK_CLKCTRL_GEN_GCLK5 |     // ....on GCLK5
                     GCLK_CLKCTRL_ID_TCC2_TC3;    // Feed the GCLK5 to TCC2 and TC3
  while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization

  REG_EIC_EVCTRL |= EIC_EVCTRL_EXTINTEO3;                                 // Enable event output on external interrupt 3
  attachInterrupt(12, NULL, HIGH);                                        // Attach interrupts to digital pin 12 (external interrupt 3)
  
  REG_EVSYS_USER = EVSYS_USER_CHANNEL(1) |                                // Attach the event user (receiver) to channel 0 (n + 1)
                   EVSYS_USER_USER(EVSYS_ID_USER_TC3_EVU);                // Set the event user (receiver) as timer TC3

  REG_EVSYS_CHANNEL = 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

  REG_TC3_EVCTRL |= 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
                   
  REG_TC3_READREQ = TC_READREQ_RREQ |             // Enable a read request
                    TC_READREQ_ADDR(0x06);        // Offset of the CTRLC register
  while (TC3->COUNT16.STATUS.bit.SYNCBUSY);       // Wait for (read) synchronization
  REG_TC3_CTRLC |= TC_CTRLC_CPTEN1 |              // Enable capture on CC1
                   TC_CTRLC_CPTEN0;               // Enable capture on CC0
  while (TC3->COUNT16.STATUS.bit.SYNCBUSY);       // Wait for (write) synchronization

  //NVIC_DisableIRQ(TC3_IRQn);
  //NVIC_ClearPendingIRQ(TC3_IRQn);
  NVIC_SetPriority(TC3_IRQn, 0);      // Set the Nested Vector Interrupt Controller (NVIC) priority for TC3 to 0 (highest)
  NVIC_EnableIRQ(TC3_IRQn);           // Connect the TC3 timer to the Nested Vector Interrupt Controller (NVIC)
 
  REG_TC3_INTENSET = TC_INTENSET_MC1 |            // Enable compare channel 1 (CC1) interrupts
                     TC_INTENSET_MC0;             // Enable compare channel 0 (CC0) interrupts
  
  REG_TC3_CTRLA |= TC_CTRLA_PRESCALER_DIV16 |     // Set prescaler to 16, 16MHz/16 = 1MHz
                   TC_CTRLA_ENABLE;               // Enable TC3
  while (TC3->COUNT16.STATUS.bit.SYNCBUSY);       // Wait for synchronization
  Serial.println("Done with TC init");
  
}

void loop() {
  static uint8_t lineSpacePosNow = 0, lineSpacePosIdx = 9;
  // read data from the GPS in the 'main loop'
  
  if (periodComplete)                             // Check if the period is complete
  {
    noInterrupts();                               // Read the new period and pulse-width
    period = isrPeriod;                   
    pulsewidth = isrPulsewidth;
    interrupts();
    Serial.print("P ");
    Serial.println(period, 2);
    Serial.print("PW "); 
    Serial.println(pulsewidth, 2);
    periodComplete = false;                       // Start a new period
  }
}

void TC3_Handler()                                // Interrupt Service Routine (ISR) for timer TC3
{     
  // Check for match counter 0 (MC0) interrupt
  if (TC3->COUNT16.INTFLAG.bit.MC0)             
  {
//    Serial.println("MO Intrpt");
    REG_TC3_READREQ = TC_READREQ_RREQ |           // Enable a read request
                      TC_READREQ_ADDR(0x18);      // Offset address of the CC0 register
    while (TC3->COUNT16.STATUS.bit.SYNCBUSY);     // Wait for (read) synchronization
    isrPeriod = REG_TC3_COUNT16_CC0;              // Copy the period  
    periodComplete = true;                        // Indicate that the period is complete
  }

  // Check for match counter 1 (MC1) interrupt
  if (TC3->COUNT16.INTFLAG.bit.MC1)           
  {
//    Serial.println("M1 Intrpt");
    REG_TC3_READREQ = TC_READREQ_RREQ |           // Enable a read request
                      TC_READREQ_ADDR(0x1A);      // Offset address of the CC1 register
    while (TC3->COUNT16.STATUS.bit.SYNCBUSY);     // Wait for (read) synchronization
    isrPulsewidth = REG_TC3_COUNT16_CC1;          // Copy the pulse-width
  }
}

The word "Done with TC init" is printed on my terminal if I comment the attachInterrupt(12, NULL, HIGH); line. I checked the function declaration at C:\Users\Ruchie\AppData\Local\Arduino15\packages\adafruit\hardware\samd\1.0.11\cores\arduino and it looks like it implements waiting for SYNCBUSY bit to get cleared. So I kind of stuck now as to what causing the code to stick somewhere and where it is happening (I hope I have a debugger to dig in further).

Thanks!

-Ruch

Hi MartinL,

Here is by the way the exact Adafruit M0 part that I am using.

Thanks,
Ruch

Hi Ruch,

The pinouts for the Adafruit Feather M0 Adalogger and the Arduino Zero are the same for digital pin 12. Both use PA19, which is physical pin 28 on the device itself.

I ran your code with an RC receiver outputting CPPM (Pulse Position Modulation) and get the following result giving you the pulse width and period in binary:

P 10111010101
PW 1011111111
P 10111001111
PW 11001100111111
P 10111010000
PW 10010100100
P 11000011110
PW 10010101010
P 10000101001
PW 10010011101
P 11010001101001
PW 10010100100

So that works OK.

If however I run it without the RC receiver attached, I get only:

My Project
Done with TC init

What pulse width and period waveform you using? The reason I ask is that if you're using a different signal you might need to reconfigure the TC3 timer.

Hi MartinL,

You are right. They indeed use the same physical pin. I did not realize that as I was only looking at their alternate pin functions. My bad.

I am using a magnetometer sensor that outputs a signal (pulses) that varies the period as a function of magnetic field strength. The frequency is from 50kHz to 150kHz within the measuring range. So the output from sensor is continuous pulses that I need to measure the period to get the field strength.

Thanks,
Ruch

At maximum frequency, I think that the ISR for TC3 will be called once in every ~6.67 us. Do you think it's too fast that I may need to do some configurations to ensure the micro can pickup the pulses? I am also running other modules in my system like the OLED and GPS, which I will deal with later once I have the capture mode up and running.

The 50kHz to 150kHz signal is at a high frequency with resepect to the SAMD21's 48MHz clock. The speed of TC3 really depends on the resolution (frequency steps) of your magnetometer, but will most likely require the TC3 timer to run at its maximum speed of 48MHz.

To set up TC3 to run at 48MHz, just change the generic clock divider from 3, down to 1:

REG_GCLK_GENDIV = GCLK_GENDIV_DIV(1) |    // Divide the 48MHz system clock by 1 = 48MHz
                    GCLK_GENDIV_ID(5);      // Set division on Generic Clock Generator (GCLK) 5
  while (GCLK->STATUS.bit.SYNCBUSY);        // Wait for synchronization

...and the TC3 timer prescaler from divide by 16, to also divide by 1:

REG_TC3_CTRLA |= TC_CTRLA_PRESCALER_DIV1 |      // Set prescaler to 1, 48MHz/1 = 48MHz
                   TC_CTRLA_ENABLE;               // Enable TC3
  while (TC3->COUNT16.STATUS.bit.SYNCBUSY);       // Wait for synchronization

The period values output from the timer will be multiples of 1/48MHz = 20.83ns.

Hi MartinL,

Thanks! I think that the code is working all along, but the problem is that the frequency of the signal that I am feeding to the TC3 timer is too fast so by the time that I kick in the attachInterrupt() in the code, my guess is that it is already calling for the ISR that is null (non-existent, but is the compiler supposed to warn me about it?) since I have it called before I have actually set up the TC3 interrupt at NVIC.

I made a little modification of your code and managed to make it work in my setup.

void setup()   {                
  Serial.begin(115200);
  
  delay(7000);  //have ample time to catch text in serial monitor
  
  Serial.println("My Project");

  pinMode(DCDC_EN, OUTPUT);
  
  REG_PM_APBCMASK |= PM_APBCMASK_EVSYS;     // Switch on the event system peripheral
  
  REG_GCLK_GENDIV = GCLK_GENDIV_DIV(1) |    // Divide the 48MHz system clock by 3 = 16MHz
                    GCLK_GENDIV_ID(5);      // Set division on Generic Clock Generator (GCLK) 5
  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 GCLK 5
                     GCLK_GENCTRL_SRC_DFLL48M |   // Set the clock source to 48MHz 
                     GCLK_GENCTRL_ID(5);          // Set clock source on GCLK 5
  while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization*/

  REG_GCLK_CLKCTRL = GCLK_CLKCTRL_CLKEN |         // Enable the generic clock...
                     GCLK_CLKCTRL_GEN_GCLK5 |     // ....on GCLK5
                     GCLK_CLKCTRL_ID_TCC2_TC3;    // Feed the GCLK5 to TCC2 and TC3
  while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization

  REG_EIC_EVCTRL |= EIC_EVCTRL_EXTINTEO3;                                 // Enable event output on external interrupt 3
  digitalWrite(DCDC_EN, 0);
  delay(3000);
//  attachInterrupt(MAGNETO_IN1, NULL, HIGH);                                        // Attach interrupts to digital pin 12 (external interrupt 3)
  
  REG_EVSYS_USER = EVSYS_USER_CHANNEL(1) |                                // Attach the event user (receiver) to channel 0 (n + 1)
                   EVSYS_USER_USER(EVSYS_ID_USER_TC3_EVU);                // Set the event user (receiver) as timer TC3

  REG_EVSYS_CHANNEL = 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

  REG_TC3_EVCTRL |= 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
                   
  REG_TC3_READREQ = TC_READREQ_RREQ |             // Enable a read request
                    TC_READREQ_ADDR(0x06);        // Offset of the CTRLC register
  while (TC3->COUNT16.STATUS.bit.SYNCBUSY);       // Wait for (read) synchronization
  REG_TC3_CTRLC |= TC_CTRLC_CPTEN1 |              // Enable capture on CC1
                   TC_CTRLC_CPTEN0;               // Enable capture on CC0
  while (TC3->COUNT16.STATUS.bit.SYNCBUSY);       // Wait for (write) synchronization

  //NVIC_DisableIRQ(TC3_IRQn);
  //NVIC_ClearPendingIRQ(TC3_IRQn);
  NVIC_SetPriority(TC3_IRQn, 0);      // Set the Nested Vector Interrupt Controller (NVIC) priority for TC3 to 0 (highest)
  NVIC_EnableIRQ(TC3_IRQn);           // Connect the TC3 timer to the Nested Vector Interrupt Controller (NVIC)
 
  REG_TC3_INTENSET = TC_INTENSET_MC1 |            // Enable compare channel 1 (CC1) interrupts
                     TC_INTENSET_MC0;             // Enable compare channel 0 (CC0) interrupts
  
  REG_TC3_CTRLA |= TC_CTRLA_PRESCALER_DIV1 |     // Set prescaler to 16, 16MHz/16 = 1MHz
                   TC_CTRLA_ENABLE;               // Enable TC3
  while (TC3->COUNT16.STATUS.bit.SYNCBUSY);       // Wait for synchronization

  Serial.println("Done with TC init");
  delay(1000);                        //wait for some delay before turning on power to magnetometers
    
  timer = millis();
  digitalWrite(DCDC_EN, 1);           
  delay(1000);                        //wait for some delay for power to stabilize
  attachInterrupt(MAGNETO_OUT1, NULL, HIGH);                       //enable interrupt
}

void loop() {
  static uint32_t sampleCntr=0;
  static uint8_t lineSpacePosNow = 0, lineSpacePosIdx = 9;
  // read data from the GPS in the 'main loop'

  // if millis() or timer wraps around, we'll just reset it
  if (timer > millis())  timer = millis();
  
  // approximately every 2 seconds or so, print out the current stats
  if (millis() - timer > 1000) { 
    timer = millis(); // reset the timer
//    sampleCntr++;
//    Serial.print("t ");
//    Serial.println(sampleCntr, DEC);
    if (periodComplete)                             // Check if the period is complete
    {
      period = isrPeriod;                   
      pulsewidth = isrPulsewidth;
      Serial.print("P ");
      Serial.println(period, DEC);
      Serial.print("PW "); 
      Serial.println(pulsewidth, DEC);
      periodComplete = false;                       // Start a new period
    }
    attachInterrupt(MAGNETO_OUT1, NULL, HIGH);     //re-arm interrupt
  }
}

void TC3_Handler()                                // Interrupt Service Routine (ISR) for timer TC3
{     
  detachInterrupt(MAGNETO_OUT1);
  // Check for match counter 0 (MC0) interrupt
  if (TC3->COUNT16.INTFLAG.bit.MC0)             
  {
    REG_TC3_READREQ = TC_READREQ_RREQ |           // Enable a read request
                      TC_READREQ_ADDR(0x18);      // Offset address of the CC0 register
    while (TC3->COUNT16.STATUS.bit.SYNCBUSY);     // Wait for (read) synchronization
    isrPeriod = REG_TC3_COUNT16_CC0;              // Copy the period  
    periodComplete = true;                        // Indicate that the period is complete
//    Serial.println("MO");
  }

  // Check for match counter 1 (MC1) interrupt
  if (TC3->COUNT16.INTFLAG.bit.MC1)           
  {
    REG_TC3_READREQ = TC_READREQ_RREQ |           // Enable a read request
                      TC_READREQ_ADDR(0x1A);      // Offset address of the CC1 register
    while (TC3->COUNT16.STATUS.bit.SYNCBUSY);     // Wait for (read) synchronization
    isrPulsewidth = REG_TC3_COUNT16_CC1;          // Copy the pulse-width
//    Serial.println("M1");
  }
}

I can see now the P and PW values being generated. I have also changed the TC3 clock settings to the one you suggested above so it now runs at max clock speed. Thank you for that wonderful piece advice!

I am, however, encountering some "weirdness" in my the behavior of my code. When the input frequency from the magnetometer is less than 55kHz, everything works fine (P and PW values get dumped on the terminal). If the frequency is greater than 55kHz, the code is kind of stuck somewhere. As I try to lower down the frequency again, the P and PW gets dumped on the terminal. I tried changing the length of characters to print but seem to not have any effect at all. Could it be something related to millis() function? I am thinking of using another Timer module to interrupt every 1000ms, but I would like to understand first what could have been the cause of the program freezing when input frequency is >55kHz.

Thanks a lot!

-Ruch

If the frequency is to high, the ISR will be called over and over again without letting time to the rest of the code to be executed. For example : if the code in you ISR take 18µs (1/55k) to execute and you have a 55kHz frequency input, the ISR will be re-called right after it finish executing over and over.

Hi Ruch,

As AloyseTech points out, it appears as though you've reached the limit SAMD21's capabilities, as the overhead of rapidly calling the interrupt service routine (ISR) is becoming too great for the processor to manage.

You also mention that you're planning to use an OLED display and GPS in your system. The GPS will also put a high demand on the processor, especially if you're parsing NMEA strings.

Is it necessary to use this magnetometer? Could you use one that uses an I2C or SPI bus for communication? This would significantly lighten the CPU load.

Thanks, AloyseTech and MartinL.

I am actually trying to disable the interrupt by issuing a detachInterrupt() right after the program enters the TC3 ISR, so the program only tries to read the output from magnetometer once every second one period (cycle) at a time in my hope to not get repetitive ISR events. I think it is worth checking though the detachInterrupt() function if there is any significant delay added to it before the actual write to the register to disable interrupt. I think what I am trying to do makes things more complicated.

What if instead of measuring the period of the signal, I actually implement a counter to count the number of pulses coming from the sensor and do a timer interrupt (using a separate timer module in SAMD) to read the count from another timer module? Basically I implement a free-running counter that I read at constant intervals in time and derive a transfer function to kind of convert those counts into equivalent intensity? I may need to reset the counters (I need 3 of them because I need to read data from 3 magnetometers) at every sampling time. So I think in this scheme I will be needing at least 4 timer modules from the SAMD. This solution kind of integrate the output from the sensor and I may lose some precision and speed there, but I think that will not matter much from what I am trying to do.

MartinL's question is a very good point. I wish I could use one that can talk via serial protocol. The thing is that I am tied to using these magnetometers because I need the extra sensitivity and directionality offered by these fluxgate magnetometer. A MEMS type is something I like to look into in the near future.

Depending on how often you need to read the magnetometer, the simplest way might be to set up your code so that it periodically activates the TC3 handler, takes a reading and then gets the handler to automatically switch itself off. That way this function won't be consuming all of the processor's time. I'm not sure if the SAMD21 can physically measure the 150kHz signal using TC capture, but if you keep your code in the handler to a minimum it might be worth a try?

It's possible to switch off interrupts from within the TC3 handler function using the timer's interrupt enable clear register (INTENCLR):

REG_TC3_INTENCLR = TC_INTENCLR_MC0;   // Disable period interrupts

To turn the handler function back on use the timer's interrupt enable set register (INTENSET):

REG_TC3_INTENSET = TC_INTENSET_MC0;  // Enable period interrpts

Thanks, MartinL.

I inserted the statements above to my code and I am getting an error somehow. Looks like the TC_INTENCLR_M0 and TC_INTENSET_M0 defines do not exist. I changed them to MC0 and MC1 so the statements looks like this

REG_TC3_INTENCLR = TC_INTENCLR_MC1 | TC_INTENCLR_MC0;

and

REG_TC3_INTENSET = TC_INTENSET_MC1 | TC_INTENSET_MC0;

You are right, still not fast enough to cope up with the input pulses. I want to start experimenting with timer counters and see if I get any luck on it.

Hi Ruch,

Thanks for the correction, I've amended it.

I ran a test with the TC capture code pared-down, measuring period only and managed to get it to work up to a maximum of 80kHz.