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可以连续发多条数据,任务慢慢处理。信号量只能通知"有事件",具体是什么事件还得另外传。
下面这张图展示了中断与任务通信的典型流程:
3.7 中断延迟的测量与优化
怎么测量中断延迟?我常用的方法:
- 在中断服务程序入口处,读取一个定时器的计数值
- 在中断服务程序出口处,再读一次
- 差值就是中断响应时间
- 多次测量取最大值,就是最坏情况中断延迟
优化中断延迟的几个方向:
- 缩短临界区:关中断的时间越短越好,能关1μs就别关10μs
- ISR做最少的活:只做数据搬运,不做数据处理
- 合理设置优先级:紧急的中断优先级高,不紧急的优先级低
- 使用零延迟中断:有些RTOS支持零延迟中断,完全不经过RTOS调度
小技巧:我习惯在临界区前后加调试输出,记录关中断的时间戳。这样能直观看到哪些临界区拖慢了系统。不过记得调试完要删掉这些输出,否则它们本身也会影响时序。
嗯,中断管理这块内容就这些。记住三个核心点:中断嵌套要控制深度,中断延迟要可预测,临界区保护要选对方法。做到这三点,你的RTOS系统就稳了一半。
本章核心要点:
- 中断嵌套:高优先级打断低优先级,层数不宜超过3层
- 中断延迟:关中断时间是主要影响因素,要控制在可预测范围内
- 临界区保护:ISR与任务共享资源用关中断法,任务间共享用信号量/互斥量
- ISR与任务通信:消息队列最常用,ISR不能调用阻塞函数
公众号:蓝海资料掘金营,微信deep3321