第11个程序
1.队列的理论知识
队列是freertos系统实现任务之间,任务和中断之间通信的一种方式。
虽然全局变量也可以实现任务之间的通信,但是其存在一些隐患问题:数据无保护,多个任务并行时,数据容易被破坏
队列:

-
遵循FIFO(先写进去的先读出来),以上图为例,任务A和B进行写入工作,右边为头部,任务C和D进行读操作
-
一般来说写数据,要从尾部写入,但是也可以从头部写入,注意:如果从头部写入,并不会覆盖头部数据,这个队列是一个环形的缓冲区,从头部写入,那么原来的数据就往后移位。
-
对于写任务来说,如果队列已经满了,那么这些任务就需要放入一个链表中进行等待
-
同理对于读任务,如果队列里边没数据,那么这些任务也需要进入一个链表等待有数据再读。
这个是创建队列的函数
QueueHandle_t xQueueGenericCreate( const UBaseType_t uxQueueLength, const UBaseType_t uxItemSize, const uint8_t ucQueueType )
看内部实现可以发现
Queue_t * pxNewQueue = NULL;
最终创建的队列是一个结构体:Queue_t。
然后看Queue_t
typedef struct QueueDefinition /* The old naming convention is used to prevent breaking kernel aware debuggers. */ { int8_t * pcHead; /*< Points to the beginning of the queue storage area. */ int8_t * pcWriteTo; /*< Points to the free next place in the storage area. */ union { QueuePointers_t xQueue; /*< Data required exclusively when this structure is used as a queue. */ SemaphoreData_t xSemaphore; /*< Data required exclusively when this structure is used as a semaphore. */ } u; List_t xTasksWaitingToSend; /*< List of tasks that are blocked waiting to post onto this queue. Stored in priority order. */ List_t xTasksWaitingToReceive; /*< List of tasks that are blocked waiting to read from this queue. Stored in priority order. */ volatile UBaseType_t uxMessagesWaiting; /*< The number of items currently in the queue. */ UBaseType_t uxLength; /*< The length of the queue defined as the number of items it will hold, not the number of bytes. */ UBaseType_t uxItemSize; /*< The size of each items that the queue will hold. */ volatile int8_t cRxLock; /*< Stores the number of items received from the queue (removed from the queue) while the queue was locked. Set to queueUNLOCKED when the queue is not locked. */ volatile int8_t cTxLock; /*< Stores the number of items transmitted to the queue (added to the queue) while the queue was locked. Set to queueUNLOCKED when the queue is not locked. */ #if ( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) ) uint8_t ucStaticallyAllocated; /*< Set to pdTRUE if the memory used by the queue was statically allocated to ensure no attempt is made to free the memory. */ #endif #if ( configUSE_QUEUE_SETS == 1 ) struct QueueDefinition * pxQueueSetContainer; #endif #if ( configUSE_TRACE_FACILITY == 1 ) UBaseType_t uxQueueNumber; uint8_t ucQueueType; #endif } xQUEUE; /* The old xQUEUE name is maintained above then typedefed to the new Queue_t * name below to enable the use of older kernel aware debuggers. */ typedef xQUEUE Queue_t; 这个是结构体具体的内容,同时我们注意到,创建队列的函数,返回值是一个handle
typedef struct QueueDefinition * QueueHandle_t;
其实这个handle就是一个指针,指向所创建的队列,和前面创建任务类似。
然后这个队列里面有很多内容,主要包含以下几个方面
int8_t * pcHead; //队列的头部指针 int8_t * pcWriteTo; //队列所需要写的下一个队列项的指针
union { QueuePointers_t xQueue; /*< Data required exclusively when this structure is used as a queue. */ SemaphoreData_t xSemaphore; /*< Data required exclusively when this structure is used as a semaphore. */ } u; 这个union我不太懂,但是通过后面的注释,因为我们是使用的是队列,也就是只用到了这个
QueuePointers_t xQueue;
然后看前面这个结构体
typedef struct QueuePointers { int8_t * pcTail; /*< Points to the byte at the end of the queue storage area. Once more byte is allocated than necessary to store the queue items, this is used as a marker. */ int8_t * pcReadFrom; /*< Points to the last place that a queued item was read from when the structure is used as a queue. */ } QueuePointers_t; 先看第二个指针,他是在读数据的时候所使用的,他指向的是上一次读取队列项的位置。
List_t xTasksWaitingToSend; List_t xTasksWaitingToReceive;这个就是前面所说的,如果队列满了写任务需要等待,就放在这个链表里,读任务同理
还有就是需要一个buf,来存放这个队列中所存储的具体的数据,这个数据是以item为基本单元,一个item字节大小不确定,通过人为给定。
主要就是有这几个部分:
-
队列的头部指针
-
队列写的时候所需要的一个指针
-
队列读的时候所需要的一个指针
-
buf:一块分配的内存,来存放这个队列的具体数据
-
链表:读任务或者写任务等待时候,也就是blocked状态时所放的位置。
-

比如这个是这个队列的buf,一共可以存放6个item,那么这个头部指针,他指向的是头部,他是不变的,刚开始没数据的时候,pcWriteTo也在这个位置,然后我们写入一个数据

头部指针我们就不看了,我们在首部写入了一个1,也就是第一个写的数据,然后pcwriteto=pcwriteto+item_size,指向下一个要写的位置,以此同理
假如写一个123,数字越大写的时间就越晚

那么我们看一下读操作,pcReadFrom,他指向的是上一次读的位置,我们假如上一个这个队列写满了数据,那么他开始读,也就是读完最后一个,他所在的位置,其实就是我画的位置,因为FIFO,最后所在的位置,应该是最后写的一个位置,也就是我画的这个位置,那么再次来到读,首先他更新一下值
pcReadFrom=pcReadFrom+item_size,因为这是一个环形数据缓冲区,所以这个pcReadFrom,他就会到pchead的位置去,来进行读1,这个1就是最先写的一个数据,符合FIFO。那么这个写数据的方法其实就是往队列尾部写数据。
那么假如说我写完123,之后,4我想写在头部,对于这个队列来说,最左边是头部,但是如果我用头部写入的方式,写到最左边的话,那就会覆盖“1”这个数据,正常是没有覆盖功能的,那其实这个时候,他会将4这个数据,写到最右边,同时pcReadFrom=pcReadFrom-item_size,

因为pcReadFrom=pcReadFrom+item_size,每次读的时候,都会执行这个,所以他其实会先读出4.也就说这个FIFO,先入先出,对于正常尾部插入,是可以说时间上先写入的先读出,但是如果是头部插入,虽然从时间他是最晚插入的,但是读的时候,他始终都是先读出头部。
队列先进先出指的是头部先出
对于这些读队列/写队列,都有一个共同参数,堵塞等待时间
/* 等同于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 ); /* * 往队列头部写入数据,如果没有空间,阻塞时间为xTicksToWait */ BaseType_t xQueueSendToFront( QueueHandle_t xQueue, const void *pvItemToQueue, TickType_t xTicksToWait ) BaseType_t xQueueReceive( QueueHandle_t xQueue, void * const pvBuffer, TickType_t xTicksToWait );
xTicksToWait 一共可以给三个参数,
-
0,比如对于写队列,如果数据满了,那就直接返回那个写队列的函数,就是这一行写队列代码执行完了,然后执行那个任务的下一行代码
-
0~portMAX_DELAY,就是这个任务他会进入堵塞状态,然后等待一个给定的时间,如果时间到了队列仍然是满的,他再返回函数,然后执行那个任务的下一行代码
-
portMAX_DELAY,这个是死等,要一直等到,有机会写入队列,不写入数据不结束堵塞状态。
读同理
同时可能会有多个任务在写入队列,那么假如他们都进入了堵塞状态,当可以写入时,谁先写入,按照以下原则
-
优先级高的先写入
-
如果优先级相同,等待时间长的先写入。
-
2.队列的使用实例
2.1使用队列实现同步
在昨天学习中,是通过全局变量来实现同步,那么今天用队列来实现,还是任务1,任务2两个函数,任务1写队列,任务2读队列,在任务1还没有写入队列的时候,也就是队列是空的,那么任务2他就会进入堵塞状态,这样不会占用cpu的时间,这个时候来看一下,任务1同样是执行一个简单的计数,需要多久能执行完。
代码:
static int sum = 0;
static int flagcalend = 0;
static int flaguartuesd = 0;
QueueHandle_t xQueuehandle;
void TaskFunction_1(void *param)
{
volatile int i = 0;
while (1)
{
for (i = 0; i < 10000000; i++)
{
sum++;
}
xQueueSend(xQueuehandle, &sum, portMAX_DELAY);
sum=1;
// flagcalend = 1;
// printf("1");
// vTaskDelete(NULL);
}
}
void TaskFunction_2(void *param)
{
int val;
while (1)
{
flagcalend=0;
xQueueReceive(xQueuehandle, &val, portMAX_DELAY);
flagcalend=1;
printf("%d\r\n", val);
}
}
void TaskGenricFunction(void *param)
{
while (1)
{
if (flaguartuesd == 0)
{
flaguartuesd = 1;
printf("%s\r\n", (char *)param);
flaguartuesd = 0;
vTaskDelay(1);
}
}
}
int main(void)
{
TaskHandle_t xHandleTask1; // 任务1的句柄
#ifdef DEBUG
debug();
#endif
prvSetupHardware();
printf("hello,world\r\n");
xQueuehandle = xQueueCreate(2, sizeof(int));
if (xQueuehandle == NULL)
{
printf("create queue failed\r\n");
}
xTaskCreate(TaskFunction_1, "task1", 100, NULL, 1, &xHandleTask1);
xTaskCreate(TaskFunction_2, "task2", 100, NULL, 1, NULL);
// xTaskCreate(TaskGenricFunction, "task4", 100, "task4 is running", 1, NULL);
// xTaskCreate(TaskGenricFunction, "task5", 100, "task5 is running", 1, NULL);
/* Start the scheduler. */
vTaskStartScheduler();
/* Will only get here if there was not enough heap space to create the
idle task. */
return 0;
}
可以看到,任务2堵塞等待的时间大概为2s,也就是任务1计数记了2秒,和只运行任务1结果一样,说明任务2没有占用cpu资源,而且我们读队列的时候,是根据sum的地址将sum的值传了进去,我们后面将sum值改变了,但是val,并没有变,这个其实就相当于直接赋值,还有第二种方式是根据地址赋值,那如果sum改了,队列中的值也会改。
2.2队列实现互斥
同样也是昨天的例子,任务4和任务5抢占一个串口,那么我们可以实现一个串口锁函数,使得只能有一个任务在使用,也就是互斥。
他可以通过队列来实现,我们可以随便创建一个队列,并给他写入一个数据,当我们比如任务4,需要打印时,我们调用锁串口函数;内部其实就是读队列,然后占用串口打印,在这个打印的过程中,这个队列已经没数据了,如果再来一个任务,他使用串口前也要调用这个函数,因为队列是空的,所以他需要进入堵塞状态。然后上一个打印完之后,再把队列写入一个数据,也可以叫解锁,那么那个堵塞状态的任务就会回到ready状态。
主要的代码:
QueueHandle_t xQueueUARThandle;
int InitiUARTLock(void)
{
int val ;
xQueueUARThandle = xQueueCreate(1, sizeof(int));
if (xQueueUARThandle == NULL)
{
printf("create UART lock queue failed\r\n");
return -1;// 创建队列失败
}
xQueueSend(xQueueUARThandle, &val, portMAX_DELAY);// 初始化锁,发送一个数据表示锁为空闲
return 0;
}
void UART_Lock(void)
{
int val;
// 获取锁,接收一个数据表示锁被占用
xQueueReceive(xQueueUARThandle, &val, portMAX_DELAY);
}
void UART_Unlock(void)
{
int val ;
// 释放锁,发送一个数据表示锁为空闲
xQueueSend(xQueueUARThandle, &val, portMAX_DELAY);
}
void TaskGenricFunction(void *param)
{
while (1)
{
UART_Lock();// 获取UART锁
printf("%s\r\n", (char *)param);
UART_Unlock();// 释放UART锁
vTaskDelay(1);//必不可少
}
}
注意,这个延迟还是不能丢,如果没有这个延迟,那么就会出现只有5打印的情况就没有4打印,解释和之前那个差不多,因为计算5用完了解锁之后,此时4返回了ready状态,但由于时间片轮转,5还在running,又再一次关锁了

可以看到正确的实现了互斥。
2.3队列的其他使用
这个队列还可以有以下两种用途:
-
分辨数据源
在向队列中写入数据时,除了一个单个数字,也可以写入一个结构体
struct {
int id;
int data;
}
将结构体写入队列,然后读取时也是读取队列。
在这看书时,突然发现书上定义结构体的时候,没有结构体名,我查阅了一下资料,发现确实可以不加名,这个被称为匿名结构体。但是有两个要点
-
如果就是struc{
......
}val,
就必须直接定义一个变量,而且以后没办法用struct来定义新变量
-
也可以通过typedf来配合使用
typedf struct{
}val,
那么这个结构体类型就被重命名为了val,我们就可以用val来定义新变量
-
-
传输大块数据
如果数据量很大的时候,我们再一个个的传入队列,再一个个读出队列,会显得效率很低。因此我们可以传入地址,队列中的数据当然可以直接传数据,但是对于很多数据的时候,我们可以传入地址
比如 char【100】;有这么多数据,我们可以将这个地址传进去
然后在读的时候,我们就读出来了这部分数据的地址,然后就直接来访问这部分数据,效率更高,但是这个缺点就是,如果我们将数据改了,我们访问到的数也会更改,对比前面实验,前面就算传入之后数据改了,队列中的数据不变,读出来的也不变。需要注意这一点。
3.队列集和邮箱(queue sets)
第12个程序
吐槽一下:韦东山老师至今还没有将队列集的内容放在pdf中,所以只能按照视频的思路,简单学习一下。
队列集主要是为了应对多个队列的问题,比如我们有多个输入设备,mouse、touch_screen、keyboard,每一个都对应一个队列,而这些队列,因为都是作为输入数据,我们需要同时检测,因此就可以用队列集。
队列集的长度=队列1的长度+队列2的长度+...(包含几个队列就加几个)
然后我们需要通过一个实验来体会一下,一共有两个任务和两个队列,这两个任务分别向两个队列中写数据,然后创建一个队列集,让这两个队列的handle和队列集产生关联,再来一个任务,来读取队列集中的数据,
代码:
QueueHandle_t xQueueHandle1;
QueueHandle_t xQueueHandle2;
QueueSetHandle_t xQueueSethandle;
void TaskFunction_1(void *param)
{
int i=0;
while (1)
{
xQueueSend(xQueueHandle1, &i, portMAX_DELAY);
i++;
vTaskDelay(10);
}
}
void TaskFunction_2(void *param)
{
int i=0;
while (1)
{
xQueueSend(xQueueHandle2, &i, portMAX_DELAY);
i--;
vTaskDelay(20);
}
}
void TaskFunction_3(void *param)
{
QueueSetMemberHandle_t xQueueSetMemberhandle;
int val=0;
while (1)
{
/*1.读队列集,看哪个队列集有数据,对于队列集同样,如果两个队列都没数据,那么这个任务会进入堵塞状态*/
xQueueSetMemberhandle=xQueueSelectFromSet(xQueueSethandle,portMAX_DELAY);//返回的handle是有数据的那一个队列
/*2.读数据*/
xQueueReceive(xQueueSetMemberhandle,&val,0);
printf("val=%d\r\n",val);
}
}
int main(void)
{
TaskHandle_t xHandleTask1; // 任务1的句柄
#ifdef DEBUG
debug();
#endif
prvSetupHardware();
printf("hello,world\r\n");
/*1.创建两个队列*/
xQueueHandle1=xQueueCreate(2,sizeof(int));
if(xQueueHandle1==NULL)
{
printf("create queue1 failed\r\n");
}
xQueueHandle2=xQueueCreate(2,sizeof(int));
if(xQueueHandle2==NULL)
{
printf("create queue2 failed\r\n");
}
/*2.创建队列集*/
xQueueSethandle=xQueueCreateSet(4);
if(xQueueSethandle==NULL)
{
printf("create queueset failed\r\n");
}
/*3.将两个队列的handle和队列集建立联系*/
xQueueAddToSet(xQueueHandle1,xQueueSethandle);
xQueueAddToSet(xQueueHandle2,xQueueSethandle);
/*4.创建三个任务*/
xTaskCreate(TaskFunction_1, "task1", 100, NULL, 1, &xHandleTask1);
xTaskCreate(TaskFunction_2, "task2", 100, NULL, 1, NULL);
xTaskCreate(TaskFunction_3, "task3", 100, NULL, 1, NULL);
/* Start the scheduler. */
vTaskStartScheduler();
/* Will only get here if there was not enough heap space to create the
idle task. */
return 0;
}
现象:

注意:xQueueSend,在向队列中写数据时,同时他也会向队列集中写入数据,在队列中写一次就也在队列集中写一次。
读数据同理
邮箱:freertos中的邮箱也可以叫橱窗
-
它是一个队列,队列长度只有1
-
写邮箱:新数据覆盖旧数据,在任务中使用xQueueOverwrite() ,在中断中使用xQueueOverwriteFromISR() 。既然是覆盖,那么无论邮箱中是否有数据,这些函数总能成功写入数据。
-
读邮箱:读数据时,数据不会被移除;在任务中使用xQueuePeek() ,在中断中使用xQueuePeekFromISR() 。这意味着,第一次调用时会因为无数据而阻塞,一旦曾经写入数据,以后读邮箱时总能成功。

1163

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



