关联文章
FreeRTOS第11章 队列(Queue)学习大纲
https://blog.youkuaiyun.com/SeventeenChen/article/details/155699651?sharetype=blogdetail&sharerId=155699651&sharerefer=PC&sharesource=SeventeenChen&spm=1011.2480.3001.8118
11.4中断中的读写队列

1. 中断写队列 (xQueueSendFromISR)
这是最常见的场景:硬件产生数据 -> 中断接收 -> 丢进队列 -> 任务处理。 比如:串口收到一个字节、ADC 转换完成、GPIO 捕获到脉冲。
核心代码模板,请务必背下这三步走:
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
// 1. 定义一个变量,用于标记是否需要切换任务
// 默认是 pdFALSE(不需要切换)
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
uint8_t rx_data = (uint8_t)(huart->Instance->DR & 0xFF);
// 2. 使用 FromISR 函数发送
// 参数3:传入变量的地址 (&xHigherPriorityTaskWoken)
// 如果队列满了,它会直接返回 errQUEUE_FULL,不会阻塞
xQueueSendFromISR(xQueue, &rx_data, &xHigherPriorityTaskWoken);
// 3. 强制上下文切换 (退出中断后直接去跑被唤醒的高优先级任务)
// 如果 xHigherPriorityTaskWoken 变成了 pdTRUE,这就话会让 CPU “插队”
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

2. 中断读队列 (xQueueReceiveFromISR)
这个场景比较少见,但也有用。 通常用于:硬件需要数据 -> 中断触发 -> 从队列拿数据 -> 喂给硬件。 比如:DAC 输出(定时器中断每隔一段时间从队列拿一个点输出波形)、串口发送中断(发完一个字节,从队列拿下一个接着发)。
void TIM3_IRQHandler(void)
{
// ... 清除中断标志位等 ...
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
uint16_t dac_value;
// 尝试从队列读数据
if (xQueueReceiveFromISR(xDACQueue, &dac_value, &xHigherPriorityTaskWoken) == pdPASS) {
// 读到了,写入硬件寄存器
HAL_DAC_SetValue(&hdac, DAC_CHANNEL_1, DAC_ALIGN_12B_R, dac_value);
} else {
// 队列空了(波形发完了),可能需要停止定时器或输出默认值
}
// 同样需要进行上下文切换检查
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
3. 重要补充与避坑指南 (进阶)
除了基本的读写,在中断中使用队列还有以下几个极其重要的补充点:





11.5关于中断写队列 例子
这里为你提供 3 个最经典、最实用 的 xQueueSendFromISR 使用场景示例。
这三个例子涵盖了:简单数据(按键)、外设通信(串口) 以及 结构体打包(定时器采样)。代码均基于 STM32 HAL 库风格,因为这是最常见的开发环境。
场景一:GPIO 外部中断(按键检测)
场景描述: 当用户按下按键时,触发外部中断。中断服务函数立即把“哪个按键被按下了(按键ID)”发送给任务去处理(如播放按键音、切换菜单)。
// 假设这是一个简单的 uint8_t 类型的队列,存按键 ID
extern QueueHandle_t xKeyQueue;
// STM32 的 GPIO 外部中断回调函数
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
// 1. 定义唤醒标志 (必须初始化为 pdFALSE)
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
uint8_t key_id = 0;
// 2. 判断是哪个引脚触发的中断
if (GPIO_Pin == GPIO_PIN_0) {
key_id = 1; // 1号键
} else if (GPIO_Pin == GPIO_PIN_13) {
key_id = 2; // 2号键
}
if (key_id != 0) {
// 3. 【关键】发送到队列
// 即使队列满了,这里也不会卡死,直接返回 errQUEUE_FULL
xQueueSendFromISR(xKeyQueue, &key_id, &xHigherPriorityTaskWoken);
}
// 4. 【关键】强制上下文切换
// 如果刚才的发送唤醒了一个高优先级的"按键处理任务"
// CPU 退出中断后,会直接跳去运行那个任务,而不是回到之前的低优先级任务
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
场景二:串口接收中断(字节流接收)
场景描述: 这是最常用的场景。STM32 串口每收到一个字节,就触发一次中断。我们需要把这个字节丢进队列,让后台任务去解析指令(比如遥控小车的前进后退)。
extern QueueHandle_t xUartRxQueue;
extern UART_HandleTypeDef huart1;
// 串口接收完成回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
// 只处理串口 1
if (huart->Instance == USART1) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// 获取刚才收到的数据 (假设存在寄存器或 DR 中)
// HAL库通常需要你先开启接收,这里简化演示拿到数据的情况
uint8_t received_char = (uint8_t)(huart->Instance->DR & 0xFF);
// 【关键】发送字符到队列
xQueueSendFromISR(xUartRxQueue, &received_char, &xHigherPriorityTaskWoken);
// 如果需要继续接收,通常这里要重新开启中断接收
// HAL_UART_Receive_IT(&huart1, ...);
// 【关键】切换调度
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
}
注意:如果波特率非常高(如 921600),这种“收一个字节进一次中断发一次队列”的方式效率太低,建议配合 DMA + 空闲中断 + 队列(传缓冲区指针)使用。
场景三:定时器中断(发送结构体/传感器数据)
场景描述: 平衡小车中,需要每隔 5ms 精确采集一次 MPU6050 数据或编码器速度。我们在定时器中断里读取数据,打包成结构体,发给 PID 计算任务。
// 1. 定义数据包结构体
typedef struct {
float angle;
float speed;
uint32_t timestamp;
} CarState_t;
extern QueueHandle_t xCarStateQueue;
// 定时器更新中断 (假设 5ms 触发一次)
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance == TIM3) {
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
CarState_t current_state;
// --- 模拟读取硬件数据 ---
current_state.angle = MPU6050_GetAngle(); // 读角度
current_state.speed = Encoder_GetSpeed(); // 读速度
current_state.timestamp = xTaskGetTickCountFromISR(); // 获取当前时刻
// 【关键】发送结构体
// FreeRTOS 会把 current_state 的内容完整的 memcpy 到队列里
if (xQueueSendFromISR(xCarStateQueue, ¤t_state, &xHigherPriorityTaskWoken) != pdPASS) {
// 队列满了!说明 PID 任务处理太慢了,跟不上 5ms 的节奏
// 这里可以做一个错误计数 Error_Count++;
}
// 【关键】立刻去唤醒等待数据的 PID 任务
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
}
进阶:紧急插队 (xQueueSendToFrontFromISR)
场景描述: 小车正在运行,突然“倾角保护中断”触发了(车倒了)。这时候不管队列里还有多少个“前进”指令排队,必须立刻发送一个“停机”指令,并让接收任务第一个读到它。
void Emergency_Button_IRQHandler(void)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
// 定义一个特殊的停机指令
Command_t stop_cmd = { .type = CMD_STOP, .value = 0 };
// 【关键】使用 SendToFront (插队到队头)
xQueueSendToFrontFromISR(xCmdQueue, &stop_cmd, &xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
总结:通用模板
不管什么中断,套路永远是这三步:
-
定义标志:
BaseType_t xHigherPriorityTaskWoken = pdFALSE; -
发送数据:
xQueueSendFromISR(..., &xHigherPriorityTaskWoken); -
切换调度:
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
930

被折叠的 条评论
为什么被折叠?



