第三章 ISR设计原则:短小精悍,拒绝阻塞
好,咱们进入正题。ISR设计,说白了就是「在刀尖上跳舞」。你想想看,中断来了,CPU正忙着呢,突然被拽过来执行你的代码。这时候你要是敢在里面磨蹭,整个系统都得跟着遭殃。
我个人习惯,写ISR之前先问自己三个问题:能不能不做?能不能少做?能不能挪出去做? 这三个问题想清楚了,ISR基本就稳了。
3.1 原则一:ISR必须短小精悍
什么叫短小精悍?我有个简单的判断标准:ISR的执行时间,不能超过中断触发周期的10%。举个例子,如果你的定时器每1ms触发一次中断,那ISR的执行时间最好控制在100μs以内。超过这个数,系统就开始抖了。
核心思想: ISR里只做最必要的事——读取硬件状态、清除中断标志、把数据扔到队列里。其他一切,统统交给任务去处理。
我在项目中遇到过一位同事,非要在ISR里做浮点运算。结果呢?中断一触发,CPU光保存浮点寄存器就花了上百微秒。系统跑起来一卡一卡的,跟幻灯片似的。后来改成整型运算,问题立刻解决。
3.1.1 典型反例
// 错误示范:ISR里做复杂计算
void bad_isr(void) {
uint32_t raw = read_adc();
double voltage = raw * 3.3 / 4096.0; // 浮点运算,慢!
double filtered = low_pass_filter(voltage); // 滤波,更慢!
printf("Voltage: %.2f\n", filtered); // 打印?疯了吧!
}
3.1.2 正确做法
// 正确示范:ISR只做最核心的事
void good_isr(void) {
uint32_t raw = read_adc();
msgQSend(adcQueue, (char*)&raw, sizeof(raw), WAIT_NONE, MSG_PRI_NORMAL);
// 清除中断标志
*(volatile uint32_t*)ADC_INT_CLR = 1;
}
看到了吗?ISR里只做了三件事:读数据、发消息、清标志。剩下的计算和打印,交给任务去做。这才是正确的打开方式。
3.2 原则二:绝对禁止阻塞
这一点怎么强调都不过分。ISR里一旦阻塞,整个系统就挂了。 为什么?因为ISR运行在中断上下文中,没有任务切换的概念。你调用一个阻塞函数,CPU就死在那了,其他中断进不来,任务也跑不了。
警告: 以下API在ISR中绝对禁止调用:
taskDelay()— 想都别想semTake()— 除非用WAIT_NONEmsgQReceive()— 会阻塞等待消息malloc()/free()— 内存分配可能触发上下文切换printf()— 内部有锁,可能死锁
我曾经踩过一个坑。有个项目,串口中断里调用了 printf() 来打印调试信息。平时跑得好好的,一上压力测试就死机。查了三天,最后发现是 printf() 内部用了互斥锁,ISR里拿锁,任务里也拿锁,死锁了。嗯,从那以后,我再也不敢在ISR里用 printf 了。
3.3 原则三:不可调用阻塞型API
这个原则其实是上一条的延伸。但我要单独拿出来说,因为很多人会在这里翻车。
VxWorks里有些API,看着人畜无害,实际上内部会阻塞。比如 semGive() 本身是安全的,但如果你给了一个优先级反转的二进制信号量,那 semGive() 内部可能会触发任务调度——这在ISR里是绝对不允许的。
我的建议: ISR里只使用以下安全API:
msgQSend(..., WAIT_NONE, ...)— 非阻塞发送消息semGive()— 释放信号量(注意:不要用semFlush())intContext()— 检查当前是否在中断上下文intLock()/intUnlock()— 中断锁定/解锁logMsg()— 替代printf()的日志函数
3.4 实战技巧:ISR的「三段式」设计
我总结了一个ISR设计的「三段式」模板,用了好多年,效果不错:
- 快速捕获: 读取硬件状态,保存必要数据
- 轻量通知: 通过消息队列或信号量通知任务
- 干净退出: 清除中断标志,恢复硬件状态
// 三段式ISR模板
void my_isr(void) {
// 第一段:快速捕获
uint32_t status = REG_READ(INT_STATUS);
uint32_t data = REG_READ(DATA_REG);
// 第二段:轻量通知
msgQSend(myQueue, (char*)&data, sizeof(data), WAIT_NONE, MSG_PRI_NORMAL);
// 第三段:干净退出
REG_WRITE(INT_CLEAR, status); // 清除中断标志
}
你看,整个ISR就三行核心代码,清晰明了。任务那边收到消息后,爱怎么处理就怎么处理,哪怕算到天荒地老也不影响中断响应。
3.5 避坑指南
最后,分享几个我踩过的坑,希望能帮你少走弯路:
- 别在ISR里调
taskDelay(): 这玩意儿在ISR里调用,直接返回ERROR,但很多人不知道,还傻等。 - 小心中断嵌套: 如果你的ISR执行时间太长,高优先级中断会嵌套进来,栈空间可能爆掉。
- 别用
printf(): 用logMsg()代替,它不会阻塞,也不会死锁。 - 注意缓存一致性: 如果ISR和任务共享数据,记得用volatile关键字,必要时加内存屏障。
记住一句话: ISR不是用来干活的,是用来「通知别人干活」的。你越短小,系统越稳定。
好了,这一章就到这里。下一章咱们聊聊中断优先级和嵌套,那又是一个容易翻车的地方。