3. 中断服务函数:ISR的编写规范、注意事项与常见陷阱
中断服务函数,简称 ISR。说白了,就是硬件发生中断时,CPU 放下手头一切,跑去执行的那段代码。
我见过太多新手,甚至有些老手,在 ISR 里翻车。翻车的原因往往不是不懂技术,而是忽略了 ISR 的特殊性。今天我就把这些年踩过的坑,还有总结出的经验,一次性说清楚。
3.1 ISR 的基本编写规范
先说说最基本的。一个合格的 ISR,长什么样?
3.1.1 函数声明与属性
不同的编译器,写法略有不同。但核心思想一致:告诉编译器,这是个中断函数,不是普通函数。
// IAR 环境
__interrupt void TIM2_IRQHandler(void)
// Keil MDK
void TIM2_IRQHandler(void) __irq
// GCC (ARM Cortex-M)
void TIM2_IRQHandler(void) __attribute__((interrupt))
我个人习惯用 GCC 的写法,清晰明了。不过不管你用哪种,记住一点:不要省略中断属性声明。省略了,编译器会用普通函数的方式处理,后果很严重——寄存器压栈不对,返回指令出错,系统直接跑飞。
3.1.2 函数体要短小精悍
ISR 不是写论文的地方。它应该像电报一样,言简意赅。
- 只做必要的事:清除中断标志、读取数据、设置一个标志位。
- 把耗时操作扔出去:数据处理、协议解析、打印输出,这些放到主循环里做。
- 避免函数调用:尤其是那些你不知道执行时间的库函数。
黄金法则:ISR 的执行时间,必须小于两次中断之间的间隔。否则,你会遇到中断嵌套,甚至中断丢失。
3.2 注意事项:这些细节决定成败
3.2.1 中断标志位的清除时机
嗯,这里要特别注意。很多芯片要求在 ISR 开头就清除中断标志,有些则要求在结尾。搞反了,就会反复触发同一个中断。
我曾经在一个项目中,用了一款国产 MCU。手册上写的是“读状态寄存器自动清除标志”。结果呢?读是读了,但没清干净。中断一直触发,CPU 卡死在 ISR 里,主循环根本跑不起来。排查了整整两天,最后发现是芯片的勘误表里写了——需要额外写一次清除寄存器。
教训:永远不要完全相信手册。尤其是中断标志位,一定要实测验证。
3.2.2 中断优先级与嵌套
中断优先级,说白了就是“谁更紧急”。高优先级的中断可以打断低优先级的 ISR。这本身没问题,但你要小心:
- 优先级不要设太多:一般 3-4 级就够用了。太多级,嵌套深度大,栈空间容易爆。
- 同优先级不要互相打断:如果两个中断同优先级,一个 ISR 没执行完,另一个来了,会排队等待。这没问题。但如果你不小心设成了“可抢占”,那就乱套了。
3.2.3 共享数据的保护
ISR 和主循环共享一个变量?小心!
举个例子:主循环在判断一个标志位,ISR 刚好在修改它。如果主循环读到一半,ISR 插进来改了值,主循环读到的就是个“半成品”。
解决办法:
- 使用 volatile 关键字:告诉编译器,这个变量可能被意外修改,不要优化。
- 关中断保护:在访问共享变量前,关掉对应的中断。访问完再打开。
volatile uint8_t g_flag = 0;
// 主循环中
__disable_irq();
if (g_flag == 1) {
// 处理数据
g_flag = 0;
}
__enable_irq();
3.3 常见陷阱:我踩过的坑,你别再踩
3.3.1 在 ISR 里调用 printf
这是最经典的陷阱。printf 本身很慢,而且很多实现里用了锁。ISR 里一调用,轻则卡死,重则死机。
我曾经有个同事,调试时图方便,在 ISR 里加了句 printf。结果一跑,系统就重启。他以为是硬件问题,折腾了一周。最后我过去一看,把 printf 注释掉,好了。
建议:调试时可以用串口直接发一个字节,或者用 GPIO 翻转来观察 ISR 的执行情况。别用 printf。
3.3.2 浮点运算
ISR 里做浮点运算?除非你的 MCU 有硬件浮点单元,否则千万别干。软件模拟浮点,慢得离谱。一个浮点乘法,可能吃掉几百个时钟周期。
你想想看,如果中断频率是 1kHz,ISR 里做个浮点运算,CPU 就啥也别干了。
3.3.3 忘记清除中断标志
这个错误低级,但特别常见。尤其是用 HAL 库的时候,很多人以为库函数会自动清标志。其实很多库函数只帮你读状态,不清标志。
我见过一个案例:某工程师用 STM32 的串口中断,接收数据。代码跑起来,只收到第一个字节,后面全丢了。查了半天,发现是中断标志没清,ISR 只执行了一次。
3.3.4 中断嵌套导致栈溢出
每个 ISR 都会占用栈空间。如果嵌套深度大,栈空间不够,系统就会跑飞。
怎么避免?
- 估算栈大小:把最坏情况下的嵌套深度算出来,然后给栈分配足够的空间。
- 使用 Cortex-M 的尾链技术:如果 MCU 支持,可以开启尾链,减少栈的使用。
3.4 知识体系:一张图看懂 ISR
下面这张图,是我自己总结的 ISR 知识体系。每次写 ISR 前,我都会对照着检查一遍。
3.5 一个完整的 ISR 示例
说了这么多,来个实际的例子。这是一个串口接收中断的 ISR,我把它作为模板用了好多年。
// 串口接收中断服务函数
void USART1_IRQHandler(void)
{
uint8_t data;
// 1. 检查中断源
if (USART1->SR & USART_SR_RXNE) {
// 2. 读取数据(同时自动清除 RXNE 标志)
data = USART1->DR;
// 3. 将数据存入环形缓冲区
if (!ring_buffer_is_full(&rx_buffer)) {
ring_buffer_put(&rx_buffer, data);
}
// 4. 设置标志位,通知主循环处理
g_rx_flag = 1;
}
// 5. 清除其他可能的中断标志(如果有)
// 某些芯片需要手动清除
}
你看,这个 ISR 里只有:检查中断源、读数据、存缓冲区、设标志。没有 printf,没有浮点运算,没有复杂的逻辑。干净利落。
个人习惯:我一般会在 ISR 里加一个计数器,每次进入 ISR 就加 1。主循环里定期检查这个计数器,如果发现数值异常(比如突然变多或变少),就知道中断有问题了。这个小技巧帮我发现过好几次硬件故障。
好了,关于 ISR 的编写规范、注意事项和常见陷阱,就聊到这里。记住:ISR 不是普通函数,它是在中断上下文中执行的。写的时候多想想“如果这时候另一个中断来了怎么办”、“如果这个 ISR 执行时间长了会怎样”。养成这个习惯,你就能避开大部分坑。
公众号:蓝海资料掘金营,微信deep3321