3
\$\begingroup\$

I have some software running on an STM32F103 (I'd also like to be able to use the F4) where I'd like to be able to 'timestamp' events (such as interrupts on external pins) to the nearest microsecond (if not better).

As the software will be running for some time I want to use a 64 bit timer, and to me it made sense to use the built in SysTick timer. I have created SysTickMajor (a 64 bit counter) which increments every time SysTick overflows, and a function getSystemTime which combines this counter and the current value of SysTick to get me an accurate 64 bit time:

#define SYSTICK_RANGE 0x1000000 // 24 bit
volatile long long SysTickMajor = SYSTICK_RANGE;

voif onInit(void) {
  SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8);
  SysTick_Config(SYSTICK_RANGE-1);
  /* Super priority for SysTick - is done over absolutely everything. */
  NVIC_InitStructure.NVIC_IRQChannel = SysTick_IRQn;
  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0;
  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
  NVIC_Init(&NVIC_InitStructure);
}

void SysTick_Handler(void) {
  SysTickMajor+=SYSTICK_RANGE;
}

long long getSystemTime() {
  long long t1 = SysTickMajor;
  long long time = (long long)SysTick->VAL;
  long long t2 = SysTickMajor;
  // times are different and systick has rolled over while reading
  if (t1!=t2 && time > (SYSTICK_RANGE>>1)) 
    return t2 - time;
  return t1-time;
}

The only problem is this doesn't appear to be 100% accurate - for instance, if I do the following:

bool toggle = false;
while (true) {
  toggle = !toggle;
  LED1.write(toggle);
  long long t = getSystemTime()() + 1000000;
  while (getSystemTime() < t);
}

I won't get a good square wave - occasionally there will be glitches (even though the SysTick interrupt is finished very quickly), because at points the time returned by getSystemTime is completely wrong.


UPDATE: When this happens (I am recording the time value on an interrupt triggered by an external event) I dump the last 3 values to serial. Repeatably I get things like:

>0x278-E2281A 
 0x278-E9999A 
 0x278-0001E5

>0x5BE-F11F51
 0x5BE-F89038
 0x5BE-0000FB

I've stuck dashes in to depict which bits come from SysTick.

Now just to avoid doubt, I've used the code from starblue below. I've also changed it to use a 32 bit value for SysTickMajor, and I've ANDed out SysTick->VAL, just in case.

It appears that the SysTick_Handler is not preempting the other interrupt!

Now I've checked over this and I did have the Preemption priorities wrong previously, but now I set NVIC_PriorityGroupConfig(NVIC_PriorityGroup_4); then set up the interrupts - SysTick preemption priority 0, and the other interrupt is 7.

Is there something else I have to do to get preemption to work?


UPDATE2: I created a tiny test case for this exact issue (SysTick not preempting other interrupts), and have put it here:

STM32 Interrupt Priority (preemption) Problems


So what's wrong with the code above? Is there a better way to get a good high resolution timer? It seems like something obvious that people would want to do but I'm struggling to find examples.

