For the Arduino Zero, I would like to generate a pulse signal on D20between 10us - 50us depended on certain calculated value in software. The pulse should start on pin D10 rising edge. What is the best way to achieve this as much as possible within the hardware timers?
Edit: the oneshot timer will be repeated frequently
The following code uses the event sytem via the External Interrupt Controller (EIC), to route a trigger source signal from PA18 (D10) to timer TCC0. Upon receiving the event, the TCC0 timer retriggers and outputs a 10us oneshot pulse on PA22 (SDA).
The code code also generates a 100us (10kHz) test output using timer TCC2 on PA16 (D11). To test just plug this pin into PA18 (D10).
Employing the event system is a more efficient way of routing the trigger signal to timer TCC0 than an interrupt service routine. In this instance, the event system generates a progagation delay of around 120ns.
Here's the code:
// Generate 10us Oneshot pusle on PA22 upon receiving rising edge trigger on PA18
// 100us (10kHz) trigger source generated on PA16
void setup()
{
PM->APBCMASK.reg |= PM_APBCMASK_EVSYS; // Switch on the event system peripheral
////////////////////////////////////////////////////////////////////////////////////////
// Genric Clock Initialisation
////////////////////////////////////////////////////////////////////////////////////////
GCLK->CLKCTRL.reg = GCLK_CLKCTRL_CLKEN | // Enable the generic clock...
GCLK_CLKCTRL_GEN_GCLK0 | // ....on GCLK0 at 48MHz
GCLK_CLKCTRL_ID_TCC0_TCC1; // Feed the GCLK0 to TCC0 and TCC1
while (GCLK->STATUS.bit.SYNCBUSY); // Wait for synchronization
GCLK->CLKCTRL.reg = GCLK_CLKCTRL_CLKEN | // Enable the generic clock...
GCLK_CLKCTRL_GEN_GCLK0 | // ....on GCLK0 at 48MHz
GCLK_CLKCTRL_ID_TCC2_TC3; // Feed the GCLK0 to TCC2 and TC3
while (GCLK->STATUS.bit.SYNCBUSY); // Wait for synchronization
////////////////////////////////////////////////////////////////////////////////////////
// TCC2 Initialisation - generate pulse every 100us (10kHz) on port pin PA16
////////////////////////////////////////////////////////////////////////////////////////
// Enable the port multiplexer on port pin PA16
PORT->Group[PORTA].PINCFG[16].bit.PMUXEN = 1;
// Set-up the pin as a TCC2/WO[0] peripheral on PA16
PORT->Group[PORTA].PMUX[16 >> 1].reg |= PORT_PMUX_PMUXE_E;
TCC2->WAVE.reg = TCC_WAVE_WAVEGEN_NPWM; // Set the TCC2 timer to normal PWM mode (NFRQ)
while(TCC2->SYNCBUSY.bit.WAVE); // Wait for synchronization
TCC2->PER.reg = 4799; // Set the period (PER) register for a PWM frequency of 10kHz (100us)
while(TCC2->SYNCBUSY.bit.PER); // Wait for synchronization
TCC2->CC[0].reg = 2400; // Set the counter compare 0 (CC0) register for a PWM duty-cycle of 50%
while(TCC2->SYNCBUSY.bit.CC0); // Wait for synchronization
TCC2->CTRLA.bit.ENABLE = 1; // Enable TCC2
while (TCC2->SYNCBUSY.bit.ENABLE); // Wait for synchronization
////////////////////////////////////////////////////////////////////////////////////////
// TCC0 Initialisation - output 10us oneshot pulse upon receiving an event on PA22
////////////////////////////////////////////////////////////////////////////////////////
// Enable the port multiplexer on port pin PA22
PORT->Group[PORTA].PINCFG[22].bit.PMUXEN = 1;
// Set-up the pin as TCC0/WO[4] peripheral on PA22
PORT->Group[PORTA].PMUX[22 >> 1].reg |= PORT_PMUX_PMUXE_F;
TCC0->WAVE.reg = TCC_WAVE_WAVEGEN_NPWM; // Set the TCC0 timer to normal PWM mode (NPWM)
while(TCC0->SYNCBUSY.bit.WAVE); // Wait for synchronization
TCC0->PER.reg = 959; // Set the period (PER) register for oneshot pulse width
while(TCC0->SYNCBUSY.bit.PER); // Wait for synchronization
TCC0->CC[0].reg = 480; // Set the counter compare 0 (CC0) register for a pulse width of 10us
while(TCC0->SYNCBUSY.bit.CC0); // Wait for synchronization
TCC0->CTRLBSET.reg = TCC_CTRLBSET_ONESHOT; // Enable oneshot operation
while(TCC0->SYNCBUSY.bit.CTRLB); // Wait for synchronization
TCC0->DRVCTRL.reg |= TCC_DRVCTRL_NRE4; // Activate the Non Recoverable Fault Enable to drive pin low after oneshot
/////////////////////////////////////////////////////////////////////////////////////////////////////////
// Event System Initialisation - set up to allow TCC0 to receive incoming pulses on PA18
/////////////////////////////////////////////////////////////////////////////////////////////////////////
// Enable the port multiplexer on port pin PA18
PORT->Group[PORTA].PINCFG[18].bit.PMUXEN = 1;
// Set-up the pin as an EIC (interrupt) peripheral on PA18
PORT->Group[PORTA].PMUX[18 >> 1].reg |= PORT_PMUX_PMUXE_A;
EIC->EVCTRL.reg |= EIC_EVCTRL_EXTINTEO2; // Enable event output on external interrupt 3
EIC->CONFIG[0].reg |= EIC_CONFIG_SENSE2_HIGH; // Set event detecting a HIGH level
EIC->INTENCLR.reg = EIC_INTENCLR_EXTINT2; // Disable interrupts on external interrupt 3
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_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_2) | // Set event generator (sender) as external interrupt 2
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_RETRIGGER; // Retrigger timer TCC0 on receiving event
TCC0->CTRLA.bit.ENABLE = 1; // Enable TCC0
while (TCC0->SYNCBUSY.bit.ENABLE); // Wait for synchronization
}
void loop(){}
LeCrAm:
Hi Juraj, thanks I will look into this! Do you think I can meet the performance as described above? Because Triac is mostly used around 50 or 60 Hz?
it uses the event system as Martin's so there is no problem with frequency
Thanks both of you! I can confirm it works. I checked by scope. In the past with the 8-bit AVR I could dream setting the registers for counters, I'm having some more difficulty with the samd21.
Employing the event system is a more efficient way of routing the trigger signal to timer TCC0 than an interrupt service routine.
Thanks, I was also looking for best solution to prevent too much prop. delay.
This means that I'm able to control the pulse-width by changing the value:
if, at a certain point, I would like to disable the "oneshot feature" temporally only for the pin D10 event, but still have the option to fire the oneshot timer manually by:
TCC0->CTRLBSET.reg = TCC_CTRLBSET_CMD_RETRIGGER; // Retrigger the timer's One/Multi-Shot pulse
while (TCC0->SYNCBUSY.bit.CTRLB); // Wait for synchronization
Above code doesn't seem to work, however if I use this, it does:
EIC->EVCTRL.reg &= ~EIC_EVCTRL_EXTINTEO2;
At the moment my code works quite ok using 2 timers, but I'm realizing that maybe there is a smarter way to achieve what I want:
I would like to output a 10KHz PWM signal on D20 with fixed small duty-cycle, until I get a variable fequency signal from D10 that will always be higher frequency than the 10KHz signal, lets assume 30KHz. I would like D20 to follow the falling edges of the 30KHz signal. I would like to change the duty-cycle in software.
TCC0->EVCTRL.reg &= ~TCC_EVCTRL_EVACT0_Msk; // Turn off the action on event 0
does turn off the events to the timer. It's just that I made a mistake in the initial post, corrected it, but perhaps this was too late.
In any case, as you mention the line:
EIC->EVCTRL.reg &= ~EIC_EVCTRL_EXTINTEO2;
does the same thing, but further upstream, as it blocks the event passing through the External Interrupt Controller (EIC) instead.
I would like to output a 10KHz PWM signal on D20 with fixed small duty-cycle, until I get a variable fequency signal from D10 that will always be higher frequency than the 10KHz signal, lets assume 30KHz. I would like D20 to follow the falling edges of the 30KHz signal. I would like to change the duty-cycle in software.
The issue is that a 10kHz continuous PWM signal doesn't know when the next 30kHz input pulse is coming. If it arrives too late, the 10kHz signal could have finished its timer cycle and started on the next one.
Using oneshot gets around this problem, since once the oneshot pulse has ended, the timer cycle is effectively over until it's retriggered again.
I'm using a seperate 10KHz ISR routine to manually trigger the Oneshot timer.
Then I would like to reset (clear) the seperate actual counter value each time I get a trigger on D10. If the frequency of D10 is higher, this will prevent the 10KHz ISR routine fires. I have everything in place, my 10KHz timer code:
void init_start_timer()
{
// Set up the generic clock (GCLK4) used to clock timers
REG_GCLK_GENDIV = GCLK_GENDIV_DIV(1) | // Divide the 48MHz clock source by divisor 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
// Feed GCLK4 to TC4 and TC5
REG_GCLK_CLKCTRL = GCLK_CLKCTRL_CLKEN | // Enable GCLK4 to TC4 and TC5
GCLK_CLKCTRL_GEN_GCLK4 | // Select GCLK4
GCLK_CLKCTRL_ID_TC4_TC5; // Feed the GCLK4 to TC4 and TC5
while (GCLK->STATUS.bit.SYNCBUSY); // Wait for synchronization
REG_TC4_COUNT16_CC0 = 0x12C0; // Set the TC4 CC0 register as the TOP value in match frequency mode
while (TC4->COUNT16.STATUS.bit.SYNCBUSY); // Wait for synchronization
//NVIC_DisableIRQ(TC4_IRQn);
//NVIC_ClearPendingIRQ(TC4_IRQn);
NVIC_SetPriority(TC4_IRQn, 0); // Set the Nested Vector Interrupt Controller (NVIC) priority for TC4 to 0 (highest)
NVIC_EnableIRQ(TC4_IRQn); // Connect TC4 to Nested Vector Interrupt Controller (NVIC)
REG_TC4_INTFLAG |= TC_INTFLAG_OVF; // Clear the interrupt flags
REG_TC4_INTENSET = TC_INTENSET_OVF; // Enable TC4 interrupts
// REG_TC4_INTENCLR = TC_INTENCLR_OVF; // Disable TC4 interrupts
REG_TC4_CTRLA |= TC_CTRLA_PRESCALER_DIV1 | // Set prescaler to 1, 48MHz/1024 = 46.875kHz
TC_CTRLA_WAVEGEN_MFRQ | // Put the timer TC4 into match frequency (MFRQ) mode
TC_CTRLA_ENABLE; // Enable TC4
while (TC4->COUNT16.STATUS.bit.SYNCBUSY); // Wait for synchronization
}
void TC4_Handler() // Interrupt Service Routine (ISR) for timer TC4
{
// Check for overflow (OVF) interrupt
if (TC4->COUNT16.INTFLAG.bit.OVF && TC4->COUNT16.INTENSET.bit.OVF)
{
// Manual triggering
if (TCC0->STATUS.bit.STOP) // Check if the previous pulse is complete
{
TCC0->CTRLBSET.reg = TCC_CTRLBSET_CMD_RETRIGGER; // Retrigger the timer's One/Multi-Shot pulse
while (TCC0->SYNCBUSY.bit.CTRLB); // Wait for synchronization
}
REG_TC4_INTFLAG = TC_INTFLAG_OVF; // Clear the OVF interrupt flag
}
}
Which register I have to control to reset the counter value to achieve the ISR routine not to fire?
The easiest way is probably to set the TCC0's duty-cycle to 0, using the counter compare (CC0) register. The timer will retrigger as usual, but no pulse will be output:
TCC0->CC[0].reg = 0; // Set the duty-cycle to 0
while(TCC0->SYNCBUSY.bit.CC0); // Wait for synchronization
Hi Martin, maybe I didn't explained well, The only thing I would like to achieve is to prevent that TC4_Handler will be fired by resetting/clearing/restarting the counter with higher frequency signal, this means if this higher frequency signal is dissapears, the counter will not be resetted anymore and TC4 will fire with a frequency of 10KHz:
void init_start_timer()
{
// Set up the generic clock (GCLK4) used to clock timers
REG_GCLK_GENDIV = GCLK_GENDIV_DIV(1) | // Divide the 48MHz clock source by divisor 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
// Feed GCLK4 to TC4 and TC5
REG_GCLK_CLKCTRL = GCLK_CLKCTRL_CLKEN | // Enable GCLK4 to TC4 and TC5
GCLK_CLKCTRL_GEN_GCLK4 | // Select GCLK4
GCLK_CLKCTRL_ID_TC4_TC5; // Feed the GCLK4 to TC4 and TC5
while (GCLK->STATUS.bit.SYNCBUSY); // Wait for synchronization
REG_TC4_COUNT16_CC0 = 0x12C0; // Set the TC4 CC0 register as the TOP value in match frequency mode
while (TC4->COUNT16.STATUS.bit.SYNCBUSY); // Wait for synchronization
//NVIC_DisableIRQ(TC4_IRQn);
//NVIC_ClearPendingIRQ(TC4_IRQn);
NVIC_SetPriority(TC4_IRQn, 0); // Set the Nested Vector Interrupt Controller (NVIC) priority for TC4 to 0 (highest)
NVIC_EnableIRQ(TC4_IRQn); // Connect TC4 to Nested Vector Interrupt Controller (NVIC)
REG_TC4_INTFLAG |= TC_INTFLAG_OVF; // Clear the interrupt flags
REG_TC4_INTENSET = TC_INTENSET_OVF; // Enable TC4 interrupts
// REG_TC4_INTENCLR = TC_INTENCLR_OVF; // Disable TC4 interrupts
REG_TC4_CTRLA |= TC_CTRLA_PRESCALER_DIV1 | // Set prescaler to 1, 48MHz/1024 = 46.875kHz
TC_CTRLA_WAVEGEN_MFRQ | // Put the timer TC4 into match frequency (MFRQ) mode
TC_CTRLA_ENABLE; // Enable TC4
while (TC4->COUNT16.STATUS.bit.SYNCBUSY); // Wait for synchronization
}
void TC4_Handler() // Interrupt Service Routine (ISR) for timer TC4
{
// Check for overflow (OVF) interrupt
if (TC4->COUNT16.INTFLAG.bit.OVF && TC4->COUNT16.INTENSET.bit.OVF)
{
// Manual triggering
if (TCC0->STATUS.bit.STOP) // Check if the previous pulse is complete
{
TCC0->CTRLBSET.reg = TCC_CTRLBSET_CMD_RETRIGGER; // Retrigger the timer's One/Multi-Shot pulse
while (TCC0->SYNCBUSY.bit.CTRLB); // Wait for synchronization
}
REG_TC4_INTFLAG = TC_INTFLAG_OVF; // Clear the OVF interrupt flag
}
}
An additional question, I'm using TCC0 for the oneshot functionality, is it also possible to use an additional/independent oneshot timer on TCC1? This oneshot timer on TCC1 should be triggered by software only, without external trigger by digital input.