Search code examples
ctimerarmembeddedstm32

Weird behavior for SysTick based timer for stm32g483


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


Solution

  • 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.