Systick - Timing on the Arduino Zero?????

I'm struggling to understand how the micros(), millis(), delay() and delaymicroseconds() work on the Arduino Zero.

They seem incredibly poor in accuracy/consistency and I'm struggling to understand why.

Using simple code to write out the microseconds every delay(1000) I get the following:

Zero - Delay
Time: 20876
Time: 1032178
Time: 2045180
Time: 3058174
Time: 4071176
Time: 5084177
Time: 6097178
Time: 7110179
Time: 8123174
Time: 9136176
Time: 10149177
Time: 11163174
Time: 12177178
Time: 13191176
Time: 14205179

And then using delayMicroseconds(1000000) I get:
Zero - DelayMicroseconds
Time: 33376
Time: 1046501
Time: 2061709
Time: 3076918
Time: 4092126
Time: 5107334
Time: 6122543
Time: 7137751
Time: 8152959
Time: 9168168
Time: 10183376
Time: 11199626
Time: 12215876
Time: 13232126
Time: 14248376

The Zero obviously uses the Systick Timer. So I started comparing to the Due. I get the following results using delay with the Due:

Due - delay
Time: 2014
Time: 1002013
Time: 2002013
Time: 3002013
Time: 4002013
Time: 5002013
Time: 6002013
Time: 7002013
Time: 8002013
Time: 9002013
Time: 10002013
Time: 11002013
Time: 12002013
Time: 13002013
Time: 14002013
Time: 15002013
Time: 16002013
Time: 17002013
Time: 18002013
Time: 19002013
Time: 20002013
Time: 21002013
Time: 22002013
Time: 23002013

This is far more consistent.

Does anyone have an intricate knowledge of how Systick is handled in the Arduino Zero? Or can point somewhere to read up on???

All help greatly appreciated!!

milli_due.ino (321 Bytes)

