跳转至

中断管理

中断是嵌入式系统实时响应外部事件的核心机制。在 FreeRTOS 环境中,中断的使用有特殊规则——必须使用专用的 FromISR API,且中断优先级需要正确配置。理解这些规则是避免系统崩溃的关键。


中断与 RTOS 的关系

裸机中的中断

在裸机中,中断可以随意使用全局变量、调用函数,因为没有任务切换的问题。

FreeRTOS 中的中断

FreeRTOS 运行时,调度器管理着任务切换和内核数据结构。如果中断随意操作这些结构,会导致数据损坏系统崩溃

graph TD
    subgraph "中断上下文"
        ISR[中断服务程序<br>ISR]
    end

    subgraph "任务上下文"
        T1[任务 1]
        T2[任务 2]
        SCH[FreeRTOS 调度器]
    end

    ISR -->|"只能调用<br>FromISR API"| SCH
    ISR -.->|"❌ 不能调用<br>普通 RTOS API"| SCH
    T1 -->|"调用普通 API"| SCH
    T2 -->|"调用普通 API"| SCH

核心规则

在中断中,只能调用以 FromISR 结尾的 FreeRTOS API!

调用普通 API(如 xQueueSend)会导致不可预测的错误,包括但不限于:硬件错误(HardFault)、数据损坏、系统死锁。


CMSIS_V2 的中断简化

好消息是,CMSIS_V2 封装层会自动检测当前上下文——它在内部判断是否在中断中,自动选择调用普通版本还是 FromISR 版本:

/* CMSIS_V2 中,同一个 API 在中断和任务中都能用! */

// 在任务中调用 → 内部调用 xQueueSend()
osMessageQueuePut(queueHandle, &data, 0, osWaitForever);

// 在中断中调用 → 内部自动调用 xQueueSendFromISR()
osMessageQueuePut(queueHandle, &data, 0, 0);  // timeout 必须为 0

中断中 timeout 必须为 0

虽然 CMSIS_V2 自动处理了 FromISR 调用,但在中断中超时参数必须设为 0(不等待)。中断中不允许任何阻塞操作。

CMSIS_V2 中可在中断中使用的 API

API 中断中可用 注意事项
osMessageQueuePut() timeout = 0
osMessageQueueGet() timeout = 0
osSemaphoreRelease()
osSemaphoreAcquire() timeout = 0
osEventFlagsSet()
osThreadFlagsSet()
osTimerStart()
osTimerStop()
osDelay() 绝不能在中断中延时
osMutexAcquire() 互斥量不能在中断中使用
osThreadNew() 不能在中断中创建任务

中断优先级配置

FreeRTOS 管理的中断范围

FreeRTOS 并不管理所有中断,它只管理优先级在特定范围内的中断:

graph TB
    subgraph "中断优先级(STM32 数字越小优先级越高)"
        P0["优先级 0-4<br>FreeRTOS 不管理<br>不能调用任何 RTOS API"]
        P5["优先级 5 = configMAX_SYSCALL_INTERRUPT_PRIORITY<br>← FreeRTOS 可管理的最高优先级"]
        P6["优先级 6-14<br>FreeRTOS 管理<br>可以调用 FromISR API"]
        P15["优先级 15 = configKERNEL_INTERRUPT_PRIORITY<br>← 最低优先级(PendSV/SysTick)"]
    end

    P0 --- P5 --- P6 --- P15

    style P0 fill:#ff6b6b,color:#fff
    style P5 fill:#ffa600,color:#fff
    style P6 fill:#51cf66,color:#fff
    style P15 fill:#339af0,color:#fff
配置项 默认值 含义
configKERNEL_INTERRUPT_PRIORITY 15 最低优先级,给 PendSV、SysTick 用
configMAX_SYSCALL_INTERRUPT_PRIORITY 5 可以调用 FreeRTOS API 的最高中断优先级

关键规则

  • 优先级 0~4 的中断:FreeRTOS 管不了,绝不能调用任何 RTOS API(包括 FromISR 版本)
  • 优先级 5~15 的中断:可以调用 FromISR API
  • 在 CubeMX 中,外部中断的 NVIC 优先级必须 ≥ 5(抢占优先级)才能安全使用 RTOS API

CubeMX 中配置中断优先级

  1. System Core → NVIC
  2. 确保需要使用 RTOS API 的中断,抢占优先级 ≥ 5
  3. SysTick 和 PendSV 的优先级由 FreeRTOS 自动管理(15),不要修改

STM32 的优先级分组

CubeMX 中 FreeRTOS 默认使用 4 位抢占优先级 + 0 位子优先级(NVIC_PRIORITYGROUP_4)。这意味着抢占优先级范围是 0~15,没有子优先级。

