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;
}