中断管理¶
中断是嵌入式系统实时响应外部事件的核心机制。在 FreeRTOS 环境中,中断的使用有特殊规则——必须使用专用的
FromISRAPI,且中断优先级需要正确配置。理解这些规则是避免系统崩溃的关键。
中断与 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 中配置中断优先级¶
- System Core → NVIC
- 确保需要使用 RTOS API 的中断,抢占优先级 ≥ 5
- 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 的中断优先级都在管理范围内。