4. 系统时钟与心跳配置:SysTick定时器的配置,HAL库与RTOS时间基准的冲突解决
好,咱们进入第四章。这一章,我打算聊聊系统的心跳——SysTick定时器。
你想想看,一个呼吸机,它要按固定的节奏送气、检测压力、切换时相。这个节奏从哪来?就是SysTick。它就像一个节拍器,嘀嗒嘀嗒,整个系统都跟着它走。
但问题来了。HAL库自己也要一个节拍,RTOS也要一个节拍。两个节拍器放在一起,搞不好就打架了。我在项目里就见过,呼吸机运行着运行着,突然节拍乱了,压力波形都变形了。嗯,这就是时间基准冲突的典型症状。
4.1 SysTick定时器到底是什么?
SysTick是Cortex-M内核自带的一个24位递减计数器。说白了,它就是一个简单的倒计时器。从你设定的值开始,每个时钟周期减1,减到0就触发一次中断。
我个人习惯,把它叫做“系统的心跳”。为什么?因为FreeRTOS的任务调度、时间片轮转、延时函数,全都依赖这个心跳。
它的配置其实很简单,就三个关键参数:
- 重装载值:决定心跳的频率。比如系统时钟是72MHz,你想要1000Hz的心跳,那重装载值就是72000-1。
- 时钟源:一般选处理器时钟(AHB时钟),或者AHB时钟的8分频。
- 中断使能:减到0时要不要触发中断?当然要,不然RTOS怎么知道该切换任务了?
核心要点:SysTick的中断优先级,一定要设置为RTOS允许的最高优先级。否则,一个低优先级的中断把SysTick堵住了,整个系统的实时性就崩了。
4.2 HAL库的“霸道”与RTOS的“需求”
HAL库有个函数叫HAL_Init(),它一上来就把SysTick占用了。它用SysTick来做HAL_Delay()的延时,以及各种超时判断。
但FreeRTOS也需要SysTick。它要用SysTick的中断来触发xPortSysTickHandler(),完成任务调度。
这就冲突了。一个SysTick,两个主人,听谁的?
我记得第一次做RTOS移植时,没注意这个冲突。结果一跑HAL_Delay(10),整个RTOS就卡死了。任务不切换了,呼吸机也不送气了。当时吓出一身冷汗。
为什么会这样?因为HAL_Delay是靠SysTick中断来计时的。但RTOS接管后,SysTick中断被用来做任务调度了。两个中断服务函数抢同一个中断源,不乱才怪。
4.3 解决方案:给HAL库另找一个“钟”
解决思路其实很直接:把HAL库的时间基准,从SysTick换到其他定时器上。让SysTick专属于RTOS。
具体怎么做?我一般分三步走:
- 禁用HAL库对SysTick的初始化:在
HAL_Init()之前,先调用HAL_SuspendTick(),或者干脆在stm32f4xx_hal_conf.h里把HAL_TICK_FREQ_DEFAULT改成其他值。 - 选一个通用定时器做HAL库的时钟源:比如TIM6或TIM7,这两个是基本定时器,没有外部引脚,最适合做内部时钟。
- 实现HAL库的时钟回调函数:重写
HAL_IncTick()和HAL_GetTick(),让它们从新的定时器读取时间。
代码示例大概是这样:
// 在main.c中
#include "stm32f4xx_hal.h"
#include "FreeRTOS.h"
#include "task.h"
// 自定义的HAL时间基准
static __IO uint32_t uwTickCustom = 0;
// 重写HAL_IncTick,由TIM6的中断调用
void HAL_IncTick(void)
{
uwTickCustom += 1; // 每1ms加1
}
// 重写HAL_GetTick
uint32_t HAL_GetTick(void)
{
return uwTickCustom;
}
// TIM6初始化,配置为1ms中断一次
void HAL_TimeBase_Init(void)
{
TIM_HandleTypeDef htim6;
htim6.Instance = TIM6;
htim6.Init.Prescaler = 72-1; // 72MHz / 72 = 1MHz
htim6.Init.Period = 1000-1; // 1MHz / 1000 = 1ms
HAL_TIM_Base_Init(&htim6);
HAL_TIM_Base_Start_IT(&htim6);
}
// TIM6中断服务函数
void TIM6_DAC_IRQHandler(void)
{
HAL_TIM_IRQHandler(&htim6); // 这里会调用HAL_IncTick
}
int main(void)
{
// 先初始化HAL库,但此时SysTick还没被配置
HAL_Init();
// 配置TIM6作为HAL的时间基准
HAL_TimeBase_Init();
// 然后配置SysTick给FreeRTOS
// SysTick_Config(SystemCoreClock / 1000); // 1ms心跳
// 启动RTOS调度器
vTaskStartScheduler();
while(1);
}
我的小技巧:在调试阶段,我会在TIM6的中断里加一个GPIO翻转。用示波器看这个GPIO的波形,就能确认HAL库的1ms基准是否准确。我曾经用这个方法,发现了一个晶振配置错误的问题。
4.4 另一种思路:让HAL库“寄生”在RTOS上
如果你不想额外占用一个定时器,还有另一种办法。那就是让HAL库的HAL_IncTick(),由FreeRTOS的空闲任务来调用。
具体做法是:
- 在
vApplicationIdleHook()中,调用HAL_IncTick()。 - 这样,每当CPU空闲时,HAL库的时间就会往前走。
但这个方法有个坑。如果系统一直很忙,没有空闲时间,那HAL库的时间就停滞了。HAL_Delay(1000)可能变成HAL_Delay(10000)。呼吸机里的超时判断就会出问题。
警告:在呼吸机这种对实时性要求极高的设备中,我不推荐用“寄生”方案。宁可多用一个定时器,也要保证时间基准的确定性。安全第一。
4.5 避坑指南:我踩过的三个坑
讲到这里,我分享几个实战中遇到的坑,你遇到了可以少走弯路。
坑一:中断优先级配置错误
我曾经把SysTick的中断优先级设成了最低。结果一个串口中断进来,处理了很长时间,SysTick被堵住了。RTOS以为时间没到,任务切换延迟了。呼吸机的吸气相被硬生生拉长了50ms。患者能感觉到吗?能。所以,SysTick优先级一定要高。
坑二:HAL_Delay在中断中调用
在RTOS环境下,HAL_Delay()本质是轮询等待。如果在中断服务函数里调用它,而中断优先级又高于SysTick,那就死锁了。我见过一个同事,在定时器中断里调了HAL_Delay(1),系统直接卡死。解决办法:中断里别用延时,用标志位或者信号量。
坑三:SysTick重装载值计算错误
系统时钟是168MHz,我算重装载值时用了168000-1,心想这是1ms。结果实际频率是1.05ms。为什么?因为我忘了考虑SysTick时钟源是AHB的8分频。所以,一定要确认时钟树,别想当然。
4.6 总结一下
SysTick的配置,说白了就是分清楚“谁用这个心跳”。HAL库和RTOS不能共享同一个SysTick中断。要么给HAL库另开一个定时器,要么让HAL库寄生在RTOS上。我个人强烈推荐前者,稳定、可靠、不挖坑。
下一章,我们会讲中断优先级分组。嗯,那个也是容易出问题的地方。到时候再聊。