在 FreeRTOS 配置中可以看到 LIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY = 5,对应的就是这个阈值。


延迟中断处理(Deferred Interrupt Processing)

问题:中断执行时间过长

// ⚠️ 不好的做法:在中断里做太多事
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    // 解析数据包(耗时)
    parse_packet(rx_buffer, 256);
    // CRC校验(耗时)
    if (verify_crc(rx_buffer))
    {
        // 处理命令(耗时)
        execute_command(rx_buffer);
    }
    // 整个中断执行了好几百 us,其他中断和任务都被延迟了!
}

解决方案:中断里只做最少的事

sequenceDiagram
    participant HW as 硬件中断
    participant ISR as 中断处理(快)
    participant Sem as 信号量/队列
    participant Task as 处理任务(慢)

    HW->>ISR: 中断触发
    Note over ISR: ① 读取/保存原始数据<br>② 清除中断标志<br>③ 通知任务
    ISR->>Sem: 释放信号量
    Note over ISR: 退出中断(< 10μs)
    Sem->>Task: 唤醒任务
    Note over Task: ④ 执行耗时处理<br>(解析、校验、响应)

代码示例

osSemaphoreId_t UartRxSemHandle;
uint8_t rx_buffer[256];
uint16_t rx_len;

/* 中断回调:快速处理,立即退出 */
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart,
                                 uint16_t Size)
{
    if (huart->Instance == USART1)
    {
        rx_len = Size;
        /* ① 只做最少的事:保存长度 + 通知任务 */
        osSemaphoreRelease(UartRxSemHandle);

        /* ② 重启接收(很快) */
        HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_buffer, 256);
    }
}

/* 处理任务:慢速处理 */
void UART_Process_Task(void *argument)
{
    for (;;)
    {
        if (osSemaphoreAcquire(UartRxSemHandle, osWaitForever) == osOK)
        {
            /* 在任务上下文中做耗时操作 */
            parse_packet(rx_buffer, rx_len);     // 解析数据包
            if (verify_crc(rx_buffer, rx_len))   // CRC 校验
            {
                execute_command(rx_buffer);       // 执行命令
                send_response();                  // 发送回复
            }
        }
    }
}

常见中断场景

场景 1:外部中断(按键/传感器触发)

osSemaphoreId_t ButtonSemHandle;

/* GPIO 外部中断回调 */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    if (GPIO_Pin == KEY_Pin)
    {
        osSemaphoreRelease(ButtonSemHandle);
    }
}

/* 按键处理任务 */
void Key_Task(void *argument)
{
    for (;;)
    {
        if (osSemaphoreAcquire(ButtonSemHandle, osWaitForever) == osOK)
        {
            osDelay(50);  // 消抖

            if (HAL_GPIO_ReadPin(KEY_GPIO_Port, KEY_Pin) == GPIO_PIN_RESET)
            {
                /* 确认按下,执行功能 */
                toggle_mode();
            }
        }
    }
}

场景 2:定时器中断 + 队列

osMessageQueueId_t TimDataQueueHandle;

/* 硬件定时器中断回调(例如 TIM2 每 1ms 触发) */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance == TIM2)
    {
        /* 在中断中读取编码器值 */
        int32_t encoder_value = __HAL_TIM_GET_COUNTER(&htim3);

        /* 通过队列发送给处理任务 */
        osMessageQueuePut(TimDataQueueHandle, &encoder_value, 0, 0);
    }
}

/* 编码器数据处理任务 */
void Encoder_Task(void *argument)
{
    int32_t value;
    int32_t last_value = 0;

    for (;;)
    {
        if (osMessageQueueGet(TimDataQueueHandle, &value, NULL,
                              osWaitForever) == osOK)
        {
            int32_t speed = value - last_value;
            last_value = value;

            /* 用速度值做 PID 控制 */
            pid_update(speed);
        }
    }
}

场景 3:DMA 传输完成

osSemaphoreId_t SPI_DmaSemHandle;

/* SPI DMA 传输完成回调 */
void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi)
{
    if (hspi->Instance == SPI1)
    {
        osSemaphoreRelease(SPI_DmaSemHandle);
    }
}

/* SPI 通信任务 */
void SPI_Task(void *argument)
{
    uint8_t tx_data[32] = { /* ... */ };
    uint8_t rx_data[32];

    for (;;)
    {
        /* 启动 DMA 传输 */
        HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_RESET);
        HAL_SPI_TransmitReceive_DMA(&hspi1, tx_data, rx_data, 32);

        /* 等待传输完成(不轮询,不浪费 CPU) */
        if (osSemaphoreAcquire(SPI_DmaSemHandle, 100) == osOK)
        {
            HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET);
            process_spi_data(rx_data);
        }
        else
        {
            /* 超时处理 */
            HAL_SPI_Abort(&hspi1);
            HAL_GPIO_WritePin(CS_GPIO_Port, CS_Pin, GPIO_PIN_SET);
        }

        osDelay(10);
    }
}

