ISR 里用任何 API,按顺序套:
FromISR 后缀的版本(QueueSend / QueueReceive / Give / Notify / EventGroupSetBits)一句话:会阻塞或要任务身份的,ISR 里都禁用;其余一律 FromISR 版。
中断是"快进快出"的上下文——不能睡、不能等。FromISR 版 API 不阻塞,且不立即切换上下文,只打标志等中断返回后再调度。
pxHigherPriorityTaskWoken 写 pdTRUE,ISR 末尾调 portYIELD_FROM_ISR(...),调度器在中断返回的安全点才真正切换优先级继承要绑定"哪个任务持有资源"并临时提升它的优先级。ISR 不是任务——没有身份、没有可提升的优先级。所以互斥量的 give/take 只发生在任务上下文。类比:VIP 插队权只发给占坑位的"任务",不发给"不排队的外来者"。二值/计数信号量的 give 不需要知道是谁,所以有 FromISR 版;互斥量靠持有者身份,没有。
/* 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 版 | |
|---|---|---|
| 阻塞吗 | 会(带超时等待) | 不会(失败立即返回) |
| 唤醒高优先级任务 | 立即切换上下文 | 只置标志,中断返回后才切换 |
| 适用 | 任务上下文 | 中断 / 任务都可用 |
xQueueReceiveFromISR(&q, &buf, &woken); → 合法(receive 不阻塞,队列空就返回失败)xSemaphoreTakeFromISR(...) → 不存在(take 会"等",必然阻塞);give 只给信号量、不给互斥量xEventGroupSetBitsFromISR(&eg, 0x01); → 合法(事件组 setBits 不阻塞、无需任务身份,有 FromISR 版;别以为只有队列/信号量才有)| 相邻概念 | 关系 | 孤立理解会犯的错 |
|---|---|---|
| 临界区 | FromISR 版内部也有数据保护,但用中断掩码而非全局关中断 | 以为中断里不能有任何保护机制 |
| 任务同步方式 | xTaskNotifyFromISR 是 ISR→任务一对一最轻的通道 |
以为中断里只有队列能用 |
| 优先级反转 | 互斥量不能进 ISR 的根因 = 优先级继承需要任务上下文 | 在中断里给互斥量 give |
| 裸机中断 | 裸机只有一种上下文;RTOS 引入任务上下文 → 中断和任务隔离 | 沿用裸机习惯直接改共享变量、不通知任务 |
2026-08-03
每主题一页,复习时只翻本目录。