中断(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
  • **优先级 ≤ 此值(数字更大,逻辑优先级更低)**的中断:可以调用 FromISR API,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-软件定时器

相关:03-队列06-资源管理返回目录