3. RTOS中的中断管理:中断嵌套、中断延迟、临界区保护

中断管理,说白了就是RTOS的"应急响应系统"。你想想看,一个嵌入式设备运行时,突然来了个外部事件——按键按下、数据到达、定时器溢出——CPU得立刻放下手头工作去处理。怎么处理?处理到一半又来一个更紧急的中断怎么办?这就是我们今天要聊的核心。

我个人习惯把中断管理比作医院的急诊室。普通任务就像门诊病人,排队等着。中断就像急救病人,来了就得优先处理。而中断嵌套,就是急救过程中又来了一位更危重的病人。

3.1 中断嵌套:优先级决定一切

中断嵌套,简单说就是"中断里套中断"。当一个低优先级中断正在执行时,来了一个高优先级中断,CPU会暂停当前中断服务程序,先去处理高优先级中断。处理完再回来继续执行低优先级中断。

为什么会这样?因为实时系统里,有些事件比另一些事件更紧急。比如电机堵转保护的中断,肯定比串口接收中断优先级高——电机烧了可比丢几个字节严重多了。

中断嵌套的核心规则:

  • 高优先级中断可以打断低优先级中断
  • 同级中断不能互相打断(取决于硬件设计)
  • 中断嵌套层数不宜过深,一般建议不超过3层

我在项目中遇到过一个问题:一个系统设计了5层中断嵌套,结果在极端情况下,最内层的中断服务程序执行时间过长,导致外层中断响应延迟超标。后来我强制规定:中断服务程序里只做最必要的事,复杂逻辑扔给任务去处理。

3.2 中断延迟:躲不开的"响应时间"

中断延迟,就是从中断信号产生到CPU开始执行中断服务程序第一行指令之间的时间。这个时间越短,系统实时性越好。

中断延迟主要由以下几部分组成:

延迟来源 说明 典型时间
硬件响应时间 CPU识别中断、保存上下文的时间 几十ns ~ 几μs
关中断时间 其他代码关闭中断导致无法响应 取决于临界区长度
高优先级中断执行 正在处理更高优先级的中断 取决于ISR执行时间
指令完成时间 CPU必须完成当前指令才能响应中断 几个时钟周期

注意:关中断时间过长是中断延迟的"头号杀手"。我曾经在一个项目中,有人在临界区里做了大量计算,导致关中断时间达到毫秒级,结果串口数据频繁丢失。排查了两天才找到原因。

嗯,这里要注意:中断延迟不是越小越好。过小的中断延迟意味着频繁的上下文切换,反而会降低系统整体吞吐量。关键是要"可预测"——你能保证最坏情况下的中断延迟不超过某个值。

3.3 临界区保护:别让共享数据"打架"

临界区,就是访问共享资源的代码段。这个共享资源可能是全局变量、硬件寄存器、缓冲区等。如果中断和任务同时访问同一个共享资源,数据就乱套了。

举个例子:一个全局变量 counter,任务A在循环里做 counter++,中断服务程序里也做 counter++。你想想看,如果任务A刚读取了counter的值,还没来得及写回去,中断来了,ISR修改了counter。等任务A恢复执行时,它用旧值加1再写回去——ISR的那次修改就被覆盖了。

这就是典型的"竞态条件"。

3.4 临界区保护方法

RTOS里常用的临界区保护方法有几种:

  • 关中断法:进入临界区前关中断,出来后再开中断。最简单粗暴,但会影响中断响应。
  • 信号量保护:用二值信号量保护共享资源。中断里用 give,任务里用 take
  • 互斥量:带优先级继承的互斥量,可以防止优先级反转。
  • 关调度器:只禁止任务切换,不关中断。适合任务间共享资源保护。

我的建议:中断服务程序和任务之间的共享资源,用关中断法最保险。任务之间的共享资源,用互斥量或信号量。别混用,否则容易出bug。

我曾经在一个项目中,用信号量保护中断和任务共享的缓冲区。结果中断里调用 give 时,信号量内部操作又触发了任务切换,导致递归调用。嗯,后来改成关中断法,问题就解决了。

3.5 代码示例:临界区保护实战

