4. 中断服务函数编写规范

中断服务函数,简称 ISR。这东西看着简单,写起来坑不少。我见过太多新手,甚至有些老手,在 ISR 里翻车。今天咱们就聊聊,怎么写一个靠谱的 ISR。

4.1 中断服务函数命名规则

命名这事儿,不同公司有不同规矩。但有几个共识,我建议你记住。

官方推荐命名

STM32 的 HAL 库,中断向量表已经定好了名字。比如:

// 串口1中断
USART1_IRQHandler()

// 定时器2中断
TIM2_IRQHandler()

// 外部中断0
EXTI0_IRQHandler()

这些名字,启动文件里已经声明了。你直接写函数体就行。千万别自己改名字,否则中断进不去。

我个人的命名习惯

如果项目是自己从头搭的,我会加个前缀,方便区分:

// 我的命名风格
void ISR_USART1_Handler(void)
void ISR_TIM2_Handler(void)
void ISR_EXTI0_Handler(void)

这样在代码里搜索 ISR,所有中断函数一目了然。我在一个 10 万行的项目里用过这个规则,找问题快很多。

命名规则总结

  • 使用 HAL 库时,保持官方命名
  • 自己写时,建议加 ISR_ 前缀
  • 名字里包含外设名和通道号
  • 不要用拼音,不要用缩写

4.2 中断服务函数注意事项

这部分是重点。我踩过的坑,都在这儿了。

第一,ISR 要短小精悍

中断服务函数,不是让你写业务逻辑的地方。它的任务只有一个:快速处理,然后退出。你想想看,中断在跑的时候,其他中断都在排队等着。你拖得越久,系统响应越慢。

我曾经接手过一个项目,ISR 里居然做了浮点运算和延时。结果系统时不时卡死。查了三天,最后发现是 ISR 执行时间太长,把其他中断饿死了。

第二,不要调用阻塞函数

在 ISR 里,绝对不能调用这些函数:

  • HAL_Delay() - 依赖 SysTick 中断,会死锁
  • printf() - 重定向到串口,串口中断优先级可能更低
  • malloc() - 不可重入,会破坏堆
  • 任何带等待循环的函数

我曾经犯过的错:在 ISR 里用 HAL_Delay(10),结果系统直接死机。因为 HAL_Delay 依赖 SysTick 中断,而 SysTick 优先级比当前中断低,永远等不到。这就是典型的死锁。

第三,注意中断优先级

STM32 的中断优先级,分抢占优先级和子优先级。高抢占优先级的中断,可以打断低优先级的。这个要设计好。

中断类型 推荐抢占优先级 说明
系统时钟(SysTick) 0 最高,保证系统调度
外部中断(按键等) 1 需要快速响应
定时器中断 2 周期性任务
串口接收 3 可以稍等
DMA 传输完成 4 最低

这个表不是死的。但原则是:越紧急的中断,优先级越高。

4.3 中断服务函数中的变量使用

变量在 ISR 里用不好,bug 会非常隐蔽。我来说说关键点。

全局变量要加 volatile

ISR 和主循环共享的变量,必须用 volatile 修饰。为什么?因为编译器会优化。它觉得这个变量只在主循环里改,就把它放到寄存器里。结果 ISR 改了内存里的值,主循环读的还是寄存器里的旧值。

// 正确的写法
volatile uint8_t g_uart_rx_flag = 0;

// 错误的写法
uint8_t g_uart_rx_flag = 0;  // 编译器可能优化掉

变量类型要小心

在 ISR 里,尽量用 uint8_tuint16_tuint32_t 这些确定长度的类型。别用 int,不同平台长度不一样。我见过一个坑:在 STM32F103 上 int 是 32 位,换到 STM32F030 上还是 32 位,但有人以为会变,结果溢出。

ISR 内部变量

ISR 内部的局部变量,没问题。但要注意栈空间。如果 ISR 嵌套很深,栈可能溢出。我建议 ISR 里的局部变量不要超过 10 个,数组尽量用静态的。

小技巧:在 ISR 里只做标记,不做处理。比如串口收到数据,ISR 里只设一个标志位,主循环里再处理数据。这样 ISR 执行时间极短,系统响应快。

// 推荐的 ISR 写法
volatile uint8_t g_data_ready = 0;
uint8_t g_rx_buffer[256];

void USART1_IRQHandler(void)
{
    // 只做最必要的事
    if (LL_USART_IsActiveFlag_RXNE(USART1))
    {
        g_rx_buffer[g_rx_index++] = LL_USART_ReceiveData8(USART1);
        g_data_ready = 1;  // 通知主循环
    }
}

嗯,这里要注意:g_rx_index 也要加 volatile,而且主循环里要处理溢出。

原子操作问题

在 Cortex-M3/M4 上,对 32 位变量的读写是原子的。但 8 位、16 位变量不一定。如果 ISR 和主循环同时操作一个变量,可能读到半新半旧的值。解决办法:

  • 关中断再操作(不推荐,影响实时性)
  • 用原子操作函数(如 __LDREXB__STREXB
  • 用消息队列传递数据

我个人习惯用消息队列。FreeRTOS 的消息队列,在 ISR 里用 xQueueSendFromISR(),安全又方便。

中断服务函数编写规范 - 知识体系 命名规则 注意事项 变量使用 HAL库保持官方命名 自定义加ISR_前缀 包含外设名+通道号 ISR要短小精悍 不调用阻塞函数 注意中断优先级 全局变量加volatile 使用确定长度类型 注意原子操作 核心原则:ISR只做标记,不做处理

总结一下:ISR 的编写,说白了就是「快进快出」。命名规范、注意阻塞、变量加 volatile,这三点做到,基本不会出大问题。我见过太多项目,因为 ISR 写得不好,后期改得痛不欲生。你想想看,中断是系统的脉搏,脉搏乱了,整个系统就完了。

好了,这一节就到这儿。记住我说的,写 ISR 的时候,多想想「我这个函数能在 10 微秒内跑完吗?」。如果不能,就该重构了。


公众号:蓝海资料掘金营,微信deep3321