上一节学习了FreeRTOS队列的基本概念,并通过xQueueCreate()创建了一个队列。
这一节开始真正使用队列,通过xQueueSend()和xQueueReceive(),让两个任务之间完成数据传递。
这里使用一个简单例子:任务1每隔1秒产生一个数据并发送到队列,任务2负责从队列中读取数据并通过串口打印出来。
一、发送数据xQueueSend()
向队列发送数据可以使用:
BaseType_t xQueueSend( QueueHandle_t xQueue, const void *pvItemToQueue, TickType_t xTicksToWait );第一个参数:
xQueue表示要向哪个队列发送数据,也就是上一节创建得到的队列句柄。
第二个参数:
pvItemToQueue表示要发送数据的地址。
第三个参数:
xTicksToWait表示队列已满时,最多等待多少个Tick。
例如:
xQueueSend( DataQueue, &data, pdMS_TO_TICKS(100) );表示向DataQueue发送data,如果队列已经满了,最多等待100ms。
二、接收数据xQueueReceive()
从队列读取数据使用:
BaseType_t xQueueReceive( QueueHandle_t xQueue, void *pvBuffer, TickType_t xTicksToWait );第一个参数:
xQueue表示从哪个队列读取数据。
第二个参数:
pvBuffer表示接收数据的变量地址。
第三个参数:
xTicksToWait表示队列为空时,最多等待多少个Tick。
例如:
xQueueReceive( DataQueue, &ReceiveData, portMAX_DELAY );如果队列中没有数据,当前任务就会进入阻塞态。
直到队列中出现数据后,任务才会重新进入就绪态。
三、创建队列
首先定义队列句柄:
QueueHandle_t DataQueue;然后在main()中创建:
DataQueue = xQueueCreate( 5, sizeof(uint32_t) );这里表示创建一个最多保存:
5个uint32_t数据的队列。
队列创建成功以后,再创建发送任务和接收任务:
if(DataQueue != NULL) { xTaskCreate( SendTask, "SendTask", 128, NULL, 2, NULL ); xTaskCreate( ReceiveTask, "ReceiveTask", 128, NULL, 1, NULL ); vTaskStartScheduler(); }四、编写发送任务
任务1负责产生数据:
void SendTask(void *pvParameters) { uint32_t data = 0; while(1) { data++; if(xQueueSend( DataQueue, &data, pdMS_TO_TICKS(100) ) == pdPASS) { printf("Send: %lu\r\n", (unsigned long)data); } vTaskDelay(pdMS_TO_TICKS(1000)); } }首先定义:
uint32_t data = 0;每次循环:
data++;然后通过:
xQueueSend();将数据发送到队列。
如果返回:
pdPASS说明数据成功进入队列。
如果等待时间结束以后队列仍然没有空间,则发送失败。
五、编写接收任务
任务2负责读取数据:
void ReceiveTask(void *pvParameters) { uint32_t ReceiveData; while(1) { if(xQueueReceive( DataQueue, &ReceiveData, portMAX_DELAY ) == pdPASS) { printf("Receive: %lu\r\n", (unsigned long)ReceiveData); } } }这里使用:
portMAX_DELAY表示如果队列中没有数据,就一直等待。
因此刚开始运行时,如果队列为空:
ReceiveTask ↓ xQueueReceive() ↓ 没有数据 ↓ 进入阻塞态此时它不会一直循环查询队列,也不会白白占用CPU。
六、数据到来以后发生什么
当SendTask执行:
xQueueSend(DataQueue, &data, ...);以后,数据被复制到队列中。
此时正在等待队列数据的:
ReceiveTask就具备了继续运行的条件。
状态变化可以理解为:
ReceiveTask 阻塞态 ↓ 队列收到数据 ↓ 就绪态 ↓ 等待调度器运行随后执行:
xQueueReceive();把队列中的数据复制到:
ReceiveData中。
七、程序运行现象
程序运行以后,可以通过串口看到类似:
Send: 1 Receive: 1 Send: 2 Receive: 2 Send: 3 Receive: 3 Send: 4 Receive: 4任务1只负责产生数据。
任务2只负责处理数据。
两个任务之间通过:
SendTask ↓ DataQueue ↓ ReceiveTask完成数据传递。
这就是FreeRTOS中非常典型的任务间通信方式。
八、等待时间怎么设置
队列函数最后一个参数非常重要。
例如:
xQueueReceive( DataQueue, &ReceiveData, 0 );如果队列为空:
立即返回不会等待。
如果写:
pdMS_TO_TICKS(100)表示最多等待100ms。
如果写:
portMAX_DELAY在相关FreeRTOS配置允许的情况下,可以让任务持续阻塞等待队列数据。
实际程序中,如果任务本身就是专门负责等待某类数据,使用阻塞等待通常比不断循环查询:
while(1) { xQueueReceive(..., 0); }更加合理。
九、需要注意的问题
首先,发送的数据类型必须和创建队列时设置的元素大小一致。
例如创建:
xQueueCreate( 5, sizeof(uint32_t) );那么这个队列就是按照uint32_t大小复制每一条数据。
其次,xQueueReceive()成功读取数据后,该条数据会从队列中移除。
所以队列中的:
10 20 30读取一次后:
20 30原来的10已经离开队列。
最后,不要在中断服务函数中直接使用普通的xQueueSend()和xQueueReceive()。中断中需要使用带有FromISR后缀的接口,这部分后面单独学习。
十、总结
这一节学习了FreeRTOS队列最基本的发送和接收操作。
xQueueSend()负责把数据发送到队列,xQueueReceive()负责从队列读取数据。当队列满或者队列为空时,还可以通过等待时间让当前任务进入阻塞态,而不是一直占用CPU查询。
通过队列,可以把数据产生和数据处理拆分成不同任务:
发送任务 → 队列 → 接收任务这样不同任务之间既能够交换数据,又不需要直接操作同一个变量。