4. FreeRTOS队列在RA外设通信中的应用
在RA系列MCU上,UART、SPI、I2C等外设通常以中断或DMA方式收发数据,而应用任务却运行在FreeRTOS调度器之下。两者之间如果直接共享缓冲区,很容易出现“中断写入时任务正在读取”的竞态,或者任务轮询等待导致CPU空转。FreeRTOS队列正是解决这一矛盾的桥梁:它把外设产生的数据以“值拷贝”的方式安全地交给任务,同时用阻塞机制替代忙等,让低优先级任务在无数据时主动让出CPU。
队列的本质:带阻塞能力的环形缓冲区
队列(Queue)是FreeRTOS中最基础的任务间通信对象。创建时需指定两个关键参数:队列长度(最多可存放多少个条目)和每个条目的大小(字节数)。内核会一次性分配一块连续内存,按“环形缓冲区 + 读写索引”的方式管理。发送方调用 xQueueSend() 把数据拷贝进队尾,接收方调用 xQueueReceive() 从队首拷贝出来。注意是值拷贝而非指针传递,因此发送方局部变量出栈后数据依然有效,这对中断服务程序尤其重要。
队列的阻塞机制体现在两个超时参数上。发送时若队列已满,任务可阻塞等待其他任务腾出空间;接收时若队列为空,任务可阻塞等待新数据到来。超时时间设为 portMAX_DELAY 表示无限等待,设为0表示立即返回。阻塞期间任务进入阻塞态,调度器会切换到其他就绪任务,CPU不会空转。这正是队列相比全局变量加轮询的核心优势。
在RA上创建UART接收队列
假设使用RA6M5的SCI0通道,波特率115200,8N1,接收采用中断方式。首先定义一个队列句柄,并在系统初始化阶段创建队列:
#define UART_RX_QUEUE_LEN 64
#define UART_RX_ITEM_SIZE 1
QueueHandle_t g_uart_rx_queue;
void uart_queue_init(void)
{
g_uart_rx_queue = xQueueCreate(UART_RX_QUEUE_LEN, UART_RX_ITEM_SIZE);
configASSERT(g_uart_rx_queue != NULL);
}
这里队列长度取64,条目大小取1字节,意味着最多缓存64个接收字节。如果上位机可能连续发送一帧上百字节的数据,64可能不够,需要根据最大帧长和任务处理速度来估算。一个实用的估算方法是:队列长度 ≥ 最大帧长 + 任务单次处理期间可能到达的字节数。例如最大帧长128字节,任务处理一帧耗时2ms,115200波特率下2ms约可到达23字节,则队列长度至少151,取160较稳妥。
中断服务程序中的发送
RA的SCI UART接收中断由FSP生成,用户可在回调函数中处理。关键点是必须使用 xQueueSendFromISR(),并且正确处理 pxHigherPriorityTaskWoken 参数:
void uart_rx_callback(uart_callback_args_t *p_args)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
uint8_t rx_byte;
if (p_args->event == UART_EVENT_RX_CHAR) {
rx_byte = (uint8_t)p_args->data;
xQueueSendFromISR(g_uart_rx_queue,
&rx_byte,
&xHigherPriorityTaskWoken);
}
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
如果队列已满,xQueueSendFromISR() 会直接返回 errQUEUE_FULL,该字节被丢弃。在调试阶段可以统计丢弃次数,若持续非零,说明队列长度不足或任务处理太慢,需要调整。注意ISR中不能使用带阻塞的 xQueueSend(),否则会触发断言。
任务侧的阻塞接收与帧解析
解析任务以阻塞方式逐字节取出数据,并按照协议组装帧。下面示例假设帧格式为:帧头0xAA、长度字节、载荷、校验和。任务在收到帧头前一直阻塞,收到后按长度继续读取:
void vUartParseTask(void *pvParameters)
{
uint8_t byte;
uint8_t frame[128];
uint8_t idx = 0;
uint8_t expect_len = 0;
for (;;) {
if (xQueueReceive(g_uart_rx_queue, &byte, portMAX_DELAY) == pdTRUE) {
if (idx == 0) {
if (byte == 0xAA) { frame[idx++] = byte; }
} else if (idx == 1) {
expect_len = byte;
frame[idx++] = byte;
} else {
frame[idx++] = byte;
if (idx >= expect_len + 3) {
if (checksum_ok(frame, idx)) {
process_frame(frame, idx);
}
idx = 0;
}
}
}
}
}
这里 portMAX_DELAY 表示任务在队列空时无限期阻塞,直到ISR写入数据将其唤醒。如果希望任务周期性做其他事情,可以把超时改为具体tick数,例如 pdMS_TO_TICKS(100),超时后执行心跳或状态检查。
参数选择与常见问题
队列长度和条目大小一旦创建便不可更改,因此初始化前要评估清楚。条目大小若设为结构体,需确保结构体本身不含指针指向临时内存。另一个常见问题是优先级反转:如果解析任务优先级低于其他任务,ISR唤醒后可能无法立即运行,导致队列积压。此时可适当提高解析任务优先级,或使用 xQueueSendFromISR 返回的唤醒标志触发一次上下文切换。
uxQueueMessagesWaiting() 在任务中打印当前队列深度,观察是否存在持续接近满的情况。若长期接近满,说明消费速度跟不上生产速度,应优化解析逻辑或增大队列。
xQueueSend()(非FromISR版本),也不要在ISR中传入阻塞超时。RA的FSP回调运行在中断上下文,违反此规则会导致系统断言或不可预测行为。
总结来看,队列在RA外设通信中的价值在于三点:用值拷贝消除共享缓冲区竞态,用阻塞机制替代轮询降低CPU占用,用FromISR接口保证中断安全。掌握创建、发送、接收和超时这四个环节,就能把UART接收数据可靠地交给任务处理。