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。

具体怎么做?我一般分三步走:

  1. 禁用HAL库对SysTick的初始化:在HAL_Init()之前,先调用HAL_SuspendTick(),或者干脆在stm32f4xx_hal_conf.h里把HAL_TICK_FREQ_DEFAULT改成其他值。
  2. 选一个通用定时器做HAL库的时钟源:比如TIM6或TIM7,这两个是基本定时器,没有外部引脚,最适合做内部时钟。
  3. 实现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上。我个人强烈推荐前者,稳定、可靠、不挖坑。

下一章,我们会讲中断优先级分组。嗯,那个也是容易出问题的地方。到时候再聊。