3

SysTick usage description

SysTick

We have a custom board based on a STM32G483 MCU (Cortex M4). We use the SysTick as a reference for software timers. The SysTick reload register is set to 0x00FFFFFF so as to have the fewest interrupts. The SysTick is clocked with the CPU clock at 128MHz, which means there is a SysTick interrupt every 131ms or so. The interrupt increments a tick counter by the load value + 1.

#define SYSTICK_LOAD_VALUE 0x00FFFFFFU

static volatile uint64_t _ticks;

void 
systick_interrupt(void)
{
    _ticks += SYSTICK_LOAD_VALUE + 1;
}

We then use the current value register to get the number of clock cycles elapsed in the current counting cycle to compute the current time.

uint64_t 
systick_get_ticks(void)
{
    return _ticks - SysTick->VAL;
}

Software timers

We can then use this value for different software timers that can theoretically count in the order of magnitude of a few clock cycles.

void
timer_start(struct timer *timer)
{
    timer->_start_tick = systick_get_ticks();
}

bool
timer_check_ticks(const struct timer timer, uint64_t duration)
{
    uint64_t difference = systick_get_ticks() - timer._start_tick;
    return (difference >= duration);
}

With function call overheads, it's impossible to be accurate to the tick, but this should still be accurate for longer periods, like 1us (128 ticks) or 1ms (128 000). Sure, the software timer will probably overshoot by some clock cycles, depending on the main loop frequency, but it shouldn't undershoot.

Tests

We were seeing some weird behavior with these timers, so we decided to test them by having the simplest main loop to toggle a GPIO that we could probe.

int
main(void)
{
    // Clock, NVIC, SysTick and GPIO initialisation
    struct pin test_gpio;
    struct timer test_timer;
    timer_start(&test_timer);
    while (TRUE) {   
        if (timer_check_ticks(test_timer, 128000U)) { // 128000 ticks @ 128MHz ==> 1ms
            gpio_toggle(test_gpio);
            timer_start(&test_timer);
        }
    }
}

With this, we were expecting a square waveform with a 50% duty cycle and a 2ms period (500Hz), which is what I was getting most of the time. However, some pulses were sometimes way shorter, for example at 185us. While trying to find the source of the problem, we also noticed that when compiling after any modification would change the length of the shorter pulse, but while the code was executing, this duration didn't seem to change.

We've checked that the core clock does run at 128MHz, the SysTick is configured as we want it, we've written a snippet to check that the SysTick interrupt is triggered at the right frequency, and that the systick_get_ticks() function returns a reliable number. This leads us to believe that the problem comes from the timer code itself, but we can't seem to find the issue.

Code is compiled with clang (--target=arm-none-eabi), STM32 HAL libraries are NOT used

Clifford
  • 88,407
  • 13
  • 85
  • 165
JLegros
  • 41
  • 5

4 Answers4

3

Consider:

#define SYSTICK_LOAD_VALUE 0x00FFFFFFU

static volatile uint32_t systick_reload_count = 0 ;

void systick_interrupt(void)
{
    systick_reload_count++ ;
}

uint64_t systick_get_ticks(void)
{
    uint32_t reload_count = 0 ; 
    uint64_t ticks = 0 ;
    do
    {
        reload_count = systick_reload_count ;

        ticks = (reload_count * (SYSTICK_LOAD_VALUE + 1)) + 
                (SYSTICK_LOAD_VALUE - SysTick->VAL + 1) ;

    } while( systick_reload_count != reload_count ) ;
}

Here the ISR is simpler (faster) and accessing systick_reload_count is an atomic operation (i.e cannot be interrupted) on this 32-bit device.

