30.ISR通信.md 7.5 KB

ISR 通信

判别规则(核心,唯一要记的)

ISR 里用任何 API,按顺序套:

  1. 会阻塞/带超时等待吗? 会(take、带超时的 send、vTaskDelay)→ 禁用
  2. 不阻塞的"发送/通知"类 → 用带 FromISR 后缀的版本(QueueSend / QueueReceive / Give / Notify / EventGroupSetBits)
  3. 互斥量(mutex)在 ISR 里彻底禁用——它靠优先级继承,需要任务上下文,无 FromISR 版

一句话:会阻塞或要任务身份的,ISR 里都禁用;其余一律 FromISR 版。

抽象描述(一句话本质)

中断是"快进快出"的上下文——不能睡、不能等。FromISR 版 API 不阻塞,且不立即切换上下文,只打标志等中断返回后再调度。

为什么 FromISR 版特殊(两个关键机制)

  • 不阻塞:队列满/空时直接返回 pdTRUE/pdFALSE,由调用方决定重试或丢弃
  • 不立即切换:唤醒更高优先级任务时只把 pxHigherPriorityTaskWoken 写 pdTRUE,ISR 末尾调 portYIELD_FROM_ISR(...),调度器在中断返回的安全点才真正切换

为什么互斥量不能进 ISR("任务上下文")

优先级继承要绑定"哪个任务持有资源"并临时提升它的优先级。ISR 不是任务——没有身份、没有可提升的优先级。所以互斥量的 give/take 只发生在任务上下文。类比:VIP 插队权只发给占坑位的"任务",不发给"不排队的外来者"。二值/计数信号量的 give 不需要知道是谁,所以有 FromISR 版;互斥量靠持有者身份,没有。

实际例子:UART 中断唤醒高优先级任务

/* data_task(优先级 3):阻塞等队列 */
xQueueReceive(&q, &data, portMAX_DELAY);   /* 队列空 → 挂起 */