临界区

什么是临界区?

临界区是一段不能被中断打断的代码。在临界区内,调度器被挂起,中断(低于阈值的)被禁用。

使用方法

/* 方法 1:禁用/恢复中断(原生 FreeRTOS API) */
taskENTER_CRITICAL();
{
    // 这里的代码不会被中断打断
    // 也不会发生任务切换
    shared_variable++;
}
taskEXIT_CRITICAL();

/* 方法 2:挂起/恢复调度器(只禁止任务切换,不影响中断) */
osKernelLock();
{
    // 不会发生任务切换,但中断仍然可以触发
    update_shared_data();
}
osKernelUnlock();

临界区的注意事项

  • 尽量短暂:临界区会禁用中断/调度,时间过长会影响系统实时性
  • 不能在临界区内调用 RTOS API(如 osDelay
  • 适合保护非常短的不适合用互斥量的操作(如修改一个变量)
  • 对于复杂共享资源,使用互斥量更合适

临界区 vs 互斥量

对比 临界区 互斥量
保护方式 禁用中断/调度 任务级锁(阻塞等待)
影响范围 全局(所有中断和任务) 仅等待该互斥量的任务
适用时长 极短(几行代码) 可以较长
嵌套使用 支持 需要递归互斥量
在 ISR 中 ✅ 可以 ❌ 不行

SysTick 与 HAL 时基

FreeRTOS 占用了 SysTick

FreeRTOS 使用 SysTick 定时器作为系统心跳(tick),驱动任务调度。但 HAL 库也需要一个时基来实现 HAL_Delay()HAL_GetTick()

CubeMX 的处理方式:

配置项 说明
FreeRTOS 时基 SysTick 固定使用 SysTick
HAL 时基 TIM1(或其他定时器) 不能和 FreeRTOS 共用 SysTick

CubeMX 自动处理

当你在 CubeMX 中启用 FreeRTOS 时,它会自动提示你将 HAL 时基改为其他定时器(如 TIM1)。务必按提示修改,否则 HAL_Delay() 行为异常。

修改路径:System Core → SYS → Timebase Source 改为 TIM1(或其他未使用的定时器)。


中断安全设计的最佳实践

设计原则清单

原则 说明
ISR 要短 中断处理 < 10μs,复杂逻辑交给任务
正确的 API 中断中只用 FromISR API,或 CMSIS_V2 API + timeout=0
优先级正确 需调用 RTOS API 的中断,抢占优先级 ≥ 5
不阻塞 中断中绝不 delay、不等待、不获取互斥量
用信号量/队列通知 中断产生事件 → 信号量/队列 → 任务处理
HAL 时基分离 FreeRTOS 用 SysTick,HAL 用 TIMx

典型的中断 + RTOS 架构

graph TD
    subgraph 中断层(快速响应)
        EXTI[外部中断<br>按键/传感器] -->|信号量| SEM1[BinarySem]
        UART_IRQ[串口中断<br>数据接收] -->|队列| Q1[RxQueue]
        TIM_IRQ[定时器中断<br>编码器采样] -->|队列| Q2[DataQueue]
        DMA_IRQ[DMA 完成<br>ADC/SPI] -->|信号量| SEM2[DmaSem]
    end

    subgraph 任务层(复杂处理)
        SEM1 --> TASK1[按键任务]
        Q1 --> TASK2[通信任务]
        Q2 --> TASK3[控制任务]
        SEM2 --> TASK4[数据处理任务]
    end

常见问题

中断中调用了普通 API,为什么有时候不崩溃?

这是未定义行为——有时恰好没出问题,但:

  • 在负载增大、任务增多时随时可能崩溃
  • 可能导致难以复现的偶发性 HardFault
  • 正确做法是始终使用 FromISR API 或 CMSIS_V2 的 timeout=0 调用

为什么 FreeRTOS 不管理所有中断?

保留高优先级中断(0~4)给极高实时性要求的外设(如紧急停机、安全检测)。这些中断不受 FreeRTOS 影响,即使调度器被锁定也能触发。

硬件定时器中断能调用 RTOS API 吗?

可以,只要该定时器中断的抢占优先级 ≥ 5(≥ configMAX_SYSCALL_INTERRUPT_PRIORITY)。在 CubeMX 的 NVIC 配置中检查。

中断嵌套会影响 FreeRTOS 吗?

ARM Cortex-M 天然支持中断嵌套(高优先级中断可打断低优先级中断)。FreeRTOS 与此兼容,但需要确保所有调用 RTOS API 的中断优先级都在管理范围内。