Weird. It looks to me like Zero uses essentially the same code for micros() (ArduinoCore-samd/delay.c at master · arduino/ArduinoCore-samd · GitHub) as Due (https://github.com/arduino/Arduino/blob/master/hardware/arduino/sam/cores/arduino/wiring.c#L35)

Hi 927456,

I've also been experiencing problems with micros() on the Zero. I tried calling the micros() function from within a number of interrupts, but it kept on adding or subtracting 1000us. I've resorted instead to using timer TC3 to generate my own micros2() function, a workaround that's similar to the code used for old AVR Arduinos, but accurate to 1us, (instead of the 4us for micros() on the AVRs).

As westfw mentions, it's a bit weird, as the code's very similar to the Due's, which appears to work OK. The only difference being that the Due also checks the systick action bit (SYSTICKACT) in its System Handler Control and State register (SHCSR), but this register isn't available on the ARM Cortex M0+ used on the Zero.

The code for the systick can be found in the Zero's "delay.c" file:

// Interrupt-compatible version of micros
// Theory: repeatedly take readings of SysTick counter, millis counter and SysTick interrupt pending flag.
// When it appears that millis counter and pending is stable and SysTick hasn't rolled over, use these
// values to calculate micros. If there is a pending SysTick, add one to the millis counter in the calculation.
uint32_t micros( void )
{
  uint32_t ticks, ticks2;
  uint32_t pend, pend2;
  uint32_t count, count2;

  ticks2  = SysTick->VAL;
  pend2   = !!(SCB->ICSR & SCB_ICSR_PENDSTSET_Msk)  ;
  count2  = _ulTickCount ;

  do
  {
    ticks=ticks2;
    pend=pend2;
    count=count2;
    ticks2  = SysTick->VAL;
    pend2   = !!(SCB->ICSR & SCB_ICSR_PENDSTSET_Msk)  ;
    count2  = _ulTickCount ;
  } while ((pend != pend2) || (count != count2) || (ticks < ticks2));

  return ((count+pend) * 1000) + (((SysTick->LOAD  - ticks)*(1048576/(VARIANT_MCK/1000000)))>>20) ;
  // this is an optimization to turn a runtime division into two compile-time divisions and
  // a runtime multiplication and shift, saving a few cycles

The systick is essentially a 24-bit timer counter running at 48MHz that ticks down to zero. At the beginning of each cycle the systick timer is loaded with the value 47999. At 48MHz it takes exactly 1ms for the timer to reach zero (underflow), whereupon it's reloaded and continues to count down oncemore.

What the micros() function does is to take a snapshot of the number of milliseconds (ulTickCount), pending interrupt bit (SCB->ICSR & SCB_ICSR_PENDSTSET_Msk) and the current systick counter value (Systick->VAL). It then takes another snapshot and compares these corresponding values to check that the systick timer hasn't underflowed in the meantime.

If it has underflowed, then we wait until the timer's been reloaded and everything has settled. The function then calculates the time elapsed in microseconds using the formula:

return ((count+pend) * 1000) + (((SysTick->LOAD  - ticks)*(1048576/(VARIANT_MCK/1000000)))>>20) ;

Note that VARIANT_MCK is 48MHz.

It calculates the number of microseconds from the number of timer ticks and appends this to the number of milliseconds elapsed mulitplied by 1000. If the timer has been reloaded the number of milliseconds is incremented by 1, by adding pending bit (pend) to the millisecond count (count).

Hi MartinL,

Do you have the code for micros2() function?

In the background I'm looking into why all the functions that rely on systick don't correctly in the Zero, but in the short term I have some code that could do with an accurate/stable microsecond timer.

I'm fairly new to Arduino, so could figure out how to make my own micros2 function but if you were willing to share it would save me some valuable time at the moment.

Many Thanks

9227465

I have some code that uses the SAMD "RTC" module as the counter for micros() and millis()
It was written for SAMD10, so it might take a bit of fixing up to work on SAMD21.
(Note that this would interfere with any other use of the RTC, and I HAVE seen libraries for the RTC that use it as a more conventional 1s clocked Time-of-day thing.)

(Check out the notes on the RTC in the README.MD file on the top-level page, too...)

Hi 927465,

Here's the micros2() code:

// Micros2 function
volatile unsigned long timer2Counter;

void setup() {
  Serial.begin(115200);                           // Set up the serial port for test purposes
  
  // Set up the generic clock (GCLK4) used to clock timers
  REG_GCLK_GENDIV = GCLK_GENDIV_DIV(3) |          // Divide the 48MHz clock source by divisor 3: 48MHz/3=16MHz
                    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 TCC2 (and TC3)
  REG_GCLK_CLKCTRL = GCLK_CLKCTRL_CLKEN |         // Enable GCLK4 to TCC2 (and TC3)
                     GCLK_CLKCTRL_GEN_GCLK4 |     // Select GCLK4
                     GCLK_CLKCTRL_ID_TCC2_TC3;    // Feed GCLK4 to TCC2 (and TC3)
  while (GCLK->STATUS.bit.SYNCBUSY);              // Wait for synchronization

  REG_TC3_COUNT8_PER = 0xFF;                      // Set period register to 255
  while (TC3->COUNT8.STATUS.bit.SYNCBUSY);        // Wait for synchronization

  REG_TC3_INTENSET = /*TC_INTENSET_MC1 | TC_INTENSET_MC0 |*/ TC_INTENSET_OVF; // Enable TC3 interrupts
  
  //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 TC3 to Nested Vector Interrupt Controller (NVIC)

  // Set the TC3 timer to tick at 2MHz, or in other words a period of 0.5us - timer overflows every 128us 
  // timer counts up to (up to 255 in 128us)
  REG_TC3_CTRLA |= TC_CTRLA_PRESCALER_DIV8 |      // Set prescaler to 8, 16MHz/8 = 2MHz
                   TC_CTRLA_MODE_COUNT8 |         // Set the counter to 8-bit mode
                   TC_CTRLA_ENABLE;               // Enable TC3
  while (TC3->COUNT8.STATUS.bit.SYNCBUSY);        // Wait for synchronization

  REG_TC3_READREQ = TC_READREQ_RCONT |            // Enable a continuous read request
                    TC_READREQ_ADDR(0x10);        // Offset of the 8 bit COUNT register
  while (TC3->COUNT8.STATUS.bit.SYNCBUSY);        // Wait for (read) synchronization
}

void loop() {
  Serial.println(micros2());                      // Testing the micros2() function
  delay(1000);                                    // Wait 1 second
}

// Micros2 is used to measure the receiver pulsewidths down to 1us accuracy
uint32_t micros2() 
{
  uint32_t m;
  uint8_t t;
     
  noInterrupts();                                 // Disable interrupts
  m = timer2Counter;                              // Get the number of overflows
  t = REG_TC3_COUNT8_COUNT;                       // Get the current TC3 count value

  if (TC3->COUNT8.INTFLAG.bit.OVF && (t < 255))   // Check if the timer has just overflowed (and we've missed it)
  {
    m++;                                          // Then in this case increment the overflow counter
  }  
  interrupts();                                   // Enable interrupts
  return ((m << 8) + t) / 2;                      // Return the number of microseconds that have occured since the timer started
}

// This ISR is called every 128us
void TC3_Handler()           // ISR TC3 overflow callback function
{
  if (TC3->COUNT8.INTFLAG.bit.OVF)
  {
    timer2Counter++;           // Increment the overflow counter
  }
  REG_TC3_INTFLAG = TC_INTFLAG_OVF;   // Rest the overflow interrupt flag
}
1 Like