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_t、uint16_t、uint32_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(),安全又方便。
总结一下:ISR 的编写,说白了就是「快进快出」。命名规范、注意阻塞、变量加 volatile,这三点做到,基本不会出大问题。我见过太多项目,因为 ISR 写得不好,后期改得痛不欲生。你想想看,中断是系统的脉搏,脉搏乱了,整个系统就完了。
好了,这一节就到这儿。记住我说的,写 ISR 的时候,多想想「我这个函数能在 10 微秒内跑完吗?」。如果不能,就该重构了。
公众号:蓝海资料掘金营,微信deep3321