1、队列的特性
队列可以理解为一个传送带,一个流水线。
队列可以包含若干个数据:队列中有若干项,这被称为"长度"(length)
每个数据大小固定
创建队列时就要指定长度、数据大小
数据的操作采用先进先出的方法(FIFO,First In First Out):写数据时放到尾部,读数据时从头部读,把数据写在队列头部并不会覆盖原来头部的数据,因为队列中的数据使用环形缓冲区管理数据,把数据放到头部时,会先移动头部位置,并不会覆盖原来数据。
2、队列的函数
2.1 创建
//函数原型
//队列长度,每一项的大小
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize );
2.2 写队列
可以把数据写到队列头部,也可以写到尾部,这些函数有两个版本:在任务中使用、在ISR中使用。
/* 等同于xQueueSendToBack
* 往队列尾部写入数据,如果没有空间,阻塞时间为xTicksToWait
*/
BaseType_t xQueueSend(QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);
/*
* 往队列尾部写入数据,如果没有空间,阻塞时间为xTicksToWait
*/
BaseType_t xQueueSendToBack(QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);
/*
* 往队列尾部写入数据,此函数可以在中断函数中使用,不可阻塞
*/
BaseType_t xQueueSendToBackFromISR(QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);
/*
* 往队列头部写入数据,如果没有空间,阻塞时间为xTicksToWait
*/
BaseType_t xQueueSendToFront(QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);
/*
* 往队列头部写入数据,此函数可以在中断函数中使用,不可阻塞
*/
BaseType_t xQueueSendToFrontFromISR(QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);
参数
|
说明
|
xQueue
|
队列句柄,要写哪个队列
|
pvItemToQueue
|
数据指针,这个数据的值会被复制进队列,
复制多大的数据?在创建队列时已经指定了数据大小
|
xTicksToWait
|
如果队列满则无法写入新数据,可以让任务进入阻塞状态,
xTicksToWait
表示阻塞的最大时间
(Tick Count)
。
如果被设为
0
,无法写入数据时函数会立刻返回;
如果被设为
portMAX_DELAY
,则会一直阻塞直到有空间可写
|
返回值
|
pdPASS
:数据成功写入了队列
errQUEUE_FULL
:写入失败,因为队列满了。
|
2.3 读队列
使用 xQueueReceive() 函数读队列,读到一个数据后,队列中该数据会被移除。这个函数有两个版 本:在任务中使用、在ISR中使用。
BaseType_t xQueueReceive( QueueHandle_t xQueue,
void * const pvBuffer,
TickType_t xTicksToWait
);
BaseType_t xQueueReceiveFromISR(
QueueHandle_t xQueue,
void *pvBuffer,
BaseType_t *pxTaskWoken
);
参数
|
说明
|
xQueue
|
队列句柄,要写哪个队列
|
pvBuffer
|
bufer
指针,队列的数据会被复制到这个
buffer
复制多大的数据?在创建队列时已经指定了数据大小
|
xTicksToWait
|
如果队列空则无法读出数据,可以让任务进入阻塞状态,
xTicksToWait
表示阻塞的最大时间
(Tick Count)
。
如果被设为
0
,无法读出数据时函数会立刻返回;
如果被设为
portMAX_DELAY
,则会一直阻塞直到有数据可写
|
返回值
|
pdPASS
:从队列读出数据入
errQUEUE_EMPTY
:读取失败,因为队列空了。
|
2.4 删除
删除队列的函数为 vQueueDelete() ,只能删除使用动态方法创建的队列,它会释放内存。
void vQueueDelete( QueueHandle_t xQueue );
2.5 查询
可以查询队列中有多少个数据、有多少空余空间。
/*
* 返回队列中可用数据的个数
*/
UBaseType_t uxQueueMessagesWaiting( const QueueHandle_t xQueue );
/*
* 返回队列中可用空间的个数
*/
UBaseType_t uxQueueSpacesAvailable( const QueueHandle_t xQueue );
2.6 复位
队列刚被创建时,里面没有数据,使用过程中可以调用 xQueueReset() 把队列恢复为初始状态。
/* pxQueue : 复位哪个队列;
* 返回值: pdPASS(必定成功)
*/
BaseType_t xQueueReset( QueueHandle_t pxQueue);
2.7 覆盖
当队列长度为1时,可以使用 xQueueOverwrite() 或 xQueueOverwriteFromISR() 来覆盖数据。 注意,队列长度必须为1。当队列满时,这些函数会覆盖里面的数据,这也意味着这些函数不会被阻塞。
/* 覆盖队列
* xQueue: 写哪个队列
* pvItemToQueue: 数据地址
* 返回值: pdTRUE表示成功, pdFALSE表示失败
*/
BaseType_t xQueueOverwrite(QueueHandle_t xQueue,
const void * pvItemToQueue
);
BaseType_t xQueueOverwriteFromISR(QueueHandle_t xQueue,
const void * pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);
2.8 偷看
如果想让队列中的数据供多方读取,也就是说读取时不要移除数据,要留给后来人。那么可以使用"窥视",也就是 xQueuePeek() 或 xQueuePeekFromISR() 。这些函数会从队列中复制出数据,但是不移除数据。这也意味着,如果队列中没有数据,那么"偷看"时会导致阻塞;一旦队列中有数据,以后每次"偷看"都会成功。
/* 偷看队列
* xQueue: 偷看哪个队列
* pvItemToQueue: 数据地址, 用来保存复制出来的数据
* xTicksToWait: 没有数据的话阻塞一会
* 返回值: pdTRUE表示成功, pdFALSE表示失败
*/
BaseType_t xQueuePeek(QueueHandle_t xQueue,
void * const pvBuffer,
TickType_t xTicksToWait
);
BaseType_t xQueuePeekFromISR(QueueHandle_t xQueue,
void *pvBuffer,
);
3、示例代码
3.1 同步
static int sum = 0;
static volatile int flagCalcEnd = 0;
static QueueHandle_t xQueueCalcHandle;
void Task1Function( void * param)
{
volatile int i = 0; //使用volatile修饰,让系统不要去优化这个变量
while(1){
for(i = 0; i < 10000000; i++){
sum++;
}
xQueueSend(xQueueCalcHandle, &sum, portMAX_DELAY);
sum = 1;
}
}
void Task2Function( void * param)
{
int val;
while(1){
flagCalcEnd = 0;
xQueueReceive(xQueueCalcHandle, &val, portMAX_DELAY);
flagCalcEnd = 1;
printf("sum = %d\r\n", val);
}
}
//main函数中
printf("hello go\r\n");
xQueueCalcHandle = xQueueCreate(2, sizeof(int));
if(xQueueCalcHandle == NULL){
printf("can not create queue\r\n");
}
xTaskCreate(Task1Function, "Task1", 100, NULL, 1, &xHandleTask1);
xTaskCreate(Task2Function, "Task2", 100, NULL, 1, NULL);
使用队列的方法实现了同步,flagCalcEnd在被拉高的时候,用时2s
3.2 互斥
static QueueHandle_t xQueueUARTHandle;
int InitUARTLock(void)
{
int val;
xQueueUARTHandle = xQueueCreate(1, sizeof(int));
if(xQueueUARTHandle == NULL){
printf("can not create queue\r\n");
return -1;
}
xQueueSend(xQueueUARTHandle, &val, portMAX_DELAY);
return 0;
}
void GetUARTLock(void)
{
int val;
xQueueReceive(xQueueUARTHandle, &val, portMAX_DELAY);
}
void PutUARTLock(void)
{
int val;
xQueueSend(xQueueUARTHandle, &val, portMAX_DELAY);
}
void TaskGenericFunction( void * param)
{
while(1){
GetUARTLock();
printf("%s\r\n", (char *)param);
//在释放锁之前,任务三在等待
PutUARTLock(); //在释放锁时,任务三进入就绪状态,但是此时任务四是运行状态,马上又要上锁,轮不到任务三去执行
vTaskDelay(1); //主动让一下
/*
除了通过vTaskDelay让出CPU资源,还有更合理的函数:
使用taskYIELD(),主动发起—次任务切换
vTaskDelay会让任务阻塞、暂停若干tick,taskYIELD()更合理
可以设置不同的优先级来实现抢占
*/
}
}
//main函数中
InitUARTLock();
xTaskCreate(TaskGenericFunction, "Task3", 100, "Task 3 is running", 1, NULL);
xTaskCreate(TaskGenericFunction, "Task4", 100, "Task 4 is running", 1, NULL);
先初始化一个锁,这个锁用队列来实现,创建完队列,往里面随便写一个数据,队列里面有数据就表示别人可以来读取这个数据。假设某个任务要使用串口,先用GetUARTLock获得串口的锁,去读队列,得到这个队列的数据就表示得到这个串口的使用权,用完串口之后就往队列里随便写一个数据,表示使用完串口了,把这个使用权释放掉。
3.3 队列集
创建两个队列Queue1和Queue2,Task1和Task2分别往这两个队列里写入数据,Task3使用Queue Set来监测这两个队列。队列集的长度是包含的队列长度之和。
static volatile int flagCalcEnd = 0;
static volatile int flagUARTused = 0;
static QueueHandle_t xQueueHandle1;
static QueueHandle_t xQueueHandle2;
static QueueSetHandle_t xQueueSet;
void Task1Function(void * param)
{
int i = 0;
while (1)
{
xQueueSend(xQueueHandle1, &i, portMAX_DELAY);
i++;
vTaskDelay(10);
}
}
void Task2Function(void * param)
{
int i = -1;
while (1)
{
xQueueSend(xQueueHandle2, &i, portMAX_DELAY);
i--;
vTaskDelay(20);
}
}
void Task3Function(void * param)
{
QueueSetMemberHandle_t handle;
int i;
while (1)
{
/* 1. read queue set: which queue has data */
handle = xQueueSelectFromSet(xQueueSet, portMAX_DELAY);
/* 2. read queue */
xQueueReceive(handle, &i, 0);
/* 3. print */
printf("get data : %d\r\n", i);
}
}
//main函数中
TaskHandle_t xHandleTask1;
/* 1. 创建2个queue */
xQueueHandle1 = xQueueCreate(2, sizeof(int));
if (xQueueHandle1 == NULL)
{
printf("can not create queue\r\n");
}
xQueueHandle2 = xQueueCreate(2, sizeof(int));
if (xQueueHandle2 == NULL)
{
printf("can not create queue\r\n");
}
/* 2. 创建queue set */
xQueueSet = xQueueCreateSet(3);
/* 3. 把2个queue添加进queue set */
xQueueAddToSet(xQueueHandle1, xQueueSet);
xQueueAddToSet(xQueueHandle2, xQueueSet);
/* 4. 创建3个任务 */
xTaskCreate(Task1Function, "Task1", 100, NULL, 1, &xHandleTask1);
xTaskCreate(Task2Function, "Task2", 100, NULL, 1, NULL);
xTaskCreate(Task3Function, "Task3", 100, NULL, 1, NULL);
3.4 邮箱(mailboc)
FreeRTOS的邮箱概念跟别的RTOS不一样,这里的邮箱称为"橱窗"也许更恰当:
它是一个队列,队列长度只有1
写邮箱:新数据覆盖旧数据,在任务中使用 xQueueOverwrite() ,在中断中使用 xQueueOverwriteFromISR() 。 既然是覆盖,那么无论邮箱中是否有数据,这些函数总能成功写入数据。
读邮箱:读数据时,数据不会被移除;在任务中使用 xQueuePeek() ,在中断中使用 xQueuePeekFromISR() 。 这意味着,第一次调用时会因为无数据而阻塞,一旦曾经写入数据,以后读邮箱时总能成功。
main函数中创建了队列(队列长度为1)、创建了发送任务、接收任务:
发送任务的优先级为2,它先执行
接收任务的优先级为1
/* 队列句柄, 创建队列时会设置这个变量 */
QueueHandle_t xQueue;
int main( void )
{
prvSetupHardware();
/* 创建队列: 长度为1,数据大小为4字节(存放一个char指针) */
xQueue = xQueueCreate( 1, sizeof(uint32_t) );
if( xQueue != NULL )
{
/* 创建1个任务用于写队列
* 任务函数会连续执行,构造buffer数据,把buffer地址写入队列
* 优先级为2
*/
xTaskCreate( vSenderTask, "Sender", 1000, NULL, 2, NULL );
/* 创建1个任务用于读队列
* 优先级为1
*/
xTaskCreate( vReceiverTask, "Receiver", 1000, NULL, 1, NULL );
/* 启动调度器 */
vTaskStartScheduler();
}
else
{
/* 无法创建队列 */
}
/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
return 0;
}