/* USART1 中断 */
void USART1_IRQHandler(void) {
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    uint8_t byte = USART1->DR;
    xQueueSendFromISR(&q, &byte, &xHigherPriorityTaskWoken);
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

时序推演(关键是第 3~5 步):

步骤 状态
0 data_task 阻塞在队列上;idle_task(优先级 1)正在跑
1 串口来字节 → CPU 进入 ISR,硬件自动把当前寄存器压进中断栈
2 xQueueSendFromISR:队列空→非空,data_task 从 Blocked 变 Ready;发现其优先级(3) > 正在跑的(1) → 把局部标志写 pdTRUE
3 此刻不切换! ISR 还在执行,中断现场还挂在栈上
4 ISR 末尾 portYIELD_FROM_ISR(pdTRUE) → 触发 PendSV(优先级最低,保证等所有中断处理完)
5 ISR 正常返回,硬件恢复中断现场 → 紧接着 PendSV 执行真正的切换:存 idle_task 现场、载入 data_task 现场
6 data_task 开始跑,处理这个字节

若没唤醒更高优先级任务,标志保持 pdFALSE → 零切换开销,直接返回原任务。

一句话pxHigherPriorityTaskWoken = 中断里的"请假条",ISR 只填"有没有人需要换上来",真正换班(PendSV)在中断收工后的安全点进行。

底层原理:中断跳转流程(计算机组成原理补课)

CPU 怎么知道下一步干嘛:靠 PC(程序计数器)指向下一条指令的内存地址,循环执行「按 PC 取指令 → 执行 → 更新 PC」。程序 = 内存代码区里的一排排指令,PC 像书签指到哪读到哪。

跳转 = 改 PC

  • 函数调用 BL func:先把返回地址存进 LR,再把 PC 改成 func 地址
  • 函数返回 BX LR:把 LR 的地址装回 PC,回到调用处继续

中断跳转 = 强制改 PC + 先存现场(区别于程序自己安排的普通跳转,中断是外部突然插入的):

步骤 动作
硬件压栈:PC(被打断位置)+ R0~R3 + R12 + LR + xPSR 共 8 个寄存器(异常帧,24 字节)存入当前栈
向量表(内存开头的"电话簿",每个中断号对应一个 ISR 入口地址),取出 ISR 地址
强制跳转:PC = ISR 地址,CPU 开始执行 ISR
ISR 干活(如 xQueueSendFromISR)
ISR 返回:BX LR(LR 是 EXC_RETURN 特殊值)
硬件弹栈:把栈里存的旧 PC 弹回 PC
接着执行被打断的那条指令,像没发生过中断

压栈压到谁的栈: | 运行模式 | 用哪个栈 | 异常帧压哪 | | -------- | -------- | -------- | | 裸机(Thread mode + MSP) | MSP | 主栈 | | RTOS 任务被打断(Thread mode + PSP) | PSP | 当前任务的栈 |

这解释了为什么栈估算必须算中断:RTOS 里任务正跑着被中断 → 异常帧压进这个任务的栈。任务栈只算局部变量+调用帧、不算中断嵌套现场,ISR 一触发就可能溢出。(嵌套中断会连续压多个异常帧。)

为什么硬件自动压栈:ISR 要用寄存器/调函数,会覆盖 R0~R3 等。硬件先存好现场,ISR 才能随便用;返回时自动弹栈,原代码无感知。类比:接电话前先把手上东西和"看到第几页"记下来。

类比:PC=读书记号,跳转=翻页,压栈=夹书签。普通跳转=自己翻页,中断跳转=电话铃响,硬件先帮你夹好书签再翻到 ISR 那页。

易混对比

普通版 FromISR 版
阻塞吗 会(带超时等待) 不会(失败立即返回)
唤醒高优先级任务 立即切换上下文 只置标志,中断返回后才切换
适用 任务上下文 中断 / 任务都可用

变体验证(3 题,全过=学会)

  1. ISR 里 xQueueReceiveFromISR(&q, &buf, &woken);合法(receive 不阻塞,队列空就返回失败)
  2. ISR 里 xSemaphoreTakeFromISR(...)不存在(take 会"等",必然阻塞);give 只给信号量、不给互斥量
  3. 陷阱题:ISR 里 xEventGroupSetBitsFromISR(&eg, 0x01);合法(事件组 setBits 不阻塞、无需任务身份,有 FromISR 版;别以为只有队列/信号量才有)

口述要点(面试怎么讲)

  • 结论先行:ISR 里看 API 两件事——会阻塞吗?要任务身份吗?
  • 为什么有 FromISR 版:① 普通 API 资源不可用时会把"当前任务"挂起阻塞,但 ISR 不属于任何任务、没有当前任务可挂,一挂就崩;② 普通 API 靠关中断保护数据结构,ISR 里再关中断会压后更紧急的中断,FromISR 版用中断掩码;③ 切换时机推迟到中断返回后的安全点
  • pxHigherPriorityTaskWoken:为什么不能直接切——中断中途切换会搞乱硬件压栈的中断现场,嵌套中断更危险;所以只登记标志,由 PendSV 在安全点切换
  • 易错点:在中断里给互斥量 give;用带超时的普通 send;以为只有队列/信号量有 FromISR 版

关系网络

相邻概念 关系 孤立理解会犯的错
临界区 FromISR 版内部也有数据保护,但用中断掩码而非全局关中断 以为中断里不能有任何保护机制
任务同步方式 xTaskNotifyFromISR 是 ISR→任务一对一最轻的通道 以为中断里只有队列能用
优先级反转 互斥量不能进 ISR 的根因 = 优先级继承需要任务上下文 在中断里给互斥量 give
裸机中断 裸机只有一种上下文;RTOS 引入任务上下文 → 中断和任务隔离 沿用裸机习惯直接改共享变量、不通知任务

学习日期

2026-08-03


每主题一页,复习时只翻本目录。