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)
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
}
}
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
}
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).
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.
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?