I'm attempting to upgrade the microcontroller in one of my companies products (a soil moisture sensor) from Atmega1284P to Seeedstudio Xiao which uses the SAMD21. To do this I must measure the frequency output of a LCR circuit at ~50MHz. The frequency is divided down by 256 using a binary ripple counter before reaching the micro, so the task is reduced to counting a 1 second sample of a signal at ~200 kHz. The attached code shows how it was done with the Atmega1284P, but I'm lost trying to work out how to do it with the SAMD21 architecture.
Many folk have supplied useful threads on related subjects, such as MartinL at Arduino Zero TCC Capture - Arduino Zero - Arduino Forum. However I'm not smart enough to put their contributions together & make it work, so if there is someone out there that is willing to assist me convert my old code to the new processor then I'd be most grateful.
I've modified the code so that it should work with D6 as the input on the Xiao. I've tested the code on my custom SAMD21 board with 200kHz pulses, but using a different input pin.
The code receives the incoming pulses on D6 and routes them to the TCC0 timer via the EIC (External Interrupt Controller) and the Event System, (a 12-channel peripheral-to-peripheral highway). The TCC0 is configured to simply count the number of input events or pulses.
The TCC2 timer meanwhile is clocked from the on-board 32.768kHz external crystal and uses this to time a period of 1 second, over which the number of pulses are counted.
Calling the startConversion() function retriggers both timers. When the 1 second has elapsed, the TCC2's interrupt service routine is called and number of pulses counted by TCC0 is displayed on the Serial console.
I appreciate that this code is somewhat different from your AVR code. It's just that the SAMD21 operates in a completely different way.
Here's the code:
// Count the number of pulses on pin D6 (PB08) over a 1 second period
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_TCC0_TCC1; // As a clock source for TCC0 and TCC1
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
//////////////////////////////////////////////////////////////////////////////////////////////
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->CTRLBSET.reg = TCC_CTRLBSET_ONESHOT; // Enable oneshot operation
while(TCC2->SYNCBUSY.bit.CTRLB); // Wait for synchronization
NVIC_SetPriority(TCC2_IRQn, 0); // Set the Nested Vector Interrupt Controller (NVIC) priority for TCC2 to 0 (highest)
NVIC_EnableIRQ(TCC2_IRQn); // Connect TCC2 to Nested Vector Interrupt Controller (NVIC)
TCC2->INTENSET.reg = TCC_INTENSET_OVF; // Enable overflow (OVF) interrupts on TCC2
TCC2->CTRLA.bit.ENABLE = 1; // Enable TCC2
while (TCC2->SYNCBUSY.bit.ENABLE); // Wait for synchronization
////////////////////////////////////////////////////////////////////////////////////////
// TCC0 Initialisation - measurement counter: counts the number of incoming of pulses
////////////////////////////////////////////////////////////////////////////////////////
PORT->Group[PORTB].PINCFG[8].bit.PMUXEN = 1; // Enable the port multiplexer on port pin PB08 (D6)
PORT->Group[PORTB].PMUX[8 >> 1].reg |= PORT_PMUX_PMUXE_A; // Set-up PB08 (D6) as an EIC (interrupt)
EIC->EVCTRL.reg |= EIC_EVCTRL_EXTINTEO8; // Enable event output on external interrupt 8
EIC->CONFIG[1].reg |= EIC_CONFIG_SENSE0_HIGH; // Set interrupt to detect a HIGH level
EIC->INTENCLR.reg = EIC_INTENCLR_EXTINT8; // Clear the interrupt flag on channel 8
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_TCC0_EV_0); // Set the event user (receiver) as timer TCC0, event 0
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_8) | // Set event generator (sender) as external interrupt 8
EVSYS_CHANNEL_CHANNEL(0); // Attach the generator (sender) to channel 0
TCC0->EVCTRL.reg = TCC_EVCTRL_TCEI0 | // Enable TCC0 event 0 inputs
TCC_EVCTRL_EVACT0_INC; // Increment the TCC0 counter on receiving an event 0
TCC0->CTRLA.bit.ENABLE = 1; // Enable TCC0
while (TCC0->SYNCBUSY.bit.ENABLE); // Wait for synchronization
}
void loop()
{
startConversion(); // Start a conversion over the 1 second integration window
delay(2000); // Wait for 1 second
}
void startConversion()
{
TCC2->CTRLBSET.reg = TCC_CTRLBSET_CMD_RETRIGGER; // Retrigger the timer TCC2 to start the integration window
while (TCC2->SYNCBUSY.bit.CTRLB); // Wait for synchronization
TCC0->CTRLBSET.reg = TCC_CTRLBSET_CMD_RETRIGGER; // Retrigger the timer TCC0 to start the count
while (TCC0->SYNCBUSY.bit.CTRLB); // Wait for synchronization
}
void TCC2_Handler()
{
TCC2->INTFLAG.bit.OVF = 1; // Clear the TCC2 overflow interrupt flag
TCC0->CTRLBSET.reg = TCC_CTRLBSET_CMD_READSYNC; // Trigger a read synchronization on the COUNT register
while (TCC0->SYNCBUSY.bit.CTRLB); // Wait for the CTRLB register write synchronization
while (TCC0->SYNCBUSY.bit.COUNT); // Wait for the COUNT register read sychronization
SerialUSB.println(TCC0->COUNT.reg); // Print the result
}
While modifying your code to suit my application (which involves making a single measurement of the frequency output, measuring a few other voltages, transmitting the data to a basestation then sending the Xiao into deep sleep @8uA) it was necessary to add a delay of 1100 ms at the end of startConversion(), otherwise various events happen out of order, such as bad reads of the frequency upon startup or other measurements in startConversion() happening out of order with the frequency measurement.
Can you please us if there is a method of detecting when the 1 second timer at the end of startConversion() is up so values can be returned to the main loop() after TCC2_handler() has done it's job? Curiously I added a flag to the end of TCC2_handler() that I tried to trap in startConversion() with a while loop, but the code just hung. Rather than solving this curiosity I switched to adding delay(1100) and it worked fine. Somehow this feels like cheating and it would be more efficient to have an event generated by TCC2 to trap.
You should be able to simply use a volatile boolean variable flag, to indicate to the main loop() that the TCC2_Handler() interrupt service routine has finished.
For example, by adding the variables at the start of the sketch:
volatile boolean countComplete = false; // Count complete flag
volatile uint32_t count; // Variable to hold the TCC0 count value
Set the countComplete flag in the interrupt service routine and copy the count value:
void TCC2_Handler()
{
TCC2->INTFLAG.bit.OVF = 1; // Clear the TCC2 overflow interrupt flag
TCC0->CTRLBSET.reg = TCC_CTRLBSET_CMD_READSYNC; // Trigger a read synchronization on the COUNT register
while (TCC0->SYNCBUSY.bit.CTRLB); // Wait for the CTRLB register write synchronization
while (TCC0->SYNCBUSY.bit.COUNT); // Wait for the COUNT register read sychronization
count = TCC0->COUNT.reg; // Read the result
countComplete = true; // Set the count complete flag
}
Finally, in the main loop() block and wait for the interrupt service routine to complete and when finished clear the countComplete flag and print the result, or put the microcontroller to sleep:
void loop()
{
startConversion(); // Start a conversion over the 1 second integration window
while(!countComplete); // Block until the TCC2_Handler() function returns
countComplete = false; // Clear the count complete flag
SerialUSB.println(count); // Print the result
delay(1000); // Wait for 1 second
}
It's probably best, if you could please start a new topic on the Arduino Zero forum for this issue, as period and pulse width measurement is somewhat different from frequency measurement.
It just makes everything more readable, if there's a separation between the two discussions on different topics.