/* 共享缓冲区 */
static uint8_t buffer[256];
static uint32_t write_index = 0;
static uint32_t read_index = 0;

/* 中断服务程序:写入数据 */
void UART_ISR(void)
{
    uint8_t data;
    
    /* 读取硬件数据 */
    data = UART->DR;
    
    /* 进入临界区:关中断 */
    taskENTER_CRITICAL();
    
    buffer[write_index] = data;
    write_index = (write_index + 1) % 256;
    
    /* 退出临界区:开中断 */
    taskEXIT_CRITICAL();
}

/* 任务:读取数据 */
void Read_Task(void *param)
{
    uint8_t data;
    
    while(1)
    {
        /* 进入临界区 */
        taskENTER_CRITICAL();
        
        if(read_index != write_index)
        {
            data = buffer[read_index];
            read_index = (read_index + 1) % 256;
            
            /* 退出临界区 */
            taskEXIT_CRITICAL();
            
            /* 处理数据 */
            ProcessData(data);
        }
        else
        {
            /* 退出临界区 */
            taskEXIT_CRITICAL();
            
            /* 缓冲区为空,等待 */
            vTaskDelay(10);
        }
    }
}

避坑指南:我曾经在临界区里调用 printf 调试,结果 printf 内部又触发了中断,导致死锁。记住:临界区里不要调用任何可能触发中断或任务切换的函数。

3.6 中断与任务的通信设计

中断服务程序只做最紧急的事,然后把数据通过某种机制传递给任务。常用的通信方式有:

  • 消息队列:ISR把数据放入队列,任务从队列读取。适合数据流场景。
  • 信号量:ISR释放信号量,任务等待信号量。适合事件通知场景。
  • 共享缓冲区:ISR写入,任务读取。需要临界区保护。
  • 任务通知:RTOS提供的轻量级通信机制,比信号量更快。

我个人习惯用消息队列。为什么?因为消息队列自带缓冲,ISR可以连续发多条数据,任务慢慢处理。信号量只能通知"有事件",具体是什么事件还得另外传。

下面这张图展示了中断与任务通信的典型流程:

中断与任务通信流程图 硬件外设 中断信号 ISR (关中断保护) 放入数据 消息队列 (FIFO缓冲) 长度:N 读取数据 任务 (处理数据) 关键说明: 1. 硬件外设产生中断信号,CPU响应后执行ISR 2. ISR在关中断保护下,将数据放入消息队列 3. 消息队列作为缓冲,解耦ISR和任务的执行速度 4. 任务从队列中读取数据,进行后续处理 5. 队列满时,ISR可选择丢弃或覆盖旧数据(取决于设计) 注意:ISR中不能调用阻塞函数,如等待信号量、延时等

3.7 中断延迟的测量与优化

怎么测量中断延迟?我常用的方法:

  1. 在中断服务程序入口处,读取一个定时器的计数值
  2. 在中断服务程序出口处,再读一次
  3. 差值就是中断响应时间
  4. 多次测量取最大值,就是最坏情况中断延迟

优化中断延迟的几个方向:

  • 缩短临界区:关中断的时间越短越好,能关1μs就别关10μs
  • ISR做最少的活:只做数据搬运,不做数据处理
  • 合理设置优先级:紧急的中断优先级高,不紧急的优先级低
  • 使用零延迟中断:有些RTOS支持零延迟中断,完全不经过RTOS调度

小技巧:我习惯在临界区前后加调试输出,记录关中断的时间戳。这样能直观看到哪些临界区拖慢了系统。不过记得调试完要删掉这些输出,否则它们本身也会影响时序。

嗯,中断管理这块内容就这些。记住三个核心点:中断嵌套要控制深度,中断延迟要可预测,临界区保护要选对方法。做到这三点,你的RTOS系统就稳了一半。

本章核心要点:

  • 中断嵌套:高优先级打断低优先级,层数不宜超过3层
  • 中断延迟:关中断时间是主要影响因素,要控制在可预测范围内
  • 临界区保护:ISR与任务共享资源用关中断法,任务间共享用信号量/互斥量
  • ISR与任务通信:消息队列最常用,ISR不能调用阻塞函数

公众号:蓝海资料掘金营,微信deep3321