arduino按键响应设计采用中断会比轮询更快吗?中断是每条指令执行前都要检查吗?
52 2025-08-23 17:29
arduino按键响应设计采用中断会比轮询更快吗?中断是每条指令执行前都要检查吗?
先把结论说在前面:
-
“按键响应更快”:对 Arduino 这种单核 MCU 来说,中断的响应延迟通常比轮询短,因为它不必等主循环下一次“逛”到你那条
digitalRead()
。 -
“中断不是每条指令都检查”:AVR(Arduino Uno/Nano 等)在每条指令结束后才去看有没有挂起的中断,而不是“执行前”。这个检查是硬件自动完成的,一个时钟周期就能完成,不影响正在执行的那条指令。
1. 中断 vs 轮询,到底谁更快?
表格
复制
场景 | 中断 | 轮询 |
---|---|---|
典型延迟 | 3~5 µs(进入 ISR 的硬件+软件开销) | 取决于 loop() 里一圈跑多久,几十 µs 到几 ms 都可能 |
CPU 利用率 | 平时 100% 跑主程序,中断来了才打断 | 必须定期“问”按键,白白转圈 |
抖动/丢事件 | 理论上可以捕获最窄几十 ns 的脉冲 | 如果 loop() 被 delay() 或大量运算卡住,就可能漏掉 |
额外代价 | ISR 里要写去抖、共享变量保护 | 代码简单,但主循环里要插 if (digitalRead(...)) |
因此,“中断更快”主要是指事件发生后到 MCU 开始执行你写的处理代码之间的延迟更小,而不是说 ISR 本身比
digitalRead()
执行得快。2. 中断检查发生在什么时刻?
以 AVR 为例(Arduino Uno/Nano):
-
一条指令完整执行完后,硬件才去看“全局中断使能位 I 是否为 1”以及“某个中断标志位是否被置位”。
-
如果条件满足,硬件自动把 PC(程序计数器)压栈,跳到对应中断向量。
-
整个过程对程序员是透明的,不存在“每条指令执行前”都要浪费时间去查表。
换句话说,中断是“事后”检查,而不是“事前”检查;真正决定延迟的是“当前这条指令还剩几个时钟周期”。
3. 实战建议
-
如果按键只是调个菜单、刷新屏幕,用轮询+简单去抖就够了,代码量小,移植方便。
-
如果按键会唤醒睡眠的 MCU,或者必须捕获极短脉冲(比如旋转编码器),就用中断+去抖(软件或硬件 RC)。
-
中断里别写
delay()
、别长时间关中断;把真正耗时的活儿扔回主循环,ISR 只置标志位。
一句话:中断赢在“响应及时”,轮询赢在“代码简单”;真正的实时极限还得看整个系统调度,而不是只看中断这一条机制。
你刚刚提到的中断向量是什么?
中断向量(interrupt vector)就是**“中断发生以后,CPU 要去哪里找处理代码”**的一张“地址表”。
可以把 MCU 的内存想象成一本大书:
可以把 MCU 的内存想象成一本大书:
-
正常时,CPU 按顺序一页页读(执行主程序)。
-
某个外设突然喊“我有急事!”——这就是中断请求。
-
CPU 把当前页码(返回地址)压栈做书签,然后翻到中断向量表里对应的那一行,看上面写着“跳到第 X 页”——那一页就是中断服务程序 ISR。
-
ISR 跑完后,CPU 再从栈里把原页码弹出来,继续读主程序。
以 Arduino Uno(ATmega328P)为例
芯片上电时,Flash 最前面 32 个字(64 字节)就是这张向量表,每一行都是一个 2 字节地址:
表格
复制
向量号 | 地址 | 源 | 典型用途 |
---|---|---|---|
1 | 0x0000 | 复位 | 上电或 RESET 引脚触发 |
2 | 0x0002 | INT0 | 外部中断 0(比如 attachInterrupt(0, …) 的 D2) |
3 | 0x0004 | INT1 | 外部中断 1(D3) |
4 | 0x0006 | PCINT0 | 引脚变化中断(整组 D8~D13) |
… | … | … | … |
Arduino 的
attachInterrupt()
函数帮你做了三件事:-
把你写的 ISR 地址填到对应向量位置(运行时通过函数指针表重映射)。
-
设置 MCU 控制寄存器,使能对应的“中断允许位”。
-
退出时再恢复现场,把 CPU 交还主循环。
一句话总结
中断向量就是“中断→地址”的硬件级跳转表。写 Arduino 时,你通常不用直接改它,只需知道:
当某个中断触发时,CPU 会自动跳到向量表里登记好的 ISR 入口,这就是“中断向量”的作用。
当某个中断触发时,CPU 会自动跳到向量表里登记好的 ISR 入口,这就是“中断向量”的作用。
为什么称作向量表,哪里是向量?
“向量”这个词在中文里容易让人联想到“方向的量”,但在计算机体系结构里,它只是沿用了早期数学/物理里“一个量包含多个分量”的概念,而不必有方向意义。
具体地说:
-
向量 = 一串地址分量
在 8 位 AVR 里,每个中断对应一个 16 位(2 字节)地址,这 2 个字节被看成“指向 ISR 的向量”。把一排这样的地址按中断号顺序排好,就是向量表(vector table)。 -
为什么叫“向量”而不是“地址表”?
历史沿袭:-
早期大型机/小型机文档里,把“中断号 → 入口地址”的映射关系叫 interrupt vector;
-
Intel、Motorola 等厂商沿用了这个术语;
-
中文翻译直接音译/意译为“中断向量”,于是就有了“向量表”。
-
-
并没有“方向”含义
你完全可以把它想成 “中断入口地址数组”,只是约定俗成叫“向量表”。
一句话:
在 MCU 里,“向量”就是“入口地址”这个 16 位量;把它们按中断号排成一行,就成了“向量表”。
在 MCU 里,“向量”就是“入口地址”这个 16 位量;把它们按中断号排成一行,就成了“向量表”。
那么如果把Arduino 这种单核 MCU 换成esp32这种双核呢?
把场景从 Arduino-AVR 单核 MCU 搬到 ESP32(双核 Xtensa LX6) 以后,问题要拆成两层来看:
-
“中断是否仍比轮询更快?”
-
“中断检查还是每条指令后都做一次吗?”
1) 响应延迟:中断 vs 轮询
表格
复制
项目 | 单核 AVR | 双核 ESP32 |
---|---|---|
核心数 | 1 | 2(通常称 PRO CPU / APP CPU) |
主频 | 16 MHz | 80~240 MHz |
中断延迟 | 3~5 µs | 典型 < 1 µs(进入 C-level ISR) |
并行的可能性 | 无 | 可以把主循环、Wi-Fi、BLE、按键 ISR 分别绑在不同核,真正的并行 |
在 ESP32 上:
-
如果你把 按键 GPIO 中断绑在 Core 0,而 主循环跑在 Core 1,那么:
-
Core 0 一旦检测到边沿,立即跳往 ISR,无需等 Core 1 的 loop() 逛完一圈;
-
因此 延迟下限比单核更低,且不会受到 Core 1 里 Wi-Fi、HTTP、UI 等耗时任务的干扰。
-
-
如果你只用单核(menuconfig 里可关掉双核),ESP32 仍比 AVR 快一个数量级,但“中断更快”的结论与 AVR 一致——只是绝对数值更小。
2) 中断采样时机
Xtensa 的异常机制与 AVR 类似:
-
每条指令退休(retire)后,硬件检查有没有 pending 的异常/中断;
-
如果有,且全局中断使能,则把 PC 压栈,跳到向量表对应条目;
-
所以仍然是 “指令完成后” 而不是“指令执行前”去查表。
-
与 AVR 不同的是:Xtensa 还有 中断等级(level)与优先级,高优先级可打断低优先级 ISR,但检查点仍一样。
3) 额外注意:FreeRTOS & GPIO 中断
ESP32 在 Arduino Core 里跑的是 FreeRTOS:
-
attachInterrupt()
实际注册的是 GPIO 中断 → ISR → 任务通知 的二级分发:-
硬件 ISR 里只做“清标志+发通知”;
-
真正用户代码在 高优先级 FreeRTOS 任务 里执行。
-
-
因此你看到的“中断延迟”其实是 硬件 ISR+任务切换 的总和,仍 < 10 µs,远快于轮询。
一句话总结
-
“中断更快”在 ESP32 上依旧成立,且因双核并行、更高主频,延迟进一步缩小。
-
中断采样时机仍是“每条指令退休”后做一次,与 AVR 思想一致,只是实现细节不同。
全部评论