stm32g483 基于 SysTick 的计时器的奇怪行为
Weird behavior for SysTick based timer for stm32g483
SysTick使用说明
SysTick
我们有一个基于 STM32G483 MCU (Cortex M4) 的定制板。我们使用 SysTick 作为软件定时器的参考。 SysTick 重载寄存器设置为 0x00FFFFFF 以便产生最少的中断。 SysTick 以 128MHz 的 CPU 时钟计时,这意味着每 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 被配置为我们想要的,我们已经编写了一个片段来检查 SysTick 中断是否以正确的频率触发,并且 systick_get_ticks()
函数 returns 是一个可靠的数字。这使我们认为问题出在计时器代码本身,但我们似乎找不到问题所在。
代码使用 clang (--target=arm-none-eabi
) 编译,未使用 STM32 HAL 库
您的函数 systick_get_ticks
不 thread/interrupt 安全。如果它被系统中断,return 值将不正确。
对于 thread/interrupt 安全版本,您可以使用:
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
(另外,请停止命名以下划线开头的变量。这些名称是保留的,如果使用它们会出现未定义的行为)。
考虑:
#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 位设备上的 atomic 操作(即不能被中断)。
systick_get_ticks
中的while循环确保如果在非原子ticks
计算期间发生重新加载,则获取新的systick_reload_count
并ticks
重新计算。循环通常不应迭代超过两次(您必须中断 131 毫秒,在这种情况下您还有其他问题!),因此保持确定性。
此解决方案的一个重要方面是计算是在重新加载计数的本地副本中执行的,而不是在易失性计数本身上执行的。
stm32的systick不好用,如果你利用stm32的systick初始化代码,你会发现它至少被修改了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库(但很麻烦)。
最终在汇编中实现它:
#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
和 strex
指令用作互斥体,确保 _ticks
的值在两条指令之间没有被修改。
SysTick使用说明
SysTick
我们有一个基于 STM32G483 MCU (Cortex M4) 的定制板。我们使用 SysTick 作为软件定时器的参考。 SysTick 重载寄存器设置为 0x00FFFFFF 以便产生最少的中断。 SysTick 以 128MHz 的 CPU 时钟计时,这意味着每 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 被配置为我们想要的,我们已经编写了一个片段来检查 SysTick 中断是否以正确的频率触发,并且 systick_get_ticks()
函数 returns 是一个可靠的数字。这使我们认为问题出在计时器代码本身,但我们似乎找不到问题所在。
代码使用 clang (--target=arm-none-eabi
) 编译,未使用 STM32 HAL 库
您的函数 systick_get_ticks
不 thread/interrupt 安全。如果它被系统中断,return 值将不正确。
对于 thread/interrupt 安全版本,您可以使用:
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
(另外,请停止命名以下划线开头的变量。这些名称是保留的,如果使用它们会出现未定义的行为)。
考虑:
#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 位设备上的 atomic 操作(即不能被中断)。
systick_get_ticks
中的while循环确保如果在非原子ticks
计算期间发生重新加载,则获取新的systick_reload_count
并ticks
重新计算。循环通常不应迭代超过两次(您必须中断 131 毫秒,在这种情况下您还有其他问题!),因此保持确定性。
此解决方案的一个重要方面是计算是在重新加载计数的本地副本中执行的,而不是在易失性计数本身上执行的。
stm32的systick不好用,如果你利用stm32的systick初始化代码,你会发现它至少被修改了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库(但很麻烦)。
最终在汇编中实现它:
#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
和 strex
指令用作互斥体,确保 _ticks
的值在两条指令之间没有被修改。