4、中断服务程序(ISR)设计原则:ISR应短小精悍、避免阻塞、不执行耗时操作

TinyML 设备的中断系统是连接物理世界与推理引擎的“神经末梢”。在资源极度受限的 MCU 上,一个设计不当的 ISR 不仅会破坏实时性,还可能导致推理任务因优先级反转或栈溢出而崩溃。本章聚焦 ISR 设计的三大铁律——短小精悍、避免阻塞、不执行耗时操作,并深入探讨如何在事件唤醒与推理任务之间划定清晰、安全的边界。

中断源 传感器数据就绪 (GPIO/EXTI) 定时器溢出 (TIM) DMA传输完成 (DMA) 通信接收 (UART/I2C/SPI) ISR 设计核心原则 ① 短小精悍 只做标志位/计数/数据搬运 ② 避免阻塞 禁止 delay/while 等待/信号量获取 ③ 不执行耗时操作 禁止浮点运算/推理/日志打印 推荐模式:置标志位 → 退出中断 主循环/RTOS任务中处理实际工作 主循环/任务 检查事件标志 读取传感器数据 特征提取 TinyML推理 事件标志 图:TinyML 中断事件唤醒与推理任务边界划分示意

4.1 核心原理:为什么 ISR 必须“短小精悍”

在 TinyML 设备中,ISR 运行在 CPU 的异常上下文,拥有最高抢占优先级。当 ISR 执行时,所有普通任务(包括推理任务)都被挂起。如果 ISR 占用 CPU 时间过长,会导致以下连锁问题:

  • 推理实时性受损:例如一个 10ms 的 ISR 会阻塞 100Hz 控制环路的调度,导致推理结果输出延迟。
  • 中断嵌套风险:长时间 ISR 期间,更高优先级的中断无法及时响应,可能丢失关键事件(如 DMA 溢出)。
  • 栈溢出隐患:ISR 使用独立栈空间,若在其中调用复杂函数(如 printf、浮点运算),栈消耗剧增,在 8KB RAM 的 MCU 上极易溢出。

“短小精悍”的本质是:ISR 只做“记录事件”和“最小必要的数据搬运”,将“事件处理”和“数据计算”全部推迟到主循环或 RTOS 任务中。这符合嵌入式系统设计的“临界区最小化”原则。

经验法则:ISR 执行时间应控制在 10µs 以内(对于 48MHz Cortex-M0+ 约 500 个时钟周期)。如果 ISR 超过 50µs,必须重新审视设计——是否将耗时操作错误地放入了中断上下文。

4.2 判断方法与操作流程:如何设计一个合规的 ISR

设计 ISR 时,可以遵循以下“四步审查法”来确保其符合原则:

  1. 列出 ISR 必须完成的最小动作:通常只有三类——置位事件标志、读取并保存硬件寄存器数据、清除中断挂起位。
  2. 检查是否有任何阻塞调用:包括 delay、while 循环等待硬件状态、获取互斥锁/信号量(带阻塞超时)。
  3. 评估是否有耗时操作:浮点运算(尤其软件浮点)、除法、memcpy 大块数据、日志输出(UART 发送)都应禁止。
  4. 确认数据一致性:ISR 与主循环共享的变量必须使用 volatile 修饰,并考虑原子操作或临界区保护。

以下是一个典型的合规 ISR 代码模板(以 STM32 外部中断为例):

// 全局事件标志(volatile 保证可见性)
volatile uint32_t g_event_flags = 0;
#define EVT_SENSOR_READY  (1UL << 0)

// 传感器数据缓冲区(ISR 与主循环共享)
volatile uint16_t g_sensor_raw[8];
volatile uint8_t  g_sensor_idx = 0;

void EXTI0_IRQHandler(void) {
    // 1. 清除中断挂起位(必须最先做)
    if (EXTI->PR & EXTI_PR_PR0) {
        EXTI->PR = EXTI_PR_PR0;
    }
    
    // 2. 读取硬件数据寄存器(最小必要操作)
    if (g_sensor_idx < 8) {
        g_sensor_raw[g_sensor_idx++] = (uint16_t)(ADC1->DR & 0xFFF);
    }
    
    // 3. 置位事件标志,通知主循环
    g_event_flags |= EVT_SENSOR_READY;
    
    // 4. 立即返回,不做任何其他处理
}

4.3 案例拆解:加速度计唤醒中断导致推理卡顿

