7.1 KiB
7.1 KiB
FreeRTOS:ISR 上下文 / FromISR API / MPU 受限任务
FreeRTOS 表面很小,**最大的坑不是"简单",而是上下文**:xQueueSend() vs xQueueSendFromISR()
xTaskNotifyGive() vs vTaskNotifyGiveFromISR()
taskENTER_CRITICAL() vs taskENTER_CRITICAL_FROM_ISR()
这些不是可随意互换的别名。 在 ISR 里看到 task 版 API,不能顺着"正常业务逻辑"继续分析——先判断它到底在不在 ISR 上下文、以及是不是符号/包装器识别错了。 </CORE RULE>
一、为什么要有 FromISR 变体
在 ISR 里,调度器实际上是冻结的:ISR 不能阻塞,内核也不能在 ISR 内做任务切换。普通 API 可能阻塞(例如等队列空位)或在内部触发调度——在中断上下文里都是禁止的。
...FromISR() 变体的共同点:不阻塞、使用更轻量的临界区、把调度决定交回给应用。
二、pxHigherPriorityTaskWoken 与 portYIELD_FROM_ISR
- 以
xQueueSendFromISR(queue, item, pxHigherPriorityTaskWoken)为例:若这次发送解除了某个任务的阻塞且该任务优先级高于当前运行任务,则把*pxHigherPriorityTaskWoken置为pdTRUE,表示退出中断前应请求一次上下文切换 - 自 V7.3.0 起该参数可选(可传 NULL)。取舍要清楚:
- 传 NULL 后若调用
portYIELD_FROM_ISR(pdTRUE)→ 总是强制让出(可能是不必要的切换,甚至切到同优先级轮转的对等任务) - 传 NULL 后不 yield → 更高优先级的就绪任务可能一直等到后续事件才被调度
- 传 NULL 后若调用
- 典型形态:
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xQueueSendFromISR(xQueue, &item, &xHigherPriorityTaskWoken);
if (xHigherPriorityTaskWoken) { portYIELD_FROM_ISR(); }
portYIELD_FROM_ISR()并不在调用点立即切换:在 Cortex-M 上通常是 pend PendSV,真正的切换发生在中断退出之后(tail-chaining)——这对时序分析很重要
所以:"函数尾部突然出现调度/yield 相关代码"的 CFG,可能只是 ISR 正常触发高优先级任务切换。
三、configMAX_SYSCALL_INTERRUPT_PRIORITY:越界的中断不能碰任何 API
- 该配置定义允许调用 FreeRTOS API 的最高(数值最低的)中断优先级
- 数值上比它更低(更紧急)的 ISR 不得调用任何 FreeRTOS API(包括 FromISR 版本),也不能使用
portYIELD_FROM_ISR——这类 ISR 只能做纯硬件操作或置标志 - Cortex-M 的临界区通过写 BASEPRI 实现:只屏蔽该优先级及以下的中断,让最紧急的中断仍能通过
- 注意优先级数值方向:Cortex-M/STM32 上数值越小优先级越高,与任务优先级的方向相反——这一条经常导致误判
- 典型故障:ISR 调用了
xQueueSendFromISR但没有设置 NVIC 优先级(默认 0 = 最高),触发端口层的优先级校验断言(vPortValidateInterruptPriority一类)
四、FreeRTOS-MPU:受限任务与运行期改区
受限任务通过 xTaskCreateRestricted(TaskParameters_t *, TaskHandle_t *) 创建。参数是静态配置的结构体:任务函数、名字、栈深度、参数、优先级、栈缓冲,以及 xRegions[portNUM_CONFIGURABLE_REGIONS](Cortex-M3 上为 3 个)。
uxPriority与portPRIVILEGE_BIT相或 → 特权模式任务;否则任务以用户模式运行- 栈缓冲须满足 MPU 约束,常用编译期对齐(如
__attribute__((aligned(...))));传 NULL 时使用端口提供的对齐分配函数 xTaskCreateRestrictedStatic():完全不动态分配(需开启静态分配支持),还要求应用提供StaticTask_t存储——可把它放进内核特权数据区MemoryRegion_t { pvBaseAddress, ulLengthInBytes, ulParameters }:区域的大小与对齐必须是同一个 2 的幂(硬约束)ulParameters可取(可相或):读写、特权只读、只读、特权读写、可缓存/可缓冲、永不执行 等标志- 每次任务切换时 MPU 会被动态重配,为进入的任务授予其自身栈的读写权限
- 运行期可以改区:
vTaskAllocateMPURegions(handle, regions)(handle 传 NULL 表示修改当前任务),底层经端口函数写入硬件
于是:
任务 A 能访问 buffer X,任务 B 访问同一地址 fault
→ 不一定是内存损坏,可能是 MPU 策略
同一任务昨天能访问,运行到某阶段后不能了
→ 可能是因为运行期重新分配了 MPU 区域
另外:特权任务可访问整个内存映射;用户模式任务可访问除"特权专用"之外的全部 RAM/FLASH。端口层提供的降权入口(切到用户模式)是单向的——无法再回到特权模式。
决策树
拿到 FreeRTOS 目标
├─ 看到一个中断服务函数
│ ├─ 它调用的是 task 版还是 FromISR 版 API?
│ ├─ 是 task 版 → 先确认(上下文判定 / 符号或包装器识别)哪里错了
│ └─ 有 pxHigherPriorityTaskWoken + portYIELD_FROM_ISR → ISR 正常触发切换,不是混淆
├─ 中断优先级相关异常
│ ├─ 查该 ISR 的优先级是否高于 configMAX_SYSCALL_INTERRUPT_PRIORITY(更紧急)
│ ├─ 更紧急 → 它不应调用任何 FreeRTOS API
│ └─ 注意数值方向与任务优先级相反
├─ 某地址"有时能访问、有时不能"
│ ├─ 查是否 MPU 受限任务(xTaskCreateRestricted)
│ ├─ 查是否运行期调用了 vTaskAllocateMPURegions
│ └─ 别直接判堆/栈损坏
└─ 上下文判定
└─ 区分任务上下文与 ISR 上下文(能否阻塞、能否触发调度)
工具与验证
- 上下文:ISR 与任务代码的入口、端口层的进入/退出宏、会话中使用的 API 变体
- 优先级:
configMAX_SYSCALL_INTERRUPT_PRIORITY相关配置与各中断的实际 NVIC 优先级 - MPU:任务的
xRegions配置、运行期改区调用、端口层写硬件的路径 - 验证:能同时说清「上下文是任务还是 ISR + 所用 API 变体是否匹配 + 地址权限来自哪组 MPU 区域」
该平台的坑(汇总)
- 把 task 版与 FromISR 版 API 当可互换:阻塞与调度语义完全不同
- 在中断上下文按阻塞语义继续分析:ISR 不能阻塞,也不能在内部触发调度
- 忽略
pxHigherPriorityTaskWoken:漏掉 yield 会让更高优先级就绪任务被推迟 - 以为
portYIELD_FROM_ISR()立即切换:通常是 pend,真正切换在中断退出后 - 把 ISR 尾部的调度代码当混淆:那是正常的切换请求
- 忘记中断优先级数值方向与任务优先级相反:判断"能否调用 API"会反
- 在高于
configMAX_SYSCALL_INTERRUPT_PRIORITY的 ISR 里调用任何 API:连 FromISR 也不行 - 把 MPU 导致的访问失败判成内存损坏:受限任务的区域约束是配置决定的
- 假设 MPU 区域在任务生命周期内不变:运行期可以重新分配
- 忽略"降权到用户模式无法回退":单向行为,不是权限丢失