Hall、干簧管、按键或防拆输入快速跳变时,如果硬中断回调直接操作协议栈、Flash、LED 定时器或复杂状态机,就容易出现延迟、丢事件、资源压力和难以复现的异常。更稳妥的设计是 ISR 采样并投递,主任务消抖、合并和执行副作用。

先确认回调在哪个上下文

看到一个名为 gpio_callback 的函数,不能默认它运行在普通任务。排查前应追完整调用链:

1
2
3
4
5
IRQ handler
-> GPIO driver
-> registered callback
-> application logic
-> protocol / timer / storage

重点确认:

  • 回调是否在硬中断中同步调用;
  • 是否分配动态 buffer;
  • 是否发送无线命令;
  • 是否读写非易失存储;
  • 是否重启共享软件定时器;
  • 下一条触发边沿何时重新配置。

如果一路追到了协议发送、日志格式化或存储操作,ISR 已经承担了过多业务。

为什么快速输入会放大问题

机械抖动、磁性输入临界变化和外部干扰都可能产生短时间多次边沿。此时直接在 ISR 处理业务会带来三个问题。

ISR 执行时间不可控

协议栈发送可能需要申请资源、检查网络状态并进入多层调用。执行时间越长,越容易推迟其他中断和系统调度。

中间状态产生重复副作用

如果每个边沿都重启 LED pattern、更新属性并发送一次消息,最终稳定状态到来前已经制造了大量无效操作。

休眠和唤醒产生竞态

低功耗设备可能刚记录一个候选电平就重新进入睡眠,或者按旧电平配置下一次唤醒边沿,导致状态收敛不完整。

推荐的职责划分

ISR:采样、记账、快速重臂

伪代码如下:

1
2
3
4
5
6
7
8
9
void gpio_isr(void)
{
bool level = gpio_read(INPUT_PIN);

latest_level = level;
change_generation++;
gpio_arm_opposite_edge(INPUT_PIN, level);
event_set(GPIO_CHANGED);
}

ISR 不做这些事情:

  • 不发送协议消息;
  • 不更新复杂应用状态;
  • 不操作 Flash;
  • 不等待消抖时间;
  • 不重启多个业务定时器;
  • 不打印大段格式化日志。

ISR 与主任务共享的数据需要满足平台的原子性和可见性要求;在 RTOS 中也应选择明确支持 ISR 的事件或队列 API。

主任务:稳定态收敛

主任务维护候选电平、变化代次和稳定时间:

1
2
3
4
5
6
收到变化事件
-> 读取最新物理电平
-> 与 candidate 比较
-> 变化则重置 stable_since
-> 稳定窗口达到
-> 一次性更新状态和副作用

最终动作按确定顺序执行:

1
2
3
4
更新本地状态
-> 更新属性
-> 更新指示
-> 发送最终上报

这样可以把一串抖动边沿合并成一次业务变化。

不要忘记低功耗门控

只把逻辑搬出 ISR 还不够。设备可能在消抖完成前重新睡眠,因此 pending 状态必须参与休眠决策:

1
2
pending input exists -> keep awake
stable state committed -> clear pending

进入睡眠前建议:

  1. 读取当前电平;
  2. 配置相反电平或边沿为唤醒条件;
  3. 再读取一次;
  4. 如果两次不一致或仍有 pending,放弃本轮睡眠。

唤醒后也不应把初始化读到的第一个瞬态值直接上报,应重新进入稳定态收敛。

消抖时间没有万能值

固定写成 20 ms、50 ms 或 200 ms 都不能自动证明正确。窗口应由输入器件、电气特性、用户动作和功耗目标共同决定。

建议至少覆盖:

  • 单次正常动作;
  • 临界位置缓慢变化;
  • 快速连续切换;
  • 长按或长期保持;
  • 普通睡眠与 retention 唤醒;
  • 网络繁忙和 buffer 紧张场景。

验证时记录 ISR 次数、最终业务事件数、最大收敛时间、是否阻止休眠以及复位原因。预期关系通常是“多次边沿,至多一次最终业务变化”。

证据边界

代码审查可以证明 ISR 中存在不适合的调用链,也能证明改造后职责已经分离;但它不能单独证明某次现场复位的根因。复位类型仍需要启动原因、看门狗记录、异常寄存器或可复现压力测试支持。