背景与约束:某可穿戴设备使用 LIS3DH 加速度计,通过 INT1 引脚触发外部中断唤醒 MCU。MCU 为 STM32L431(80MHz,64KB RAM),运行轻量级 TFLite Micro 推理模型(约 20ms 推理时间)。设备要求:当检测到剧烈运动时,立即启动推理判断是否为跌倒。

初始现象:设备在跌倒检测时频繁出现“假阳性”和“响应延迟”。示波器测量发现,从运动发生到推理输出结果的时间长达 180ms,远超设计指标 50ms。

关键参数与日志:通过调试 GPIO 翻转测量 ISR 执行时间,发现 ISR 耗时高达 45ms。进一步检查代码发现,ISR 中直接调用了 TFLite 推理函数 interpreter->Invoke(),并且还包含 printf 打印传感器原始数据。

分析判断过程:

  • 推理函数 Invoke() 包含大量浮点运算和循环,在 80MHz MCU 上需要 20-40ms,严重阻塞了其他中断。
  • printf 通过 UART 发送,波特率 115200,发送 100 字节需要约 8.7ms,且 UART 发送是阻塞式的。
  • 两个耗时操作叠加,导致 ISR 占用 CPU 时间过长,系统实时性崩溃。

实际操作步骤:

  1. 将推理调用从 ISR 中移除,改为在 ISR 中仅置位 EVT_FALL_DETECT 标志。
  2. 将 printf 替换为环形缓冲区记录,由主循环定期刷新日志。
  3. 在主循环中检测到标志后,先读取完整的加速度计 FIFO 数据(通过 SPI DMA),再进行推理。
  4. 为推理任务设置 10ms 超时看门狗,防止异常卡死。

结果对比:

指标 优化前 优化后
ISR 执行时间 45ms 2.5µs
事件唤醒到推理启动延迟 180ms 8ms
系统中断响应最坏情况 50ms+ 5µs
RAM 额外消耗 0(但栈溢出风险高) +256B(环形缓冲区)

风险与代价:优化后,推理结果延迟增加了约 5ms(因为需要等待主循环调度),但整体响应时间从 180ms 降至 13ms,完全满足 50ms 指标。代价是代码结构稍复杂,需要管理事件标志和环形缓冲区。

复盘经验:ISR 中永远不要出现“业务逻辑”。即使推理函数看起来“很快”,在中断上下文中执行也是危险的——因为中断可能发生在任意时刻,包括正在访问共享外设时。正确的做法是:ISR 只负责“唤醒”,主循环负责“思考”。

4.4 常见误区与总结复盘

以下是 TinyML 开发者在 ISR 设计中常见的五个误区:

误区 后果 正确做法
在 ISR 中调用 HAL_Delay() 系统时钟中断被阻塞,调度器瘫痪 使用非阻塞延时或状态机
在 ISR 中获取 RTOS 信号量(带阻塞) 优先级反转,死锁 使用 xSemaphoreGiveFromISR() 无阻塞版本
在 ISR 中执行浮点运算 CPU 占用剧增(软件浮点库开销大) 将浮点运算移至主循环,ISR 只存整数原始值
ISR 中调用 printf 调试 UART 阻塞,丢失后续中断 使用 DMA + 环形缓冲区异步日志
共享变量未加 volatile 编译器优化导致数据不同步 所有跨上下文变量声明为 volatile
特别注意:在 Cortex-M 系列 MCU 上,即使 ISR 中只做“简单”操作,也要注意中断优先级分组。如果多个外设中断共享优先级,可能出现“中断风暴”——一个 ISR 尚未退出,另一个同优先级中断又触发,导致 CPU 永远在中断上下文中执行,主循环饿死。建议为关键事件(如传感器唤醒)分配最高优先级,为 DMA 完成等分配较低优先级。

总结复盘:ISR 设计的本质是“事件通知”与“事件处理”的职责分离。在 TinyML 场景中,推理任务通常需要 10-100ms 的连续 CPU 时间,这决定了它不能运行在中断上下文中。正确的架构是:

  • 中断层:负责捕获物理事件,置位标志,最小数据搬运(<10µs)。
  • 调度层:主循环或 RTOS 任务轮询标志,决定何时启动推理。
  • 推理层:在任务上下文中执行 TFLite Micro 推理,可被更高优先级中断抢占。

这种分层设计不仅保证了实时性,还让代码更易测试和维护。记住一条铁律:如果你在 ISR 中写了超过 20 行代码,或者看到了 for 循环嵌套,请停下来重新设计。