4、Tick中断服务编程:SysTick_Handler编写、中断优先级设置、临界区保护

好,咱们接着聊。上一节我们把SysTick定时器初始化好了,让它开始滴答滴答地跑。但光跑还不够,你得告诉CPU,每次滴答一下,该干点啥。这就是Tick中断服务函数——SysTick_Handler的活儿。

4.1 SysTick_Handler:心脏的每一次跳动

说白了,这个函数就是系统的心跳。每次SysTick计数到0,触发中断,CPU就跳进来执行这里面的代码。我个人习惯,这个函数里只放最核心、最紧急的事。

void SysTick_Handler(void)
{
    // 1. 系统节拍计数器加1
    uwTick++;
    
    // 2. 检查并执行软件定时器回调
    // 这里通常由HAL库或RTOS接管
}

你看,代码就这么简单。但简单背后有门道。uwTick这个变量,是整个系统的时间基准。我在项目中遇到过,有人在这个函数里塞了串口打印、LED闪烁,甚至做了复杂运算。结果呢?系统响应变慢,任务调度乱套。

警告:SysTick_Handler里不要做耗时操作!它应该像心跳一样,快速、规律、不拖泥带水。任何阻塞、延时、复杂计算,都请移步到任务或主循环中。

4.2 中断优先级设置:别让心跳被堵住

为什么会强调优先级?你想想看,如果系统正在处理一个紧急的外部中断,比如电机堵转保护,这时候SysTick中断来了,你是先处理心跳,还是先保护电机?

嗯,这里要注意。SysTick中断的优先级,通常设置为中等或较低。因为它的工作是维持系统节拍,偶尔延迟一两个节拍,系统还能撑住。但电机保护延迟了,可能就烧了。

设置方法很简单,以Cortex-M3/M4为例:

// 设置SysTick中断优先级为最低(数值越大优先级越低)
// 注意:NVIC优先级分组要先配置好
NVIC_SetPriority(SysTick_IRQn, 0x0F);  // 假设分组为4位抢占优先级

我曾经在一个项目中,把SysTick优先级设得比所有中断都高。结果一个高频率的外部中断频繁触发,SysTick每次都抢着执行,导致外部中断响应延迟,数据采集丢包。调试了整整两天才找到原因。

我的建议:SysTick优先级设为系统中断中最低的一档。除非你有特殊需求,比如需要极其精确的时间测量,否则别动它。

4.3 临界区保护:别让数据乱掉

好,现在有个新问题。uwTick这个变量,在中断里被修改。如果主循环也在读它,会发生什么?

举个例子:主循环里判断if(uwTick > 1000),刚读到高8位,中断来了,uwTick从0x00FF变成了0x0100。中断返回后,继续读低8位。结果读到的值是0x0000?还是0x01FF?

这就是典型的“数据不一致”问题。解决它,就需要临界区保护。

临界区,说白了就是一段“不允许被打断”的代码。在单核MCU上,最简单的办法就是——关中断。

// 进入临界区
__disable_irq();

// 读取或修改共享变量
uint32_t local_tick = uwTick;

// 退出临界区
__enable_irq();

但这里有个坑。如果你在中断里也调用了这个保护,那__disable_irq()会把所有中断都关了,包括它自己。这就死锁了。

避坑指南:我曾经在SysTick_Handler里调用了一个带临界区保护的函数,结果系统一跑就卡死。后来才发现,中断里关中断,等于自己把自己锁门外了。

正确的做法是:

  • 在中断服务函数中:不需要临界区保护。因为中断本身已经禁止了同级或低优先级中断的嵌套。
  • 在主循环或任务中:访问共享变量时,必须进入临界区。

更专业的做法,是用RTOS提供的临界区API,或者用Cortex-M的LDREX/STREX指令实现无锁访问。但对于简单的节拍计数器,关中断就够了。

4.4 实战:一个完整的Tick中断示例

来,我把前面讲的串起来,给你一个完整的示例。这个例子在STM32上验证过,你可以直接移植。

/* 系统节拍计数器 */
static volatile uint32_t uwTick = 0;

/* SysTick中断服务函数 */
void SysTick_Handler(void)
{
    uwTick++;
}

/* 获取当前节拍值(带临界区保护) */
uint32_t GetTick(void)
{
    uint32_t tick;
    
    __disable_irq();      // 进入临界区
    tick = uwTick;        // 原子读取
    __enable_irq();       // 退出临界区
    
    return tick;
}

/* 毫秒延时函数(基于SysTick) */
void DelayMs(uint32_t delay)
{
    uint32_t start = GetTick();
    
    while((GetTick() - start) < delay)
    {
        // 等待,可在此处加入低功耗模式
    }
}

你看,GetTick()函数加了临界区保护,保证了读取的原子性。DelayMs()里每次循环都调用GetTick(),虽然效率不高,但胜在安全可靠。

小技巧:如果你用RTOS,比如FreeRTOS,它自带的vTaskDelay()xTaskGetTickCount()已经帮你处理好了临界区。直接用就行,别自己再造轮子。

4.5 总结一下

嗯,这一节内容不少,但核心就三点:

  1. SysTick_Handler要精简——只做节拍计数和必要的系统维护。
  2. 中断优先级要合理——SysTick优先级别太高,给更紧急的中断让路。
  3. 临界区保护要到位——访问共享变量时,记得关中断,但别在中断里关中断。

这些经验,都是我一个个坑踩出来的。你照着做,能少走很多弯路。下一节,咱们聊聊如何用SysTick实现软件定时器,那才是真正实用的东西。