Timer Synchronization in Arduino Zero

Hi, I am doing a project which using timers to do hardware synchronization to various sensors with Arduino Zero. However, there is around 2ms difference between the rising edge of the signal pulses from various timers. May I know is it possible to do timer synchronization in Arduino Zero?

Had been searching the internet for few days and briefly go through the datasheet but still didn't manage to find any hit. There is GTCCR register in Arduino Mega for this purpose but cannot find the relevant information in Arduino Zero.

Timer synchronization in Arduino Mega

Depending on accuracy required but consider driving them all from the same clock. Clock skew alone will give you problems. The best is to sync off a common clock or pulse. In doing this, software can nail you if you do not have the same amount of cycle time on each.

HI @chunjye0808

Timer output synchronisation is something that's not mentioned in the SAMD21's datasheet, even though, as you mention, the AVR processors have provision for it.

One way however, is to use the SAMD21's event system: a 12 channel peripheral-to-peripheral highway. This highway allows a "generator's" (or sender's) signal to be received by multiple "users" (or receivers) on a single channel.

In our case the sender is the CPU, generating a software generated pulse on to a given event system channel. The receivers are the TCC timers that should acquire the pulse simultaneously and are configured to retrigger (restart) upon receiving it.

The following code sets up two TCC timers: TCC0 and TCC1, both on channel 0 and on port pins PA08 and PA14 respectively. Both are configured identically and set to output a 1kHz, 50% duty-cycle PWM signal upon receiving the pulse on the event system. Note that this test code waits for 5 seconds after initalisation before triggering with the delay() function. This can be removed for normal operation.

Furthermore, there's no need to stop the timers during intialisation, or after enabling them, since it turns out that they won't start until they're retriggered anyway.