\$\endgroup\$
  • \$\begingroup\$ Maybe you will try DMA? Increase some variable on timer overflow. \$\endgroup\$ – Eddy_Em Jul 16 '13 at 18:50
  • \$\begingroup\$ Do you have an example of incrementing a variable with DMA? Just a note: There should really be interrupt disable/enable around each mention of SysTickMajor in getSystemTime - however even this doesn't stop the occasional glitch \$\endgroup\$ – Gordon Williams Jul 16 '13 at 19:23
  • \$\begingroup\$ Your main mistake is that you run while (getSystemTime() < t); : this function longs for some time, that's why you won't be able to get time perfectly. Work on interrupts or make atomic operation of time calculation (SysTick interrupt would modify some long long variable by DMA or other mechanism, and last your line may be like while(SystemTime - SystemTime0 < 1000000) \$\endgroup\$ – Eddy_Em Jul 17 '13 at 4:54
  • \$\begingroup\$ It's not like it's a little bit off - occasionally the pulse could be anywhere between 0 and the length it was supposed to be. I have now modified the question after I tried some other things. However if you do have a solution which involves DMA (I'm not sure how it can be done?) I'd be very interested. \$\endgroup\$ – Gordon Williams Jul 17 '13 at 18:31
5
\$\begingroup\$

As you've discovered, handling counter overflow with an interrupt handler is tricky and race-prone. Even with your priority boost, you'll get the wrong answer if interrupts are disabled while you read the counter. There's a much simpler technique which works if you're not reading the clock from interrupt context (note that I'm using the DWT cycle counter here, which is built into the processor thus fast and simple):

uint64_t last_cycle_count_64 = 0;

// Call at least every 2^32 cycles (every 59.6 seconds @ 72 MHz).
// Do not call from interrupt context!
uint64_t GetCycleCount64() {
  last_cycle_count_64 += DWT->CYCCNT - (uint32_t)(last_cycle_count_64);
  return last_cycle_count_64;
}

If you need to call it from interrupt context (or with a preemptive OS):

volatile uint64_t last_cycle_count_64 = 0;

// Call at least every 2^32 cycles (every 59.6 seconds @ 72 MHz).
uint64_t GetCycleCount64() {
  uint32_t primask;
  asm volatile ("mrs %0, PRIMASK" : "=r"(primask));
  asm volatile ("cpsid i");  // Disable interrupts.
  int64_t r = last_cycle_count_64;
  r += DWT->CYCCNT - (uint32_t)(r);
  last_cycle_count_64 = r;
  asm volatile ("msr PRIMASK, %0" : : "r"(primask));  // Restore interrupts.
  return r;
}

You may need to enable the cycle counter first:

DWT->CYCCNT |= DWT_CTRL_CYCCNTENA_Msk;  // Set bit 0.
\$\endgroup\$
  • \$\begingroup\$ Nice - so the very act of reading the counter clears it? I guess potentially this could be called on the SysTick timer overflow IRQ, which would ensure that it never got to the point of overflowing. \$\endgroup\$ – Gordon Williams May 22 '15 at 8:26
  • \$\begingroup\$ No, reading the counter does not clear it. This code simply updates a larger 64-bit counter using the current value of the smaller 32-bit counter. In principle, you could do the same thing with the SysTick register, but you'd have to perform your update more frequently. \$\endgroup\$ – nneonneo Jul 22 '18 at 3:52
  • \$\begingroup\$ This general method seems like a good one if you make a custom timer for it. However, the DWT->CYCCNT counter only increments when the debug probe is attached. If you want to use it with the debug probe detached you'll need to enable it separately. However doing so this may allow an attacker access to all the debug registers so it seems inadvisable to rely on DWT->CYCCNT. \$\endgroup\$ – Evan Aug 29 at 21:23
  • \$\begingroup\$ Evan states: "the DWT->CYCCNT counter only increments when the debug probe is attached". At least on my platform (STM32F407), the CYCCNT counter appears to increment regardless of whether a debug probe is attached. I'm looking at the ARMv7-M Architecture Reference Manual (DDI0403E_d), and I don't see any mention of requiring a debug probe. I do see that the counter halts when the processor is halted, which is typically desireable: "CYCCNT does not increment when the processor is halted in Debug state." \$\endgroup\$ – SethML Sep 3 at 17:56
  • \$\begingroup\$ Ah, you do have to enable CYCCNT by setting the low bit of DWT_CTRL. Added a description of this to the answer. \$\endgroup\$ – SethML Sep 3 at 18:06
3
\$\begingroup\$

I just found the answer from a very helpful poster on the STM32 forum:

The following isn't correct. SysTick is a 'System Handler', and as such the priority isn't set in this way at all:

  NVIC_InitStructure.NVIC_IRQChannel = SysTick_IRQn;
  NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; // Highest priority
  NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0;
  NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
  NVIC_Init(&NVIC_InitStructure);

It's actually set with:

  NVIC_SetPriority(SysTick_IRQn, 0);

Calling that code instead solves the problem!

So my version of getSystemTime could have had a problem, and starblue's method is nicer than mine - however the actual cause of the problems was the above.

Just thought I'd add one other possible improvement to getSystemTime that I tried as well:

do {
 major0 = SysTickMajor;
 minor = SysTick->VAL;
 major1 = SysTickMajor;
while (major0 != major1);
return major0 - minor;

This will effectively negate the problem where you could (somehow) end up with the SysTick being implemented twice in very quick succession.

\$\endgroup\$
2
\$\begingroup\$

Your code has two problems:

  • You don't know whether the interrupt that increased SysTickMajor happened before or after you read the timer value into time.

  • Operations on 64-bit values are not atomic on a 32-bit system. So the value for SysTickMajor could be updated by the interrupt between reading the two 32-bit words.

I would do it as follows:

major0 = SysTickMajor;
minor1 = SysTick->VAL;
major1 = SysTickMajor;

minor2 = SysTick->VAL;
major2 = SysTickMajor;

if ( major0 == major1 )
{
    time = major1 + minor1;
}
else
{
    time = major2 + minor2;
}

Here you assume that if SysTickMajor changed during the first block of assignments it won't change again in the second block. (This assumption could be wrong if this code is interrupted during this routine and can't run for a long time.)

See also this question on Stackoverflow.

\$\endgroup\$
  • \$\begingroup\$ Thanks - I've now started using your code (see modified question) but I still have the problem! I've even swapped the 64 bit values down to 32 bit in order to be sure that wasn't an issue. It now looks like the SysTick interrupt is not being called when SysTick overflows, rather that there being a synchronization issue. \$\endgroup\$ – Gordon Williams Jul 17 '13 at 18:29
  • \$\begingroup\$ I prefer to read low/high/low and repeat if needed. Among other things, that approach will work correctly even if "logical carry propagation" is slow. Suppose, for example, that when by the time the processor reads the data from SysTick->VAL it has already started executing the instruction to read SysTickMajor into major1. The minor1 value could reflect a timer that has wrapped, even though major1 would not. Using low/high/low, such things won't happen unless the relative timing difference between high- and low-word actions exceeds the time between the two low-word reads. \$\endgroup\$ – supercat Jul 17 '13 at 19:12

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.