The while loop in systick_get_ticks ensures that if the reload occurs during the non-atomic ticks calculation, the new systick_reload_count is obtained and ticks recalculated. The loop should never normally iterate more than twice (you'd have to be interrupted for the 131ms, in which case you have other problems!), so remains deterministic.

An important aspect of this solution is that the calculation is performed in the local copy of the reload count, not on the volatile count itself.

Clifford
  • 88,407
  • 13
  • 85
  • 165
  • the systick load value - val needs to be anded with 0x00FFFFFF for that math to work – old_timer Jul 08 '21 at 11:59
  • This idea seemed promising. However, this unfortunately didn't solve the problem. We had previously tried to mask the systick interrupt completely while doing the ticks calculations. As a side note, the `ticks` calculation in your code needs a `+` instead of a `-` between the parentheses I believe. – JLegros Jul 08 '21 at 11:59
  • I'll look at the problem again - perhaps there are other issues I didn't spot, but your method is nonetheless flawed. Disabling the interrupt would be is seriously flawed - the SYSTICK still runs and you'll still have an inconsistent between the `VAL` count and the reload. – Clifford Jul 08 '21 at 12:05
  • @old_timer Explain further. On Cortex-M note that SYSTICK is a 24bit counter. 0x00FFFFFF is its maximum value. – Clifford Jul 08 '21 at 12:08
  • ahhhhhh, sorry. it is when you take two samples of val and subtract them you need the mask, you are taking max minus current, nevermind.... – old_timer Jul 08 '21 at 12:10
  • pretty common when using systick on an cortex-m to have issues not masking the result and I was expecting the OP's problem to be that. there is a race condition so it could just be the race not the math. – old_timer Jul 08 '21 at 12:11
  • @old_timer ... and it down counts of course. – Clifford Jul 08 '21 at 12:12
  • yeah, I was thinking there was an up/down thing going on here too – old_timer Jul 08 '21 at 12:13
  • @Clifford I forgot to update the interrupt handler when testing your solution. It actually works, so thank you ! – JLegros Jul 08 '21 at 12:13
  • there is the that is not how volatile works issue. there is the race condition (read the volatile value multiple times, but since you are adding an offset that changes, this makes it harder), up/down and perhaps other issues. – old_timer Jul 08 '21 at 12:15
  • @old_timer the addition is performed on the local atomically captured copy if the volatile, not the volatile. There are caveats here that the 32 bit assignment will be atomic. But this is system level code, very specific to this platform. – Clifford Jul 08 '21 at 12:25
  • you can find lots of dicussions on what volatile can and cannot do for you in general (not to be assumed reliable for communcating between isr code and forground code)(its intended use is for access to control and status registers). implementation varies as with all other similar habits that just happen to work on some compilers. that is what I meant. can resolve it by inspecting the compiler output (for each build) – old_timer Jul 08 '21 at 13:47
  • there is a race condition here which can possibly give bad results off by one roll over. – old_timer Jul 08 '21 at 13:47
  • in the original question – old_timer Jul 08 '21 at 13:53
1

Your function systick_get_ticks is not thread/interrupt safe. If it gets interrupted by the systick the return value will be incorrect.

For a thread/interrupt safe version you could use:

https://github.com/tcv-git/goodmicro/blob/master/lib/goodmicro_armv7m/uptime.h

https://github.com/tcv-git/goodmicro/blob/master/lib/goodmicro_armv7m/uptime_sysclk.s

(Also, please stop naming variables starting with an underscore. These names are reserved, you get undefined behavior if use them).

Tom V
  • 4,827
  • 2
  • 5
  • 22
  • You should include a solution in the answer rather then link to an answer elsewhere. – Clifford Jul 08 '21 at 11:05
  • The bit about underscores is worth a comment, not part of an answer. Unless you believe the undefined behaviour to be the cause of the problem? Could be - its undefined - but clearly unlikley. – Clifford Jul 08 '21 at 14:14
  • If you did post the code however I'd point out, no documentation, no explanation, and assembler on a C tagged question. – Clifford Jul 08 '21 at 16:13
  • The answer is really only the first paragraph. I'll put the link in a comment next time. – Tom V Jul 09 '21 at 14:03
  • I agree; I am being an ass. Apologies. That is a valid answer. – Clifford Jul 09 '21 at 14:05
  • I have a bee in my bonnet about links being better than copy+paste at the moment because this week I spent some time trying to put author and licence information back into code that certain people in my company had copied into our commercial product from stackexchange without attribution. Obviously if you create an answer specifically for a question then the stackexchange address is the address, but most questions already have a better answer somewhere else on the web (and who knows, I might have had time to write some documentation for that code by the time some future person clicks on it). – Tom V Jul 09 '21 at 14:24
  • Regarding your confidence that GitHub links are "permanent" I came across this: https://stackoverflow.com/questions/6759592/how-to-enable-int128-on-visual-studio/39016672#39016672 - every link is a 404. – Clifford Jul 13 '21 at 09:01
  • Well nothing is permanent if you live long enough, but I trust github approximately as much as stack exchange. In that case the answerer has deliberately chosen to delete the repository. – Tom V Jul 13 '21 at 12:22
  • The point is if SO expires, or the question is deleted the question and answer expire at the same time, so it is academic. – Clifford Jul 13 '21 at 12:25
1

Ended up implementing it in assembly :


#define _SYSTICK_RELOAD_VALUE 0xFFFFFF

.macro mov32, reg, val
    movw \reg, #:lower16:\val
    movt \reg, #:upper16:\val
.endm

.data
    @ static uint64_t _ticks = _SYSTICK_RELOAD_VALUE;
    _ticks: .quad _SYSTICK_RELOAD_VALUE

.text
    .thumb_func
    .global systick_init
    @ void systick_init(void)
    systick_init:
        @ Set systick reload value register
        mov32   r0, _SYSTICK_RELOAD_VALUE
        mov32   r1, 0xE000E014
        str     r0, [r1]

        @ Set the systick current value register to 0
        mov32   r0, 0
        mov32   r1, 0xE000E018
        str     r0, [r1]

        @ Enable systick, enable interrupt, and use processor clock
        mov32   r0, 0x7
        mov32   r1, 0xE000E010
        str     r0, [r1]

        @ Return
        bx      lr

    .thumb_func
    .global systick_interrupt
    @ void systick_interrupt(void)
    systick_interrupt:
        @ Load tick counter, MSB last (guard MSB with LSB)
        mov32   r2, _ticks
        ldr     r0, [r2] @ LSB
        ldr     r1, [r2, 4] @ MSB

        @ Add reload value + 1
        mov32   r3, _SYSTICK_RELOAD_VALUE + 1
        adds    r0, r0, r3 @ LSB
        adc     r1, r1, 0 @ MSB

        @ Write back tick counter, MSB first (guard MSB with LSB)
        str     r1, [r2, 4] @ MSB
        str     r0, [r2] @ LSB

        @ Return
        bx      lr

    .thumb_func
    .global systick_get_ticks
    @ uint64_t systick_get_ticks(void)
    systick_get_ticks:
        push    {r4-r5}

        @ Constants
        mov32   r4, _ticks
        mov32   r5, 0xE000E018

    1:
        @ Load tick counter and current systick value
        ldrex   r2, [r4] @ Tick counter LSB into r2
        ldr     r1, [r4, 4] @ Tick counter MSB into r1
        ldr     r0, [r5] @ Current systick value into r0

        @ Attempt to dummy write back the LSB of the tick counter
        @ If the operation fails this means the tick counter was accessed
        @ concurrently, or an interrupt fired and we must try again
        strex   r3, r2, [r4]
        cmp     r3, 0
        bne     1b

        @ Compute global tick value into r0 and r1
        subs    r0, r2, r0
        sbc     r1, r1, 0

        @ Return the result in r0 and r1
        pop     {r4-r5}
        bx      lr

ldrex and strex instructions serve as mutexes, ensuring the value of _ticks hasn't been modified between the two instructions.

JLegros
  • 41
  • 5
0

the stm32's systick isn't easy to use,if u exploited stm32 systick init code,u would find it has been modified at lastest 3 times,even it will be modified after get it,like this:

static __INLINE uint32_t SysTick_Config(uint32_t ticks)
{
  if (ticks > SysTick_LOAD_RELOAD_Msk)  return (1);            /* Reload value impossible */
 
  SysTick->LOAD  = (ticks & SysTick_LOAD_RELOAD_Msk) - 1;      /* set reload register */
  NVIC_SetPriority (SysTick_IRQn, (1<<__NVIC_PRIO_BITS) - 1);  /* set Priority for Cortex-M0 System Interrupts */
  SysTick->VAL   = 0;                                          /* Load the SysTick Counter Value */
  SysTick->CTRL  |= SysTick_CTRL_CLKSOURCE_Msk |
                   SysTick_CTRL_TICKINT_Msk   |
                   SysTick_CTRL_ENABLE_Msk;                    /* Enable SysTick IRQ and SysTick Timer */
  return (0);                                                  /* Function successful */

so i suggest u use other TIMERs or amend stm32 library(but so troublesome).

  • This is just a initial config code intended to be run only once, so *"it has been modified at least 3 times"* doesn't make sense. Configuring other timers isn't any easier than this. – Tagli Jul 09 '21 at 05:50