4. 中断优化策略:让CPU喘口气

中断延迟,说白了就是CPU从收到中断信号到真正执行ISR第一行代码的时间差。这个差值如果太大,你的系统就可能丢数据、丢事件,甚至直接崩掉。我见过不少项目,前期功能跑得挺好,一上压力测试就各种诡异问题,最后查下来都是中断延迟惹的祸。

这一章,我们集中火力解决四个核心问题:临界区怎么优化、ISR怎么瘦身、优先级怎么分配、以及传说中的零延迟中断到底怎么用。

4.1 临界区优化:别把整个系统锁死

临界区,就是那些不能被中断打断的代码段。最常见的做法是关中断、开中断。但问题在于——很多人关得太多了。

常见误区:在临界区里做耗时操作,比如打印日志、内存分配、甚至延时等待。这等于把整个系统的中断响应按在地上摩擦。

我自己的优化原则很简单:临界区只做三件事——读/写共享变量、修改寄存器、更新链表指针。其他事情,一律挪出去。

操作类型是否适合留在临界区说明
读写全局变量✅ 适合通常几条指令,很快
操作硬件寄存器✅ 适合原子操作,必须保护
打印调试信息❌ 不适合串口输出太慢,微秒级变毫秒级
动态内存分配❌ 不适合malloc可能触发系统调用,时间不可控
复杂计算❌ 不适合算完再赋值,别在临界区里算
我的习惯:临界区代码行数控制在5行以内。超过5行,我会重新审视设计——是不是数据结构选错了?是不是锁的粒度太粗了?

举个例子,一个常见的临界区优化手法是双缓冲。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就完事的。分配不好,高优先级中断可能被低优先级中断阻塞,这叫优先级反转。

我一般按这个原则分配:

  1. 时间敏感型:最高优先级。比如定时器中断、电机控制PWM中断。这类中断一旦延迟,系统行为就出问题。
  2. 数据丢失敏感型:次高优先级。比如UART、SPI、I2C接收中断。延迟可能导致硬件FIFO溢出,丢数据。
  3. 事件通知型:中等优先级。比如GPIO外部中断、按键中断。延迟一点问题不大,但不能无限期延迟。
  4. 后台服务型:最低优先级。比如RTC闹钟、看门狗喂狗。这些可以等。
中断类型推荐优先级延迟容忍度典型例子
时间敏感最高<1μs定时器、PWM、编码器
数据丢失敏感高<10μsUART、SPI、DMA完成
事件通知中<100μsGPIO、按键、触摸
后台服务低<1msRTC、看门狗、ADC采样完成
避坑指南:我曾经在一个项目里把两个中断设成了相同优先级,结果它们同时触发时,CPU不知道先处理谁,导致其中一个中断丢失。从那以后,我坚持每个中断优先级都不同,除非硬件强制分组。

4.4 零延迟中断技术:真的能零延迟吗?

零延迟中断,听起来很玄乎。其实它不是一个硬件特性,而是一种设计模式。核心思想是:让ISR的执行时间趋近于零,或者说,让ISR只做一件事——设置一个标志位或触发一个硬件动作,然后立即返回。

真正的零延迟中断,通常依赖硬件机制:

  • 硬件直接内存访问(DMA):数据从外设直接搬到内存,不需要CPU介入。中断只在DMA传输完成时触发一次。
  • 硬件事件链接(Event Link):某些MCU支持外设之间直接连接,比如定时器溢出直接触发ADC采样,完全绕过CPU。
  • 中断嵌套与抢占:高优先级中断可以打断低优先级ISR,保证紧急事件零延迟响应。

我举个例子。一个高速数据采集系统,ADC每1μs产生一次中断。如果每个中断都进ISR,CPU根本忙不过来。这时候零延迟中断的做法是:

  1. ADC数据通过DMA直接搬到内存缓冲区
  2. DMA传输完成中断每1ms触发一次
  3. ISR里只做一件事:交换缓冲区指针,发信号量给处理任务

这样,中断频率从1MHz降到1kHz,CPU负载从90%降到5%。

零延迟中断的本质:不是让ISR跑得更快,而是让ISR几乎什么都不做。把工作交给硬件(DMA、事件链接)或软件任务。
// 零延迟中断的典型实现
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 本章知识结构图

下面这张图总结了中断优化的四个核心方向,以及它们之间的关系:

中断优化策略知识结构 中断优化策略 临界区优化 ISR瘦身 优先级分配 零延迟中断 减少临界区长度 只做入队/发信号 按时间敏感度排序 DMA+事件链接 核心目标:让ISR执行时间趋近于零 把工作交给硬件(DMA/事件链接)或软件任务

4.6 本章小结

中断优化没有银弹。临界区优化、ISR瘦身、优先级分配、零延迟中断,这四个策略要组合使用。我个人习惯是:先做ISR瘦身,把耗时操作挪出去;然后优化临界区,保证共享数据安全;再根据时间敏感度分配优先级;最后用DMA或事件链接实现真正的零延迟。

嗯,这里要注意:优化不是一蹴而就的。我建议你在项目初期就定好中断策略,而不是等到系统出问题了再回头改。中断优先级一旦定下来,后期改动的成本很高——所有依赖中断的任务都可能受影响。

一句话总结:ISR越短,系统越稳。把ISR当成一个快递员——只负责把包裹放到门口,不负责拆包、验货、入库。