在嵌入式开发中,中断几乎是所有RTOS的灵魂
。
它让CPU在关键事件发生时立刻中断当前任务,去处理更重要的事。
但很多人没意识到——中断并不是免费的。
不知道你有没有遇到过这样的问题:
定时器中断刚配好,一秒几十次,结果主循环的逻辑突然变慢,调试打印也卡顿。
问题很可能不在代码逻辑,而是ISR(中断服务函数)的开销太大。
中断开销的本质

中断的执行开销,其实分两部分:固定开销 和 可变开销。
固定开销指的是那些你无法避免的部分——比如 CPU 检测到中断、跳转到中断入口、清标志位、执行返回指令,这些都属于硬性成本
。
就算你的中断函数什么都不干,只是空跑一圈,也得花掉一段时间。
而真正影响性能的,是后面这部分——可变开销。
这部分主要是编译器在帮你自动保存和恢复寄存器的时间。
每次进入中断,CPU 都要把寄存器一个个压栈(push),等中断处理完再一个个弹回来(pop)。
寄存器越多、编译器越热心
,这个过程就越耗时。
举个例子,一个使用 12 个寄存器的中断函数,每次进入都要执行 12 次压栈、12 次出栈。
哪怕中断里的逻辑非常短,这一进一出也可能比你的业务逻辑更费时。
算一笔账就知道了
假设你在一个 1MHz 的单片机上,用中断收发串口数据,波特率是 38400。
每次接收或发送一个字节就触发一次中断,大约每 130 微秒就来一次。
如果每次中断要保存和恢复 12 个寄存器,那 CPU 光是在堆栈上搬运数据就要花掉近 18% 的时间!
也就是说,几乎五分之一的算力浪费在什么都没干的进出中断上。
而如果你能想办法只用到 6 个寄存器,这部分时间立刻减半。 不但节省 CPU 资源,还能节省堆栈空间。
为什么编译器让中断更慢了
早期的中断函数往往是用汇编写的,开发者自己控制保存哪些寄存器。
而现在,编译器太智能
了——你只要在函数前面加一个中断关键字,编译器就会自动帮你搞定一切:
注册中断向量、入栈出栈寄存器、甚至帮你清标志位。
方便是方便了,但问题也来了——编译器并不知道你到底用了哪些寄存器,于是它选择了最保险的做法:
全都保存
这样一来,ISR(中断服务程序)再短也逃不开一堆 push/pop 操作,性能就这么被吃掉
了。
中断里最不该做的一件事:调用函数
有些人为了让代码整洁,会在中断里调用一个函数,比如这样:
void fifo_AddEvent(uint8_t event);
__interrupt void timer_isr(void)
{
TCCR0B = 0; // 停止定时器
fifo_AddEvent(Event); // 发送事件
}
看起来干净又整齐,但问题大了:
编译器必须假设 fifo_AddEvent 会用到所有可能的寄存器,于是它先把整个上下文保存一遍,再去执行函数,最后再恢复回来。
最终结果?
一个看似两行的中断函数,可能在汇编层面上保存了 15 个寄存器,比你想象的复杂十倍。
那我们该怎么办?
中断是必须的,函数调用也避免不了,但我们有几种方法可以显著减少这个损耗。

能不调用函数就别调用
如果逻辑非常简单,比如只是设置个标志位、计数器加一,那就在中断里直接干完。 别多此一举。
把被调用函数写成内联函数
C99 或 C++ 里可以使用 inline。 这样编译器会在中断里直接展开函数,不需要额外的调用开销,也不会引入多余的寄存器保存。
放在同一个源文件里
如果 ISR 和被调用的函数放在同一个 .C 文件里,并且函数是 static, 大多数编译器能分析出实际使用的寄存器,从而自动优化入栈出栈动作。 这也是一种靠近编译器
的小技巧。
最后总结
写中断代码的时候,真正要追求的不是“代码短”,而是“CPU 在忙什么”。
当你能精确算出每次中断花多少时间、哪些寄存器被压栈、堆栈用了多少,那你就已经超过绝大多数人了。
别忘了,越是底层的优化,越能带来真实的性能提升。