4. 中断优化策略:让CPU喘口气
中断延迟,说白了就是CPU从收到中断信号到真正执行ISR第一行代码的时间差。这个差值如果太大,你的系统就可能丢数据、丢事件,甚至直接崩掉。我见过不少项目,前期功能跑得挺好,一上压力测试就各种诡异问题,最后查下来都是中断延迟惹的祸。
这一章,我们集中火力解决四个核心问题:临界区怎么优化、ISR怎么瘦身、优先级怎么分配、以及传说中的零延迟中断到底怎么用。
4.1 临界区优化:别把整个系统锁死
临界区,就是那些不能被中断打断的代码段。最常见的做法是关中断、开中断。但问题在于——很多人关得太多了。
我自己的优化原则很简单:临界区只做三件事——读/写共享变量、修改寄存器、更新链表指针。其他事情,一律挪出去。
| 操作类型 | 是否适合留在临界区 | 说明 |
|---|---|---|
| 读写全局变量 | ✅ 适合 | 通常几条指令,很快 |
| 操作硬件寄存器 | ✅ 适合 | 原子操作,必须保护 |
| 打印调试信息 | ❌ 不适合 | 串口输出太慢,微秒级变毫秒级 |
| 动态内存分配 | ❌ 不适合 | malloc可能触发系统调用,时间不可控 |
| 复杂计算 | ❌ 不适合 | 算完再赋值,别在临界区里算 |
举个例子,一个常见的临界区优化手法是双缓冲。ISR往缓冲区A写数据,主循环从缓冲区B读数据。两个缓冲区通过一个原子指针交换。这样临界区只需要交换指针,几行代码搞定。
// 优化前:临界区里拷贝大量数据
void ISR_Handler(void) {
enter_critical();
memcpy(shared_buf, new_data, 1024); // 1024字节拷贝,太慢了
exit_critical();
}
// 优化后:临界区只交换指针
void ISR_Handler(void) {
enter_critical();
active_buf = (active_buf == bufA) ? bufB : bufA; // 一行搞定
exit_critical();
// 数据拷贝放到主循环里做
}
4.2 中断服务函数瘦身:ISR不是主程序
我曾经接手过一个项目,ISR里写了300多行代码,包括状态机、协议解析、甚至还有浮点运算。结果呢?中断延迟飙到200微秒以上,系统动不动就丢包。
ISR的黄金法则是:能不做,就不做。ISR只做最紧急的事,剩下的交给任务或主循环。
具体来说,ISR里应该只做这些:
- 读取硬件状态寄存器,清除中断标志
- 把数据放到一个轻量级队列或缓冲区
- 发送信号量或事件标志,唤醒等待的任务
其他所有事情——协议解析、数据校验、业务逻辑——全部放到任务里做。
瘦身前:ISR里做协议解析 + 数据校验 + 状态更新,耗时约150μs
瘦身后:ISR只做数据入队 + 发信号量,耗时约5μs
中断延迟从150μs降到5μs,系统吞吐量提升3倍
// 瘦身前:ISR里什么都干
void UART_ISR(void) {
uint8_t byte = UART->DR;
parse_protocol(byte); // 协议解析
validate_checksum(); // 校验
update_state_machine(); // 状态更新
process_command(); // 命令处理
}
// 瘦身后:ISR只做一件事
void UART_ISR(void) {
uint8_t byte = UART->DR;
ringbuf_push(&rx_queue, byte); // 入队,几微秒
os_semaphore_post(&rx_sem); // 发信号量
}
4.3 中断优先级分配策略:谁急谁先上
中断优先级分配,不是随便排个123就完事的。分配不好,高优先级中断可能被低优先级中断阻塞,这叫优先级反转。
我一般按这个原则分配:
- 时间敏感型:最高优先级。比如定时器中断、电机控制PWM中断。这类中断一旦延迟,系统行为就出问题。
- 数据丢失敏感型:次高优先级。比如UART、SPI、I2C接收中断。延迟可能导致硬件FIFO溢出,丢数据。
- 事件通知型:中等优先级。比如GPIO外部中断、按键中断。延迟一点问题不大,但不能无限期延迟。
- 后台服务型:最低优先级。比如RTC闹钟、看门狗喂狗。这些可以等。
| 中断类型 | 推荐优先级 | 延迟容忍度 | 典型例子 |
|---|---|---|---|
| 时间敏感 | 最高 | <1μs | 定时器、PWM、编码器 |
| 数据丢失敏感 | 高 | <10μs | UART、SPI、DMA完成 |
| 事件通知 | 中 | <100μs | GPIO、按键、触摸 |
| 后台服务 | 低 | <1ms | RTC、看门狗、ADC采样完成 |
4.4 零延迟中断技术:真的能零延迟吗?
零延迟中断,听起来很玄乎。其实它不是一个硬件特性,而是一种设计模式。核心思想是:让ISR的执行时间趋近于零,或者说,让ISR只做一件事——设置一个标志位或触发一个硬件动作,然后立即返回。
真正的零延迟中断,通常依赖硬件机制:
- 硬件直接内存访问(DMA):数据从外设直接搬到内存,不需要CPU介入。中断只在DMA传输完成时触发一次。
- 硬件事件链接(Event Link):某些MCU支持外设之间直接连接,比如定时器溢出直接触发ADC采样,完全绕过CPU。
- 中断嵌套与抢占:高优先级中断可以打断低优先级ISR,保证紧急事件零延迟响应。
我举个例子。一个高速数据采集系统,ADC每1μs产生一次中断。如果每个中断都进ISR,CPU根本忙不过来。这时候零延迟中断的做法是:
- ADC数据通过DMA直接搬到内存缓冲区
- DMA传输完成中断每1ms触发一次
- ISR里只做一件事:交换缓冲区指针,发信号量给处理任务
这样,中断频率从1MHz降到1kHz,CPU负载从90%降到5%。
// 零延迟中断的典型实现
void DMA_TransferComplete_ISR(void) {
// 只做一件事:交换缓冲区
uint8_t *tmp = current_buf;
current_buf = ready_buf;
ready_buf = tmp;
// 通知任务处理
os_semaphore_post(&process_sem);
// ISR结束,总共不到10条指令
}
4.5 本章知识结构图
下面这张图总结了中断优化的四个核心方向,以及它们之间的关系:
4.6 本章小结
中断优化没有银弹。临界区优化、ISR瘦身、优先级分配、零延迟中断,这四个策略要组合使用。我个人习惯是:先做ISR瘦身,把耗时操作挪出去;然后优化临界区,保证共享数据安全;再根据时间敏感度分配优先级;最后用DMA或事件链接实现真正的零延迟。
嗯,这里要注意:优化不是一蹴而就的。我建议你在项目初期就定好中断策略,而不是等到系统出问题了再回头改。中断优先级一旦定下来,后期改动的成本很高——所有依赖中断的任务都可能受影响。