【问题标题】:Weird behavior for SysTick based timer for stm32g483stm32g483 基于 SysTick 的计时器的奇怪行为
【发布时间】:2021-07-08 08:53:27
【问题描述】:

SysTick 使用说明

系统提示

我们有一个基于 STM32G483 MCU (Cortex M4) 的定制板。我们使用 SysTick 作为软件定时器的参考。 SysTick 重载寄存器设置为 0x00FFFFFF 以使中断最少。 SysTick 的 CPU 时钟频率为 128MHz,这意味着每 131ms 左右就有一个 SysTick 中断。中断将滴答计数器增加负载值 + 1。

#define SYSTICK_LOAD_VALUE 0x00FFFFFFU

static volatile uint64_t _ticks;

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

然后我们使用当前值寄存器获取当前计数周期中经过的时钟周期数来计算当前时间。

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

软件计时器

然后,我们可以将此值用于不同的软件定时器,这些定时器理论上可以按几个时钟周期的数量级计数。

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);
}

由于函数调用开销,不可能精确到滴答声,但这在更长的时间段内仍然应该是准确的,例如 1us(128 个滴答声)或 1ms(128 000)。当然,软件定时器可能会超出一些时钟周期,具体取决于主循环频率,但它不应该低于。

测试

我们看到这些计时器出现了一些奇怪的行为,因此我们决定通过最简单的主循环来切换我们可以探测的 GPIO 来测试它们。

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);
        }
    }
}

有了这个,我们期待一个具有 50% 占空比和 2ms 周期 (500Hz) 的方波,这是我大部分时间得到的。然而,有些脉冲有时更短,例如 185us。在试图找到问题的根源时,我们也注意到,在编译后任何修改都会改变较短脉冲的长度,但在代码执行时,这个持续时间似乎没有改变。

我们检查了核心时钟确实以 128MHz 运行,SysTick 配置为我们想要的,我们编写了一个 sn-p 来检查 SysTick 中断是否以正确的频率触发,并且 @ 987654325@ 函数返回一个可靠的数字。这让我们相信问题出在计时器代码本身,但我们似乎无法找到问题所在。

使用 clang (--target=arm-none-eabi) 编译代码,不使用 STM32 HAL 库

【问题讨论】:

    标签: c timer arm embedded stm32


    【解决方案1】:

    考虑:

    #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 ) ;
    }
    

    这里的 ISR 更简单(更快),访问systick_reload_count 是这个 32 位设备上的原子操作(即不能被中断)。

    systick_get_ticks 中的 while 循环确保如果在非原子ticks 计算期间发生重新加载,则获取新的systick_reload_count 并重新计算ticks。循环通常不应该重复超过两次(你必须在 131 毫秒内被中断,在这种情况下你还有其他问题!),所以保持确定性。

    此解决方案的一个重要方面是计算在重新加载计数的本地副本中执行,而不是在易失性计数本身上执行。

    【讨论】:

    • systick 加载值 - val 需要与 0x00FFFFFF 进行与运算才能使数学工作
    • 这个想法似乎很有希望。然而,不幸的是,这并没有解决问题。我们之前曾尝试在进行滴答计算时完全屏蔽 systick 中断。附带说明一下,我认为代码中的ticks 计算需要+ 而不是括号之间的-
    • 我会再看看这个问题 - 也许还有其他我没有发现的问题,但你的方法仍然有缺陷。禁用中断将存在严重缺陷 - SYSTICK 仍在运行,VAL 计数和重新加载之间仍然存在不一致。
    • @old_timer 进一步解释。在 Cortex-M 上,请注意 SYSTICK 是一个 24 位计数器。 0x00FFFFFF 是它的最大值。
    • 啊啊啊啊,对不起。当你取两个 val 样本并减去它们时,你需要掩码,你正在取 max 减去 current,没关系....
    【解决方案2】:

    您的函数systick_get_ticks 不是线程/中断安全的。如果它被 systick 中断,返回值将不正确。

    对于您可以使用的线程/中断安全版本:

    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

    (另外,请停止命名以下划线开头的变量。这些名称是保留的,如果使用它们会得到未定义的行为)。

    【讨论】:

    • 您应该在答案中包含解决方案,而不是链接到其他地方的答案。
    • 关于下划线的部分值得评论,而不是答案的一部分。除非您认为未定义的行为是问题的原因?可能 - 它未定义 - 但显然 unlikley。
    • 如果你确实发布了代码,但我会指出,没有文档,没有解释,也没有关于 C 标记问题的汇编程序。
    • 答案真的只有第一段。下次我会把链接放在评论中。
    • 我同意;我是个混蛋。道歉。这是一个有效的答案。
    【解决方案3】:

    最终在汇编中实现它:

    
    #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
    

    ldrexstrex 指令作为互斥体,确保_ticks 的值在两条指令之间没有被修改。

    【讨论】:

      【解决方案4】:

      stm32的systick不好用,如果你利用stm32 systick init code,你会发现它至少被修改了3次,甚至得到它后也会被修改,像这样:

      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 */
      

      所以我建议你使用其他 TIMER 或修改 stm32 库(但很麻烦)。

      【讨论】:

      • 这只是一个初始配置代码,只打算运行一次,所以 "它至少被修改了 3 次" 没有意义。配置其他计时器并不比这更容易。
      猜你喜欢
      • 1970-01-01
      • 2014-10-12
      • 1970-01-01
      • 2018-03-07
      • 2017-05-26
      • 2021-12-30
      • 2020-11-14
      • 2016-04-13
      • 2019-01-14
      相关资源
      最近更新 更多