中断(ISR, Interrupt Service Routine)是嵌入式系统中硬件异步通知 CPU 的机制。在 FreeRTOS 中使用中断有严格的规则,违反会导致程序崩溃或数据损坏。
两套 API:为什么要有 FromISR 版本
FreeRTOS 的很多 API 函数内部会执行阻塞操作(如让任务进入 Blocked 状态),但 ISR 不是任务,没有任务上下文,不能被阻塞。如果在 ISR 中调用了会导致阻塞的 API,内核会进入异常状态。
因此,FreeRTOS 为中断提供了专用 API,函数名带 FromISR 后缀:
| 任务版本 | ISR 版本 | 功能 |
|---|---|---|
xQueueSend() / xQueueSendToBack() | xQueueSendFromISR() | 发送到队尾 |
xQueueSendToFront() | xQueueSendToFrontFromISR() | 发送到队头 |
xQueueReceive() | xQueueReceiveFromISR() | 接收数据 |
xQueuePeek() | xQueuePeekFromISR() | 查看但不删除 |
xQueueOverwrite() | xQueueOverwriteFromISR() | 覆盖写入(邮箱用) |
uxQueueMessagesWaiting() | uxQueueMessagesWaitingFromISR() | 查询消息数 |
xSemaphoreGive() | xSemaphoreGiveFromISR() | 释放信号量 |
xSemaphoreTake() | — | ISR 中不应 Take 信号量(会阻塞) |
xEventGroupSetBits() | xEventGroupSetBitsFromISR() | 设置事件组 bit |
vTaskNotifyGiveFromISR() | 本身就是 ISR 版本 | 任务通知 Give |
xTaskNotifyFromISR() | 本身就是 ISR 版本 | 任务通知(通用) |
绝对不能在 ISR 中做的事
- 调用不带
FromISR后缀的 API- 调用任何可能导致阻塞的函数(
vTaskDelay()、xSemaphoreTake()带非零超时等)- 使用
printf()等不可重入函数(除非你的 stdio 库是线程安全且 ISR 安全的)- 动态分配/释放内存(
pvPortMalloc/vPortFree不是 ISR 安全的,除非用 heap_3)
xHigherPriorityTaskWoken 参数
所有 FromISR API 都有一个共同参数 pxHigherPriorityTaskWoken,它用于实现中断退出时的任务切换。
执行流程
中断发生
│
▼
ISR 开始执行
│
▼
xQueueSendFromISR() / xSemaphoreGiveFromISR() 等
├─ 内部检测:是否有更高优先级任务因此被唤醒?
│ ├─ 是 → *pxHigherPriorityTaskWoken = pdTRUE
│ └─ 否 → 保持 pdFALSE
▼
portYIELD_FROM_ISR(xHigherPriorityTaskWoken)
├─ pdTRUE → 请求上下文切换 → ISR 退出后直接运行被唤醒的高优先级任务
└─ pdFALSE → 不切换 → ISR 退出后回到被打断的任务
使用模板
void EXTI0_IRQHandler(void)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE; // 必须初始化为 pdFALSE
// 执行各种 FromISR 调用...
xSemaphoreGiveFromISR(xBinarySem, &xHigherPriorityTaskWoken);
// 也可以多次调用,xHigherPriorityTaskWoken 会被自动"或"上 pdTRUE
// 最后:如果需要,进行上下文切换
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
不要忘记初始化
xHigherPriorityTaskWoken必须在声明时初始化为pdFALSE。未初始化的局部变量值不确定,可能导致在不该切换的时候切换。
中断与回调的区别
在 05-软件定时器 中会详细讲回调函数,这里先明确它和中断的本质区别:
| 中断(ISR) | 回调函数 | |
|---|---|---|
| 触发者 | 硬件(外设电信号) | 软件(系统/框架调用) |
| 触发方式 | CPU 被强制打断 | 普通函数调用 |
| 执行上下文 | 中断上下文(特殊) | 任务上下文(Daemon Task) |
| 能否阻塞 | ❌ 绝对不能 | ⚠️ 不建议(但不会崩溃) |
| 能否用 printf | ❌ 危险 | ✅ 可以 |
| 响应速度 | 微秒级,极快 | 取决于调度 |
| 注册方式 | 中断向量表 | 传函数指针 |
延迟中断处理(Deferred Interrupt Processing)
ISR 应该越短越好,复杂处理交给任务去做。这是 FreeRTOS 推荐的最佳实践。
原因:
- ISR 执行期间所有任务都被暂停(高优先级任务也无法运行)
- ISR 过长会影响系统实时性,可能导致其他中断被延迟或丢失
- ISR 中能做的操作很受限(不能阻塞、不能分配内存等)
标准模式:二值信号量同步
最经典的模式:ISR 只做最少的硬件操作(清中断标志、读取数据),然后释放二值信号量唤醒处理任务。
// 全局信号量句柄
SemaphoreHandle_t xBinarySem;
// 初始化时创建二值信号量
void vInit(void)
{
xBinarySem = xSemaphoreCreateBinary();
// 注意:xSemaphoreCreateBinary() 创建的信号量初始为空!
// 旧版 API(已废弃)vSemaphoreCreateBinary() 初始为满
xTaskCreate(vHandlerTask, "Handler", 128, NULL, 3, NULL);
}
// ISR:只做最必要的事
void EXTI0_IRQHandler(void)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// 1. 清除中断标志
HAL_GPIO_EXTI_IRQHandler(GPIO_PIN_0);
// 2. 释放信号量,通知处理任务
xSemaphoreGiveFromISR(xBinarySem, &xHigherPriorityTaskWoken);
// 3. 如果需要,切换到处理任务
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
// 处理任务:做耗时的复杂处理
void vHandlerTask(void *pvParameters)
{
for(;;)
{
// 等待信号量(阻塞,不占 CPU)
xSemaphoreTake(xBinarySem, portMAX_DELAY);
// 到这里说明中断发生了,做真正的处理:
// - 滤波算法
// - 数据打包
// - printf 日志
// - 发送队列等
vProcessInterruptData();
}
}
二值信号量 vs 计数信号量
| 二值信号量 | 计数信号量 | |
|---|---|---|
| 容量 | 最多 1 个 | 可以多个(最大值在创建时指定) |
| 事件会不会丢 | 处理慢了会丢(连续中断只记录一次) | 不会丢,每个事件都计数 |
| 适合场景 | 中断同步(事件发生了就行) | 多事件计数、资源池管理 |
| 创建函数 | xSemaphoreCreateBinary() | xSemaphoreCreateCounting(uxMaxCount, uxInitialCount) |
高速中断用计数信号量
如果中断频率很高,处理任务来不及处理每次中断,用二值信号量会丢失事件(多次中断合并为一次)。这时应该用计数信号量,每次中断 Give 一次,处理任务 Take 一次,确保每个中断都被处理。
任务优先级 vs 中断优先级
这是两个完全独立的概念,不要混淆:
- 任务优先级:软件层面,由 FreeRTOS 调度器管理,数字越大优先级越高
- 中断优先级:硬件层面,由 CPU NVIC(嵌套向量中断控制器)管理,数字和逻辑优先级的关系取决于架构
Cortex-M 特别注意
在 ARM Cortex-M 上,中断优先级数字越小,逻辑优先级越高(0 为最高)。这和任务优先级的规则相反!
临界区与中断优先级的关系
FreeRTOS 通过 configMAX_SYSCALL_INTERRUPT_PRIORITY 定义了一条”安全线”:
- 优先级 ≥ 此值(数字更小,逻辑优先级更高)的中断:不受 FreeRTOS 管理,可以嵌套,但不能调用任何 FreeRTOS API
- **优先级 ≤ 此值(数字更大,逻辑优先级更低)**的中断:可以调用
FromISRAPI,FreeRTOS 临界区能暂时屏蔽它们
Cortex-M 优先级(数字越小优先级越高):
优先级 0 ─┐
... │ 高于 configMAX_SYSCALL_INTERRUPT_PRIORITY
优先级 5 ─┤ (不能调用 FreeRTOS API,但不会被临界区延迟)
... │
───────────┼─ configMAX_SYSCALL_INTERRUPT_PRIORITY(安全线)
... │
优先级 10 ─┤ 低于 configMAX_SYSCALL_INTERRUPT_PRIORITY
... │ (可以调用 FromISR API,临界区会暂时屏蔽)
优先级 15 ─┘
│
└─ configKERNEL_INTERRUPT_PRIORITY(Tick 中断优先级,设为最低)
配置原则
configKERNEL_INTERRUPT_PRIORITY(FreeRTOS 自己用的中断,如 SysTick)必须设为最低逻辑优先级(最大数字)- 所有调用 FromISR API 的中断优先级必须设为 ≤
configMAX_SYSCALL_INTERRUPT_PRIORITY(数字更大,逻辑优先级更低)- 需要极快响应、不调用 API 的中断(如高优先级定时器)可以设在安全线以上
任务切换宏
| 宏 | 使用场景 |
|---|---|
taskYIELD() | 在任务中主动让出 CPU |
portYIELD_FROM_ISR(xHigherPriorityTaskWoken) | ISR 末尾请求切换 |
portEND_SWITCHING_ISR(xHigherPriorityTaskWoken) | 旧名,功能同上,已不推荐 |
下一步:05-软件定时器