4. 中断服务函数设计原则:短小精悍、避免阻塞、关中断时间最小化、可重入性
在嵌入式实时系统中,中断服务函数(ISR)是响应外部事件最直接的途径。UART 接收中断尤其频繁——每收到一个字节就会触发一次。如果 ISR 写得臃肿或阻塞,轻则丢字节,重则导致系统任务调度瘫痪。本章提炼出四条核心设计原则,并逐一拆解其背后的工程理由与落地方法。
4.1 原则一:短小精悍
ISR 应只做“最必要”的事:读取硬件寄存器、保存数据、清除中断标志。任何复杂计算、循环等待、内存分配都应移到任务级处理。
| 对比项 | ISR 内处理 | 任务级处理 |
|---|---|---|
| 典型操作 | 读 RDR、写 FIFO、清标志 | 协议解析、CRC 校验、粘包重组 |
| 执行时间 | 微秒级(1–5 µs) | 毫秒级(可被调度) |
| 对系统影响 | 阻塞其他中断 | 不影响中断响应 |
经验法则:ISR 内不要出现 for/while 循环次数不可控的代码;不要调用 printf、malloc、semaphore_take(blocking) 等可能阻塞的函数。
4.2 原则二:避免阻塞
阻塞是 ISR 的大忌。一旦 ISR 进入等待状态,同级和低优先级中断全部被屏蔽,系统实时性瞬间崩溃。常见的阻塞陷阱包括:
- 自旋等待: while (USART_ISR_TXE == 0); 等待发送完成。
- 信号量阻塞获取: 在 ISR 中调用 xSemaphoreTake() 而非 xSemaphoreGiveFromISR()。
- 互斥锁: 试图在 ISR 中获取 mutex,导致优先级反转甚至死锁。
正确做法:ISR 中只使用“FromISR”后缀的 RTOS API(如 xQueueSendFromISR、xSemaphoreGiveFromISR),且不等待资源。
4.3 原则三:关中断时间最小化
关中断是保护临界区的必要手段,但关中断时间过长会直接拉高中断延迟,导致高优先级事件丢失。UART 接收 ISR 中应尽量使用“无锁”或“原子操作”来避免长时间关中断。
典型反例:在 ISR 内用 for 循环搬运 256 字节到软件缓冲区,期间一直关中断。这会导致 SysTick 中断延迟,系统时钟漂移。
优化策略:
- 使用双缓冲或环形缓冲区: ISR 只做单字节入队,关中断仅持续几条指令。
- 利用硬件 FIFO: 设置 FIFO 触发阈值(如 8 字节),减少 ISR 进入次数。
- 临界区粒度细化: 只包裹“读索引 + 写索引”的原子更新,不包裹整个缓冲区操作。
4.4 原则四:可重入性
可重入性指同一个 ISR 在第一次调用尚未返回时,可以被再次安全调用。UART 接收 ISR 天然具有可重入风险——当多个 UART 通道共用同一个 ISR 入口,或高优先级中断嵌套时,共享数据可能被破坏。
保证可重入性的方法:
| 方法 | 说明 | 适用场景 |
|---|---|---|
| 局部变量优先 | 所有临时数据使用栈变量,不依赖全局/静态变量 | 所有 ISR |
| 原子访问 | 对全局标志、索引使用 LDREX/STREX 或关中断保护 | 多中断源共享缓冲区 |
| 专用缓冲区 | 每个 UART 通道拥有独立的环形缓冲区 | 多 UART 场景 |
| 禁止在 ISR 内调用不可重入函数 | 如 rand()、strtok()、非线程安全的库函数 | 所有 ISR |
关键检查点:ISR 中使用的每个全局变量或外设寄存器,都要问自己:“如果这个 ISR 在中间被另一个中断打断,数据会乱吗?” 如果会,就需要加保护或改用局部变量。
4.5 四条原则的协同关系
四条原则并非孤立存在,而是相互支撑:
- 短小精悍 是 避免阻塞 的前提——代码短了,自然不容易引入阻塞调用。
- 关中断时间最小化 依赖 可重入性 设计——只有数据访问是原子的,才敢开中断处理其他事件。
- 可重入性 反过来又要求 ISR 短小精悍,因为复杂逻辑更容易引入全局依赖。
实践自检清单:写完 ISR 后,逐条核对——① ISR 内有无循环/阻塞调用?② 关中断区间是否只保护了必要指令?③ 所有全局变量是否被多个中断路径访问?④ 能否把超过 10 行的逻辑移到任务中?
遵循这四条原则,UART 接收 ISR 才能在高频中断环境下稳定运行,为后续的超时检测与粘包处理打下坚实基础。