GPIO 快速跳变优化:让 ISR 只做必须做的事
Hall、干簧管、按键或防拆输入快速跳变时,如果硬中断回调直接操作协议栈、Flash、LED 定时器或复杂状态机,就容易出现延迟、丢事件、资源压力和难以复现的异常。更稳妥的设计是 ISR 采样并投递,主任务消抖、合并和执行副作用。
先确认回调在哪个上下文
看到一个名为 gpio_callback 的函数,不能默认它运行在普通任务。排查前应追完整调用链:
1 | IRQ handler |
重点确认:
- 回调是否在硬中断中同步调用;
- 是否分配动态 buffer;
- 是否发送无线命令;
- 是否读写非易失存储;
- 是否重启共享软件定时器;
- 下一条触发边沿何时重新配置。
如果一路追到了协议发送、日志格式化或存储操作,ISR 已经承担了过多业务。
为什么快速输入会放大问题
机械抖动、磁性输入临界变化和外部干扰都可能产生短时间多次边沿。此时直接在 ISR 处理业务会带来三个问题。
ISR 执行时间不可控
协议栈发送可能需要申请资源、检查网络状态并进入多层调用。执行时间越长,越容易推迟其他中断和系统调度。
中间状态产生重复副作用
如果每个边沿都重启 LED pattern、更新属性并发送一次消息,最终稳定状态到来前已经制造了大量无效操作。
休眠和唤醒产生竞态
低功耗设备可能刚记录一个候选电平就重新进入睡眠,或者按旧电平配置下一次唤醒边沿,导致状态收敛不完整。
推荐的职责划分
ISR:采样、记账、快速重臂
伪代码如下:
1 | void gpio_isr(void) |
ISR 不做这些事情:
- 不发送协议消息;
- 不更新复杂应用状态;
- 不操作 Flash;
- 不等待消抖时间;
- 不重启多个业务定时器;
- 不打印大段格式化日志。
ISR 与主任务共享的数据需要满足平台的原子性和可见性要求;在 RTOS 中也应选择明确支持 ISR 的事件或队列 API。
主任务:稳定态收敛
主任务维护候选电平、变化代次和稳定时间:
1 | 收到变化事件 |
最终动作按确定顺序执行:
1 | 更新本地状态 |
这样可以把一串抖动边沿合并成一次业务变化。
不要忘记低功耗门控
只把逻辑搬出 ISR 还不够。设备可能在消抖完成前重新睡眠,因此 pending 状态必须参与休眠决策:
1 | pending input exists -> keep awake |
进入睡眠前建议:
- 读取当前电平;
- 配置相反电平或边沿为唤醒条件;
- 再读取一次;
- 如果两次不一致或仍有 pending,放弃本轮睡眠。
唤醒后也不应把初始化读到的第一个瞬态值直接上报,应重新进入稳定态收敛。
消抖时间没有万能值
固定写成 20 ms、50 ms 或 200 ms 都不能自动证明正确。窗口应由输入器件、电气特性、用户动作和功耗目标共同决定。
建议至少覆盖:
- 单次正常动作;
- 临界位置缓慢变化;
- 快速连续切换;
- 长按或长期保持;
- 普通睡眠与 retention 唤醒;
- 网络繁忙和 buffer 紧张场景。
验证时记录 ISR 次数、最终业务事件数、最大收敛时间、是否阻止休眠以及复位原因。预期关系通常是“多次边沿,至多一次最终业务变化”。
证据边界
代码审查可以证明 ISR 中存在不适合的调用链,也能证明改造后职责已经分离;但它不能单独证明某次现场复位的根因。复位类型仍需要启动原因、看门狗记录、异常寄存器或可复现压力测试支持。