One final point, is that to use the software trigger on the Event System requires the channel to use a synchronous path and therefore it has to be clocked with a generic clock. (Usually this isn't necessary when using an asynchronous path).

Here's the code:

// Synchronise TCC0 and TCC1 timers with a software trigger on the Event System (channel 0)

void setup()
{
  /////////////////////////////////// PM & GCLK  ///////////////////////////////////
  
  PM->APBCMASK.reg |= PM_APBCMASK_EVSYS;           // Switch on the event system peripheral
  
  GCLK->CLKCTRL.reg = GCLK_CLKCTRL_CLKEN |         // Enable GCLK0 as a clock source
                      GCLK_CLKCTRL_GEN_GCLK0 |     // Select GCLK0 at 48MHz
                      GCLK_CLKCTRL_ID_EVSYS_0;     // Route GCLK0 to Event System channel 0
  
  GCLK->CLKCTRL.reg = GCLK_CLKCTRL_CLKEN |         // Enable GCLK0 as a clock source
                      GCLK_CLKCTRL_GEN_GCLK0 |     // Select GCLK0 at 48MHz
                      GCLK_CLKCTRL_ID_TCC0_TCC1;   // Route GCLK0 to TCC0 and TCC1
 
  ///////////////////////////////////// EVSYS ////////////////////////////////////////
  
  EVSYS->CTRL.bit.GCLKREQ = 1;                     // Keep event system generic clock turned on
  
  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
                    
  EVSYS->USER.reg = EVSYS_USER_CHANNEL(1) |                      // Attach the event user (receiver) to channel 0 (n + 1)
                    EVSYS_USER_USER(EVSYS_ID_USER_TCC1_EV_0);    // Set the event user (receiver) as timer TCC1

  EVSYS->CHANNEL.reg = EVSYS_CHANNEL_EDGSEL_RISING_EDGE |        // Rising edge detection
                       EVSYS_CHANNEL_PATH_SYNCHRONOUS |          // Set event path as synchronous
                       EVSYS_CHANNEL_CHANNEL(0);                 // Attach the generator (sender) to channel 0
  
  ///////////////////////////////////// TCC0 ////////////////////////////////////////
  
  PORT->Group[PORTA].PINCFG[8].bit.PMUXEN = 1;     // Enable the peripheral multiplexer on port pin PA08

  PORT->Group[PORTA].PMUX[8 >> 1].reg |= PORT_PMUX_PMUXE_F;     // Select multiplexer channel F for PA08
   
  TCC0->WAVE.reg = TCC_WAVE_WAVEGEN_NPWM;          // Setup single slope PWM on TCC0
  while (TCC0->SYNCBUSY.bit.WAVE);                 // Wait for synchronization

  TCC0->CTRLA.reg = TC_CTRLA_PRESCALER_DIV8 |      // Set prescaler to 8, 48MHz/8 = 6MHz
                    TC_CTRLA_PRESCSYNC_PRESC;      // Set the reset/reload to trigger on prescaler clock
  
  TCC0->PER.reg = 5999;                            // Set the frequency of the PWM on TCC0 to 1kHz: 48MHz / (8 * 5999 + 1) = 1kHz
  while (TCC0->SYNCBUSY.bit.PER);                  // Wait for synchronization

  TCC0->EVCTRL.reg = TCC_EVCTRL_TCEI0 |            // Enable the TCC event 0 input                     
                     TCC_EVCTRL_EVACT0_RETRIGGER;  // Retrigger the timer upon receiving an event                    

  TCC0->CC[0].reg = 3000;                          // TCC0 CC0 - 50% duty cycle
  while (TCC0->SYNCBUSY.bit.CC0);                  // Wait for synchronization
  
  TCC0->CTRLA.bit.ENABLE = 1;                      // Enable the TCC1 counter
  while (TCC0->SYNCBUSY.bit.ENABLE);               // Wait for synchronization

  ///////////////////////////////////// TCC1 ////////////////////////////////////////
  
  PORT->Group[PORTA].PINCFG[14].bit.PMUXEN = 1;    // Enable the peripheral multiplexer on port pin PA14

  PORT->Group[PORTA].PMUX[14 >> 1].reg |= PORT_PMUX_PMUXE_F;  // Select multiplexer channel F for PA14
  
  TCC1->WAVE.reg = TCC_WAVE_WAVEGEN_NPWM;          // Setup single slope PWM on TCC1
  while (TCC1->SYNCBUSY.bit.WAVE);                 // Wait for synchronization

  TCC1->CTRLA.reg = TC_CTRLA_PRESCALER_DIV8 |      // Set prescaler to 8, 48MHz/8 = 6MHz
                    TC_CTRLA_PRESCSYNC_PRESC;      // Set the reset/reload to trigger on prescaler clock
  
  TCC1->PER.reg = 5999;                            // Set the frequency of the PWM on TCC1 to 1kHz: 48MHz / (8 * 5999 + 1) = 1kHz
  while (TCC1->SYNCBUSY.bit.PER);                  // Wait for synchronization
  
  TCC1->EVCTRL.reg = TCC_EVCTRL_TCEI0 |            // Enable the TCC event 0 input                     
                     TCC_EVCTRL_EVACT0_RETRIGGER;  // Retrigger the timer upon receiving an event

  TCC1->CC[0].reg = 3000;                          // TCC1 CC0 - 50% duty cycle
  while (TCC1->SYNCBUSY.bit.CC0);                  // Wait for synchronization
  
  TCC1->CTRLA.bit.ENABLE = 1;                      // Enable the TCC1 counter
  while (TCC1->SYNCBUSY.bit.ENABLE);               // Wait for synchronization
  
  delay(5000);                                     // Add 5 second test delay

  ///////////////////////////////// Software Trigger /////////////////////////////////////

  EVSYS->CHANNEL.reg = EVSYS_CHANNEL_CHANNEL(0) |  // Select Event System channel 0
                       EVSYS_CHANNEL_SWEVT;        // Generate software trigger on Event System channel 0
}

void loop(){}

Thanks for your awesome help and I managed to achieve the timer synchronization on Arduino Zero with your help and the code provided.

I have some follow up question then I will close this thread. I am trying to using D3 (PA09) but it sem something wrong with my setup. The only two lines I changed are:

  PORT->Group[PORTA].PINCFG[9].bit.PMUXEN = 1;    // Enable the peripheral multiplexer on port pin PA9

  PORT->Group[PORTA].PMUX[9 >> 1].reg |= PORT_PMUX_PMUXE_F;  // Select multiplexer channel F for PA9

Besides, I am trying to use TCC2 timer and TC0 (8 bit) timer also but no luck. No output PWM from the selected pin. Below is the added code for TCC2 timer:

  GCLK->CLKCTRL.reg = GCLK_CLKCTRL_CLKEN |         // Enable GCLK0 to TCC2 (and TC3)
                      GCLK_CLKCTRL_GEN_GCLK0 |     // Select GCLK0
                      GCLK_CLKCTRL_ID_TCC2_TC3;    // Feed GCLK0 to TCC2 (and TC3)

  EVSYS->USER.reg = EVSYS_USER_CHANNEL(1) |                      // Attach the event user (receiver) to channel 0 (n + 1)
                    EVSYS_USER_USER(EVSYS_ID_USER_TCC2_EV_0);      // Set the event user (receiver) as timer TCC2

  ///////////////////////////////////// TCC2 ////////////////////////////////////////
  PORT->Group[PORTA].PINCFG[9].bit.PMUXEN = 1;    // Enable the peripheral multiplexer on port pin PA9

  PORT->Group[PORTA].PMUX[9 >> 1].reg |= PORT_PMUX_PMUXE_F;  // Select multiplexer channel F for PA9

  TCC2->WAVE.reg = TCC_WAVE_WAVEGEN_NPWM;          // Setup single slope PWM on TCC2
  while (TCC2->SYNCBUSY.bit.WAVE);                 // Wait for synchronization

  TCC2->CTRLA.reg = TC_CTRLA_PRESCALER_DIV64 |      // Set prescaler to 8, 48MHz/8 = 6MHz
                    TC_CTRLA_PRESCSYNC_PRESC;      // Set the reset/reload to trigger on prescaler clock
  
  TCC2->PER.reg = 24999;  
  while (TCC2->SYNCBUSY.bit.PER);                  // Wait for synchronization

  TCC2->EVCTRL.reg = TCC_EVCTRL_TCEI0 |            // Enable the TCC event 0 input                     
                     TCC_EVCTRL_EVACT0_RETRIGGER;  // Retrigger the timer upon receiving an event                    

  TCC2->CC[0].reg = 12500;    
  while (TCC2->SYNCBUSY.bit.CC0);                  // Wait for synchronization
  
  TCC2->CTRLA.bit.ENABLE = 1;                      // Enable the TCC2 counter
  while (TCC2->SYNCBUSY.bit.ENABLE);               // Wait for synchronization

Any parts that I did wrong? If you have any useful links that related to this topic, feel free to share to me and I can try to explore myself. Thanks

Hi @chunjye0808

Glad to hear that it does what require it to do.

My apologies, but to answer your questions requires a bit of explanation about the port pin registers, which isn't that straightforward, so please bear with me:

Regarding the port pin multiplexer, each pin has it's own PINCFG register to enable the peripheral multiplexer, so PINCFG[0] references PA00, PINCFG[1] => PA01, PINCFG[2] => PA02, and so on.

The PMUX register however deals with port pin even and odd pairs, so PMUX[0] references PA00 and PA01, PMUX[1] => PA02 and PA03, PMUX[2] => PA04 and PA05 and so on.

This means that there are only half the number of PMUX registers compared with PINCFG, hence the line PMUX[9 >> 1], which shifts 9 one bit to the right. This is essentially the same as dividing 9 by 2 = PMUX[4].

The PMUX register has definitions that select both the pin multiplexer switch positions: A to H, plus whether the port pin is either odd or even. For instance, PA09 is odd, while PA08 is even.

For example to access the multiplexer switch F on odd port pin PA09, you just need to change the definition from even to odd, (note the O instead of an E) in PORT_PMUX_PMUXO_F:

 PORT->Group[PORTA].PMUX[9 >> 1].reg |= PORT_PMUX_PMUXO_F;  // Select multiplexer channel F for PA09

To find which timer channels are connected to what pin, it's necessary to look at the "PORT Function Multiplex" table in the SAMD21 datasheet.

Note that just to make matters more confusing, timer TCC0's channels WO[0] to WO[3] are repeated on WO[4] to WO[7].

Likewise, timer TCC1's channels WO[0] to WO[1] are repeated on WO[2] to WO[3].

TCC2 just has WO[0] and WO[1] with no repeats.

Therefore in total TCC0 has 4 channels, while TCC1 and TCC2 each have 2.

You'll find the TCC2 timer outputs on port pins: PA12 and PA13, as well as PA16 and PA17, all on multiplexer switch E. These equate to Arduino Zero pins: MISO, GPIO, D11 and D13 respectively.

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.