2.1 中断系统与实时响应


2.1 中断系统与实时响应

本节摘要:中断是微控制器对物理世界的反射弧:外部事件不必等 CPU 轮询,硬件直接打断当前流程优先处理。本节拆解一次中断的完整生命周期,讲清中断为什么会丢、共享变量为什么必须特殊对待,并以"按键偶尔失灵"这个最常见的故障为案例,把中断优先级、去抖与中断风暴三个知识点串成一次完整排查。

第 1 章结束时,我们的板子有了稳定的心跳与干净的电源;从本节起,时间开始被精确地划分。中断位于第 2 章开头,因为它是嵌入式"响应外部世界"的根本机制——没有它,第 3 章的每一条总线、第 4 章的每一次任务切换都无从谈起。

一个轮询永远解不开的死结

先看轮询的困境:主循环里既要刷新显示,又要等按键按下。刷新一轮要 50 毫秒,这意味着按键最多被延迟 50 毫秒才被发现;若用户是快速点按,按键可能被完整错过——按下又抬起都发生在一次刷新期间。把刷新做快?那 CPU 就没时间干别的了。轮询的根本矛盾在于:CPU 的注意力是串行的,而物理事件是并行的

中断把这个死结解开:按键按下时,硬件在几个时钟周期内强制打断主循环,先处理按键,处理完回来接着刷屏。响应延迟从"主循环最长一轮"变成"几个微秒"——这个数量级的差别,就是玩具与产品的分界线。

中断的一生:它在哪里会丢

把一次中断拆成五站:事件发生 → 触发与挂起 → 仲裁响应 → 服务执行 → 恢复返回。每一站都有专属的丢失方式,知道丢在哪一站,才知道去查什么:

站点 发生什么 典型丢失原因
触发 外设产生事件信号 引脚配置错误、边沿极性选反
挂起 置位挂起标志等待响应 同一中断再次触发,标志被覆盖
仲裁 控制器按优先级排队 高优先级中断连续霸占 CPU
服务 执行中断服务例程 例程太长,后续事件被挤掉
返回 恢复现场继续主流程 服务里改了共享数据没保护

最值得展开的是"挂起"这一站:多数平台的中断挂起标志是"电平"语义而不是"计数"语义。服务例程执行期间又来了两次触发,退出后 CPU 只看到"还有一次待处理"——两次触发被合并成一次。对按键这类电平事件无所谓,但对脉冲计数、串口字节流这类每一笔都作数的事件,这就是静默丢数据。

图:一次中断的生命周期与两个易丢点

图:一次中断的生命周期与两个易丢点

volatile 与临界区:两条保命规则

规则一:中断与主循环共享的变量必须加 volatile。 优化器的世界观里,只有"它自己改自己"的变量;主循环里反复读一个"会被中断悄悄改掉"的变量,优化器可能把它缓存进寄存器,从此读到的永远是旧值。volatile 就是告诉优化器"这个变量随时会被别人改,别自作主张"。

规则二:主循环读改写共享数据时,必须短暂关中断(临界区)。 一个多字节变量(如 32 位计数器)在中断里更新,主循环读取时可能恰好读了一半——低字节是新值、高字节是旧值。关中断再读,读完立刻开,窗口只有几条指令,不影响实时性。

// 按键计数:演示 volatile 与临界区的最小正确写法 volatile uint32_t pressCount = 0; // 中断会写、主循环会读,必须 volatile void setup() { pinMode(2, INPUT_PULLUP); attachInterrupt(digitalPinToInterrupt(2), onPress, FALLING); Serial.begin(115200); } // 中断服务例程:只做最必要的事,越短越好 void onPress() { pressCount++; // AVR 上 32 位自增非原子,但在 ISR 内执行是安全的 } void loop() { noInterrupts(); // 进入临界区 uint32_t snapshot = pressCount; // 原子地取一份快照 interrupts(); // 立刻退出临界区 Serial.print("已确认按键次数:"); Serial.println(snapshot); delay(1000); // 用快照慢慢打印,不与中断抢数据 }

输入是按键信号,输出是每秒一行的累计计数。注意两处设计:中断里只做自增、不做打印(串口发送动辄毫秒级,放进中断等于把服务例程变成时间黑洞);主循环用快照模式取数,临界区只护住"取"这一个动作。

案例:按键偶尔失灵的三段式排查

背景:一台带旋转编码器的设备,用户反馈"偶尔转动一下没反应",复现概率约百分之一,桌面测试怎么也复现不了。

操作:第一步怀疑机械抖动,把中断里的去抖延时从 5 毫秒加到 20 毫秒——失灵率没变,排除。第二步审查中断代码,发现服务例程里有去抖 delay 加 I2C 写 OLED 刷新计数值:delay 在中断里靠循环等中断计时,本身就可能死锁;I2C 一次写要几毫秒。这两样加起来,服务例程占用近 10 毫秒,期间编码器快速旋转产生的脉冲全部落入"挂起合并"窗口被丢弃。第三步重构:中断里只记"有一个增量",OLED 刷新挪到主循环按脏标志执行;去抖改为主循环里 5 毫秒窗口确认。

结果:连续转动一万格无一次丢失,响应依然即时。

解读:这起故障的本质是"把主循环的活塞进了中断"。中断服务例程的纪律只有一条:只做记录,不做加工——把"发生了"记下来就退场,怎么处理留给主循环。凡是觉得"中断里顺手把事情办了更高效"的写法,都是在透支实时性。

变式:若你的平台中断嵌套(高优先级可打断低优先级服务),还要加一条:共享保护要连嵌套一起想清楚——关中断的临界区在嵌套场景下要改为关特定优先级,这在第 4 章 RTOS 一节会以系统化的方式重讲。

⚠️ 常见坑:attachInterrupt 绑定的回调里调用 Serial、Wire、SoftPWM 等依赖中断自身的库,是中断卡死的高发原因。这些库的收发完成同样依赖中断,在中断里等它们,等于站在自己影子里等天亮。

💡 关键直觉:判断一段代码能不能放进中断服务例程,只问一句——"它依赖别人吗?"凡是要等的、要锁的、要打印的,都是依赖,都不该进中断。

本节要点回顾

  • 中断解决串行注意力与并行事件的矛盾,响应延迟从毫秒级降到微秒级;
  • 五站生命周期各有丢失方式,挂起合并与服务过长是最高频的两站;
  • volatile 管优化器,临界区管原子性,两条规则各司其职、都要写;
  • 服务例程只记录不加工,耗时操作一律挪回主循环或任务;
  • 嵌套与库依赖是进阶雷区,"它依赖别人吗"是判断能不能进中断的唯一标准。

作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U