Freertos学习记录day8-队列(queue)的使用

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

    }

    将结构体写入队列,然后读取时也是读取队列。

    在这看书时,突然发现书上定义结构体的时候,没有结构体名,我查阅了一下资料,发现确实可以不加名,这个被称为匿名结构体。但是有两个要点

    1. 如果就是struc{

    ......

    }val,

    就必须直接定义一个变量,而且以后没办法用struct来定义新变量

    1. 也可以通过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() 。这意味着,第一次调用时会因为无数据而阻塞,一旦曾经写入数据,以后读邮箱时总能成功。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值