11、FreeRTOS 队列、队列集,邮箱的使用

文章目录

  • 一、队列的特性
    • 1.1 队列常规操作
    • 1.2 传输数据的两种方法
    • 1.3 队列的阻塞访问
  • 二 队列函数
    • 2.1创建
    • 2.2 复位
    • 2.3 删除
    • 2.4 写队列
    • 2.5 读队列
    • 2.6 查询
    • 2.7 覆盖/偷看
  • 三、示例
    • 3.1示例 队列的基本使用
    • 3.2 示例: 分辨数据源
    • 3.3 示例: 传输大块数据
    • 3.4 : 邮箱(Mailbox)
  • 四、队列集

一、队列的特性

1.1 队列常规操作

队列的简化操如入下图所示,从此图可知:

  • 队列可以包含若干个数据:队列中有若干项,这被称为"长度"(length)
  • 每个数据大小固定
  • 创建队列时就要指定长度、数据大小
  • 数据的操作采用先进先出的方法(FIFO,First In First Out):写数据时放到尾部,读数据时从头部
  • 也可以强制写队列头部:覆盖头部数据

在这里插入图片描述

更详细的操作入下图所示:

在这里插入图片描述

1.2 传输数据的两种方法

使用队列传输数据时有两种方法:

  • 拷贝:把数据、把变量的值复制进队列里
  • 引用:把数据、把变量的地址复制进队列里

reeRTOS使用拷贝值的方法,这更简单:

  • 局部变量的值可以发送到队列中,后续即使函数退出、局部变量被回收,也不会影响队列中的数据
  • 无需分配buffer来保存数据,队列中有buffer
  • 局部变量可以马上再次使用
  • 发送任务、接收任务解耦:接收任务不需要知道这数据是谁的、也不需要发送任务来释放数据
  • 如果数据实在太大,你还是可以使用队列传输它的地址
  • 队列的空间有FreeRTOS内核分配,无需任务操心
  • 对于有内存保护功能的系统,如果队列使用引用方法,也就是使用地址,必须确保双方任务对这个
    地址都有访问权限。使用拷贝方法时,则无此限制:内核有足够的权限,把数据复制进队列、再把
    数据复制出队列。

1.3 队列的阻塞访问

只要知道队列的句柄,谁都可以读、写该队列。任务、ISR都可读、写队列。可以多个任务读写队列。
任务读写队列时,简单地说:如果读写不成功,则阻塞;可以指定超时时间。口语化地说,就是可以定
个闹钟:如果能读写了就马上进入就绪态,否则就阻塞直到超时。

某个任务读队列时,如果队列没有数据,则该任务可以进入阻塞状态:还可以指定阻塞的时间。如果队
列有数据了,则该阻塞的任务会变为就绪态。如果一直都没有数据,则时间到之后它也会进入就绪态。

既然读取队列的任务个数没有限制,那么当多个任务读取空队列时,这些任务都会进入阻塞状态:有多
个任务在等待同一个队列的数据。当队列中有数据时,哪个任务会进入就绪态?

  • 优先级最高的任务
  • 如果大家的优先级相同,那等待时间最久的任务会进入就绪态

跟读队列类似,一个任务要写队列时,如果队列满了,该任务也可以进入阻塞状态:还可以指定阻塞的
时间。如果队列有空间了,则该阻塞的任务会变为就绪态。如果一直都没有空间,则时间到之后它也会
进入就绪态。

既然写队列的任务个数没有限制,那么当多个任务写"满队列"时,这些任务都会进入阻塞状态:有多个
任务在等待同一个队列的空间。当队列中有空间时,哪个任务会进入就绪态

  • 优先级最高的任务
  • 如果大家的优先级相同,那等待时间最久的任务会进入就绪态

二 队列函数

使用队列的流程:创建队列、写队列、读队列、删除队列。

2.1创建

队列的创建有两种方法:动态分配内存、静态分配内存,

前提 configSUPPORT_DYNAMIC_ALLOCATION 置于1

  • 动态分配内存:xQueueCreate,队列的内存在函数内部动态分配

函数原型如下:

QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize );
参数说明
uxQueueLength队列长度,最多能存放多少个数据(item)
uxItemSize每个数据(item)的大小:以字节为单位
返回值非0:成功,返回句柄,以后使用句柄来操作队列
NULL:失败,因为内存不足
  • 静态分配内存:xQueueCreateStatic,队列的内存要事先分配好

前提:configSUPPORT_STATIC_ALLOCATION 置于1

函数原型如下:

QueueHandle_t xQueueCreateStatic(
								UBaseType_t uxQueueLength,
    							UBaseType_t uxItemSize,
                                uint8_t *pucQueueStorageBuffer,
                                StaticQueue_t *pxQueueBuffer
);
参数说明
uxQueueLength队列长度,最多能存放多少个数据(item)
uxItemSize每个数据(item)的大小:以字节为单位
pucQueueStorageBuffer如果uxItemSize非0,pucQueueStorageBuffer必须指向一个
uint8_t数组,
此数组大小至少为"uxQueueLength * uxItemSize"
pxQueueBuffer必须执行一个StaticQueue_t结构体,用来保存队列的数据结构
返回值非0:成功,返回句柄,以后使用句柄来操作队列
NULL:失败,因为pxQueueBuffer为NULL

示例代码

// 示例代码
#define QUEUE_LENGTH 10
#define ITEM_SIZE sizeof( uint32_t )

// xQueueBuffer用来保存队列结构体
StaticQueue_t xQueueBuffer;

// ucQueueStorage 用来保存队列的数据
// 大小为:队列长度 * 数据大小

uint8_t ucQueueStorage[ QUEUE_LENGTH * ITEM_SIZE ];

void vATask( void *pvParameters )
{
QueueHandle_t xQueue1;
// 创建队列: 可以容纳QUEUE_LENGTH个数据,每个数据大小是ITEM_SIZE
xQueue1 = xQueueCreateStatic( QUEUE_LENGTH,
                             ITEM_SIZE,
                             ucQueueStorage,
                             &xQueueBuffer );
}

2.2 复位

队列刚被创建时,里面没有数据;使用过程中可以调用xQueueReset() 把队列恢复为初始状态,此函
数原型为:

/* pxQueue : 复位哪个队列;
* 返回值: pdPASS(必定成功)
*/
BaseType_t xQueueReset( QueueHandle_t pxQueue);

2.3 删除

删除队列的函数为vQueueDelete() ,只能删除使用动态方法创建的队列,它会释放内存。原型如下:

void vQueueDelete( QueueHandle_t xQueue );

2.4 写队列

可以把数据写到队列头部,也可以写到尾部,这些函数有两个版本:在任务中使用、在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.5 读队列

使用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队列句柄,要读哪个队列
pvBufferbufer指针,队列的数据会被复制到这个buffer
复制多大的数据?在创建队列时已经指定了数据大小
xTicksToWait果队列空则无法读出数据,可以让任务进入阻塞状态,
xTicksToWait表示阻塞的最大时间(Tick Count)。
如果被设为0,无法读出数据时函数会立刻返回;
如果被设为portMAX_DELAY,则会一直阻塞直到有数据可写
返回值pdPASS:从队列读出数据入
errQUEUE_EMPTY:读取失败,因为队列空了。

2.6 查询

可以查询队列中有多少个数据、有多少空余空间。函数原型如下:

/*
* 返回队列中可用数据的个数
*/
UBaseType_t uxQueueMessagesWaiting( const QueueHandle_t xQueue );
/*
* 返回队列中可用空间的个数
*/
UBaseType_t uxQueueSpacesAvailable( const QueueHandle_t xQueue );

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

如果想让队列中的数据供多方读取,也就是说读取时不要移除数据,要留给后来人。那么可以使用"窥
视",也就是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.1示例 队列的基本使用

本程序会创建一个队列,然后创建2个发送任务、1个接收任务:
发送任务优先级为1,分别往队列中写入100200
接收任务优先级为2,读队列、打印数值

main函数中创建的队列、创建了发送任务、接收任务,代码如下:

/* 队列句柄, 创建队列时会设置这个变量 */
QueueHandle_t xQueue;
int main( void )
{
    prvSetupHardware();
    /* 创建队列: 长度为5,数据大小为4字节(存放一个整数) */
    xQueue = xQueueCreate( 5, sizeof( int32_t ) );
    if( xQueue != NULL )
    {
        /* 创建2个任务用于写队列, 传入的参数分别是100、200
        * 任务函数会连续执行,向队列发送数值100、200
        * 优先级为1
        */
        xTaskCreate( vSenderTask, "Sender1", 1000, ( void * ) 100, 1, NULL );
        xTaskCreate( vSenderTask, "Sender2", 1000, ( void * ) 200, 1, NULL );
        /* 创建1个任务用于读队列
        * 优先级为2, 高于上面的两个任务
        * 这意味着队列一有数据就会被读走
        */
        xTaskCreate( vReceiverTask, "Receiver", 1000, NULL, 2, NULL );
        /* 启动调度器 */
        vTaskStartScheduler();
    }
    else
    {
    /* 无法创建队列 */
    }
    /* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
    return 0;
}

发送任务的函数中,不断往队列中写入数值,代码如下:

static void vSenderTask( void *pvParameters )
{
int32_t lValueToSend;
BaseType_t xStatus;
/* 我们会使用这个函数创建2个任务
* 这些任务的pvParameters不一样
*/
lValueToSend = ( int32_t ) pvParameters;
/* 无限循环 */
    for( ;; )
    {
    /* 写队列
    * xQueue: 写哪个队列
    * &lValueToSend: 写什么数据? 传入数据的地址, 会从这个地址把数据复制进队列
    * 0: 不阻塞, 如果队列满的话, 写入失败, 立刻返回
    */
    xStatus = xQueueSendToBack( xQueue, &lValueToSend, 0 );
        if( xStatus != pdPASS )
        {
        	printf( "Could not send to the queue.\r\n" );
        }
    }
}

接收任务的函数中,读取队列、判断返回值、打印,代码如下:

static void vReceiverTask( void *pvParameters )
{
/* 读取队列时, 用这个变量来存放数据 */
int32_t lReceivedValue;
BaseType_t xStatus;
const TickType_t xTicksToWait = pdMS_TO_TICKS( 100UL );
/* 无限循环 */
    for( ;; )
    {
        /* 读队列
        * xQueue: 读哪个队列
        * &lReceivedValue: 读到的数据复制到这个地址
        * xTicksToWait: 如果队列为空, 阻塞一会
        */
        xStatus = xQueueReceive( xQueue, &lReceivedValue, xTicksToWait );
        if( xStatus == pdPASS )
        {
        /* 读到了数据 */
        printf( "Received = %d\r\n", lReceivedValue );
        }
        else
        {
        /* 没读到数据 */
        printf( "Could not receive from the queue.\r\n" );
        }
    }
}

程序运行结果如下:

在这里插入图片描述

任务调度情况如下图所示:

在这里插入图片描述

3.2 示例: 分辨数据源

当有多个发送任务,通过同一个队列发出数据,接收任务如何分辨数据来源?数据本身带有"来源"信
息,比如写入队列的数据是一个结构体,结构体中的lDataSouceID用来表示数据来源:

typedef struct {
ID_t eDataID;
int32_t lDataValue;
}Data_t;

不同的发送任务,先构造好结构体,填入自己的eDataID ,再写队列;接收任务读出数据后,根据
eDataID 就可以知道数据来源了,如下图所示:

  • CAN任务发送的数据:eDataID=eMotorSpeed
  • HMI任务发送的数据:eDataID=eSpeedSetPoint

在这里插入图片描述

程序会创建一个队列,然后创建2个发送任务、1个接收任务:

  • 创建的队列,用来发送结构体:数据大小是结构体的大小
  • 发送任务优先级为2,分别往队列中写入自己的结构体,结构体中会标明数据来源
  • 接收任务优先级为1,读队列、根据数据来源打印信息

main函数中创建了队列、创建了发送任务、接收任务,代码如下:

/* 定义2种数据来源(ID) */
typedef enum
{
	eMotorSpeed,
	eSpeedSetPoint
} ID_t;

/* 定义在队列中传输的数据的格式 */
typedef struct {
    ID_t eDataID;
    int32_t lDataValue;
}Data_t;

/* 定义2个结构体 */
static const Data_t xStructsToSend[ 2 ] =
{
	{ eMotorSpeed,    10 }, /* CAN任务发送的数据 */
	{ eSpeedSetPoint, 5 }   /* HMI任务发送的数据 */
};


/* vSenderTask被用来创建2个任务,用于写队列
 * vReceiverTask被用来创建1个任务,用于读队列
 */
static void vSenderTask( void *pvParameters );
static void vReceiverTask( void *pvParameters );

/*-----------------------------------------------------------*/

/* 队列句柄, 创建队列时会设置这个变量 */
QueueHandle_t xQueue;

int main( void )
{
	prvSetupHardware();
	
    /* 创建队列: 长度为5,数据大小为4字节(存放一个整数) */
    xQueue = xQueueCreate( 5, sizeof( Data_t ) );

	if( xQueue != NULL )
	{
		/* 创建2个任务用于写队列, 传入的参数是不同的结构体地址
		 * 任务函数会连续执行,向队列发送结构体
		 * 优先级为2
		 */
		xTaskCreate( vSenderTask, "CAN Task", 1000, ( void * ) &( xStructsToSend[ 0 ] ), 2, NULL );
		xTaskCreate( vSenderTask, "HMI Task", 1000, ( void * ) &( xStructsToSend[ 1 ] ), 2, NULL );

		/* 创建1个任务用于读队列
		 * 优先级为1, 低于上面的两个任务
		 * 这意味着发送任务优先写队列,队列常常是满的状态
		 */
		xTaskCreate( vReceiverTask, "Receiver", 1000, NULL, 1, NULL );

		/* 启动调度器 */
		vTaskStartScheduler();
	}
	else
	{
		/* 无法创建队列 */
	}

	/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
	return 0;
}

发送任务的函数中,不断往队列中写入数值,代码如下:

static void vSenderTask( void *pvParameters )
{
	BaseType_t xStatus;
	const TickType_t xTicksToWait = pdMS_TO_TICKS( 100UL );

	/* 无限循环 */
	for( ;; )
	{
		/* 写队列
		 * xQueue: 写哪个队列
		 * pvParameters: 写什么数据? 传入数据的地址, 会从这个地址把数据复制进队列
		 * xTicksToWait: 如果队列满的话, 阻塞一会
		 */
		xStatus = xQueueSendToBack( xQueue, pvParameters, xTicksToWait );

		if( xStatus != pdPASS )
		{
			printf( "Could not send to the queue.\r\n" );
		}
	}
}

接收任务的函数中,读取队列、判断返回值、打印,代码如下:

static void vReceiverTask( void *pvParameters )
{
	/* 读取队列时, 用这个变量来存放数据 */
	Data_t xReceivedStructure;
	BaseType_t xStatus;

	/* 无限循环 */
	for( ;; )
	{
		/* 读队列
		 * xQueue: 读哪个队列
		 * &xReceivedStructure: 读到的数据复制到这个地址
		 * 0: 没有数据就即刻返回,不阻塞
		 */
		xStatus = xQueueReceive( xQueue, &xReceivedStructure, 0 );

		if( xStatus == pdPASS )
		{
			/* 读到了数据 */
			if( xReceivedStructure.eDataID == eMotorSpeed )
			{
				printf( "From CAN, MotorSpeed = %d\r\n", xReceivedStructure.lDataValue );
			}
			else if( xReceivedStructure.eDataID == eSpeedSetPoint )
			{
				printf( "From HMI, SpeedSetPoint = %d\r\n", xReceivedStructure.lDataValue );
			}
		}
		else
		{
			/* 没读到数据 */
			printf( "Could not receive from the queue.\r\n" );
		}
	}
}

运行结果如下:

在这里插入图片描述

任务调度情况如下图所示:

  • t1:HMI是最后创建的最高优先级任务,它先执行,一下子向队列写入5个数据,把队列都写满了
  • t2:队列已经满了,HMI任务再发起第6次写操作时,进入阻塞状态。这时CAN任务是最高优先级
    的就绪态任务,它开始执行
  • t3:CAN任务发现队列已经满了,进入阻塞状态;接收任务变为最高优先级的就绪态任务,它开始
    运行
  • t4:现在,HMI任务、CAN任务的优先级都比接收任务高,它们都在等待队列有空闲的空间;一旦
    接收任务读出1个数据,会马上被抢占。被谁抢占?谁等待最久?HMI任务!所以在t4时刻,切换
    到HMI任务。
  • t5:HMI任务向队列写入第6个数据,然后再次阻塞,这是CAN任务已经阻塞很久了。接收任务变
    为最高优先级的就绪态任务,开始执行。
  • t6:现在,HMI任务、CAN任务的优先级都比接收任务高,它们都在等待队列有空闲的空间;一旦
    接收任务读出1个数据,会马上被抢占。被谁抢占?谁等待最久?CAN任务!所以在t6时刻,切换
    到CAN任务。
  • t7:CAN任务向队列写入数据,因为仅仅有一个空间供写入,所以它马上再次进入阻塞状态。这时
    HMI任务、CAN任务都在等待空闲空间,只有接收任务可以继续执行。
    在这里插入图片描述

3.3 示例: 传输大块数据

FreeRTOS的队列使用拷贝传输,也就是要传输uint32_t时,把4字节的数据拷贝进队列;要传输一个8
字节的结构体时,把8字节的数据拷贝进队列。

如果要传输1000字节的结构体呢?写队列时拷贝1000字节,读队列时再拷贝1000字节?不建议这么
做,影响效率!

这时候,我们要传输的是这个巨大结构体的地址:把它的地址写入队列,对方从队列得到这个地址,使
用地址去访问那1000字节的数据。

使用地址来间接传输数据时,这些数据放在RAM里,对于这块RAM,要保证这几点:

  • RAM的所有者、操作者,必须清晰明了
    这块内存,就被称为"共享内存"。要确保不能同时修改RAM。比如,在写队列之前只有由发送者修
    改这块RAM,在读队列之后只能由接收者访问这块RAM。

  • RAM要保持可用

    这块RAM应该是全局变量,或者是动态分配的内存。对于动然分配的内存,要确保它不能提前释
    放:要等到接收者用完后再释放。另外,不能是局部变量。

程序会创建一个队列,然后创建1个发送任务、1个接收任务:

  • 创建的队列:长度为1,用来传输"char *"指针
  • 发送任务优先级为1,在字符数组中写好数据后,把它的地址写入队列
  • 接收任务优先级为2,读队列得到"char *"值,把它打印出来

这个程序故意设置接收任务的优先级更高,在它访问数组的过程中,接收任务无法执行、无法写这个数
组。

main 函数中创建了队列、创建了发送任务、接收任务,代码如下

/* 定义一个字符数组 */
static char pcBuffer[100];


/* vSenderTask被用来创建2个任务,用于写队列
 * vReceiverTask被用来创建1个任务,用于读队列
 */
static void vSenderTask( void *pvParameters );
static void vReceiverTask( void *pvParameters );

/*-----------------------------------------------------------*/

/* 队列句柄, 创建队列时会设置这个变量 */
QueueHandle_t xQueue;

int main( void )
{
	prvSetupHardware();
	
    /* 创建队列: 长度为1,数据大小为4字节(存放一个char指针) */
    xQueue = xQueueCreate( 1, sizeof(char *) );

	if( xQueue != NULL )
	{
		/* 创建1个任务用于写队列
		 * 任务函数会连续执行,构造buffer数据,把buffer地址写入队列
		 * 优先级为1
		 */
		xTaskCreate( vSenderTask, "Sender", 1000, NULL, 1, NULL );

		/* 创建1个任务用于读队列
		 * 优先级为2, 高于上面的两个任务
		 * 这意味着读队列得到buffer地址后,本任务使用buffer时不会被打断
		 */
		xTaskCreate( vReceiverTask, "Receiver", 1000, NULL, 2, NULL );

		/* 启动调度器 */
		vTaskStartScheduler();
	}
	else
	{
		/* 无法创建队列 */
	}

	/* 如果程序运行到了这里就表示出错了, 一般是内存不足 */
	return 0;
}

发送任务的函数中,现在全局大数组pcBuffer中构造数据,然后把它的地址写入队列,代码如下:

static void vSenderTask( void *pvParameters )
{
	BaseType_t xStatus;
	static int cnt = 0;
	
	char *buffer;

	/* 无限循环 */
	for( ;; )
	{
		sprintf(pcBuffer, "www.100ask.net Msg %d\r\n", cnt++);
		buffer = pcBuffer; // buffer变量等于数组的地址, 下面要把这个地址写入队列
		
		/* 写队列
		 * xQueue: 写哪个队列
		 * pvParameters: 写什么数据? 传入数据的地址, 会从这个地址把数据复制进队列
		 * 0: 如果队列满的话, 即刻返回
		 */
		xStatus = xQueueSendToBack( xQueue, &buffer, 0 ); /* 只需要写入4字节, 无需写入整个buffer */

		if( xStatus != pdPASS )
		{
			printf( "Could not send to the queue.\r\n" );
		}
	}
}

接收任务的函数中,读取队列、得到buffer的地址、打印,代码如下:

static void vReceiverTask( void *pvParameters )
{
	/* 读取队列时, 用这个变量来存放数据 */
	char *buffer;
	const TickType_t xTicksToWait = pdMS_TO_TICKS( 100UL );	
	BaseType_t xStatus;

	/* 无限循环 */
	for( ;; )
	{
		/* 读队列
		 * xQueue: 读哪个队列
		 * &xReceivedStructure: 读到的数据复制到这个地址
		 * xTicksToWait: 没有数据就阻塞一会
		 */
		xStatus = xQueueReceive( xQueue, &buffer, xTicksToWait);

		if( xStatus == pdPASS )
		{
			/* 读到了数据 */
			printf("Get: %s", buffer);
		}
		else
		{
			/* 没读到数据 */
			printf( "Could not receive from the queue.\r\n" );
		}
	}
}

运行结果如下图所示:

在这里插入图片描述

3.4 : 邮箱(Mailbox)

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

发送任务、接收任务的代码和执行流程如下:

  • A:发送任务先执行,马上阻塞
  • BC:接收任务执行,这是邮箱无数据,打印"Could not …"。在发送任务阻塞过程中,接收任务多
    次执行、多次打印。
  • D:发送任务从阻塞状态退出,立刻执行、写队列
  • E:发送任务再次阻塞
  • FG、HI、……:接收任务不断"偷看"邮箱,得到同一个数据,打印出多个"Get: 0"
  • J:发送任务从阻塞状态退出,立刻执行、覆盖队列,写入1
  • K:发送任务再次阻塞
  • LM、……:接收任务不断"偷看"邮箱,得到同一个数据,打印出多个"Get: 1"

在这里插入图片描述

运行结果如下图所示:

在这里插入图片描述

四、队列集

队列知道是什么,那么队列集呢,没错就是套娃,队列集里面是队列。

队列集使用的前提

configUSE_QUEUE_SETS 置1

int main( void )
{
	TaskHandle_t xHandleTask1;
		
#ifdef DEBUG
  debug();
#endif

	prvSetupHardware();

	printf("Hello, world!\r\n");

	/* 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);

	/* Start the scheduler. */
	vTaskStartScheduler();

	/* Will only get here if there was not enough heap space to create the
	idle task. */
	return 0;
}
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);
	}
}

我们把队列1 队列2 添加到queue set里面

队列1 和队列2 分别再任务1和任务2里面发送数据

队列集再任务三里面接收数据

效果:

在这里插入图片描述


文章是自己总结而记录,有些知识点没说明白的,请各位看官多多提意见,多多交流,欢迎大家留言
如果技术交流可以加以下群,方便沟通
QQ群:370278903
点击链接加入群聊【蜡笔小芯的嵌入式交流群】
![])

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/620715.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

多人同时导出 Excel 干崩服务器!新来的阿里大佬给出的解决方案太优雅了!

前言 业务诉求:考虑到数据库数据日渐增多,导出会有全量数据的导出,多人同时导出可以会对服务性能造成影响,导出涉及到mysql查询的io操作,还涉及文件输入、输出流的io操作,所以对服务器的性能会影响的比较大…

十进制整数转平衡三进制

求解原视频&#xff1a;平衡三进制 求赞&#xff01;100赞买个乒乓球拍&#xff01;_哔哩哔哩_bilibili 题目&#xff1a; 上海市计算机学会竞赛平台 | YACS 求解程序&#xff1a; using namespace std; #include <iostream> #include <cstring>string work(int n…

网页版五子棋的自动化测试

目录 前言 一、主要技术 二、测试环境的准备部署 三、测试用例 四、执行测试 4.1、公共类设计 创建浏览器驱动对象 测试套件 释放驱动类 4.2、功能测试 登录页面 注册页面 游戏大厅页面 游戏房间页面 测试套件结果 4.3、界面测试 登录页面 注册页面 游戏大…

【Linux基础】Vim保姆级一键配置教程(手把手教你把Vim打造成高效率C++开发环境)

目录 一、前言 二、安装Vim 三、原始Vim编译器的缺陷分析 四、Vim配置 &#x1f95d;预备知识----.vimrc 隐藏文件 &#x1f34b;手动配置 Vim --- &#xff08;不推荐&#xff09; &#x1f347;自动化一键配置 Vim --- (强烈推荐) ✨功能演示 五、共勉 一、前言 Vim作为…

如何8步完成hadoop单机安装

前言 Hadoop是一个开源框架&#xff0c;用于存储和处理大规模数据集。 系统要求 Ubuntu 20.044GB&#xff08;建议8GB&#xff09;hadoop-3.3.6 步骤1&#xff1a;更新系统 打开终端并输入以下命令来更新您的系统&#xff1a; apt update 步骤2&#xff1a;安装Java Had…

NSSCTF Web方向的例题和相关知识点(一)

[SWPUCTF 2021 新生赛]jicao 解题&#xff1a; 打开环境&#xff0c;是一段php代码 包含了flag.php文件&#xff0c;设定了一个POST请求的id和GET请求的json 语句会对GET请求的数据进行json解码 如果id和json变量的值都等于设定字符串&#xff0c;则得到 flag 我们可以使用…

测试人的福音:开源流量回放工具快速上手实践

笔者前段时间在参加测开大会时了解到了一款开源的自动化回归测试工具 AREX。主要是通过复制线上真实流量到测试环境进行回归测试&#xff0c;同时还做到了接口返回值的比对和写接口的验证&#xff0c;回放不会产生真实的数据或者调用&#xff0c;都是基于 Mock 数据的&#xff…

VastGaussian:用于大型场景重建的巨大3D高斯函数

VastGaussian:用于大型场景重建的巨大3D高斯函数 摘要IntroductionRelated WorkPreliminariesMethod VastGaussian: Vast 3D Gaussians for Large Scene Reconstruction. 摘要 现有基于NeRF的大型场景重建方法在视觉效果和渲染速度方面往往存在限制。虽然最近的3D高斯分裂在小…

基于Python的校园舆情管理系统(附源码、文档说明)

博主介绍&#xff1a;✌IT徐师兄、7年大厂程序员经历。全网粉丝15W、csdn博客专家、掘金/华为云//InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;&#x1f3…

SpringAMQP-消息转换器

这边发送消息接收消息默认是jdk的序列化方式&#xff0c;发送到服务器是以字节码的形式&#xff0c;我们看不懂也很占内存&#xff0c;所以我们要手动设置一下 我这边设置成json的序列化方式&#xff0c;注意发送方和接收方的序列化方式要保持一致 不然回报错。 引入依赖&#…

微信小程序之转盘抽奖

1. 实现效果 2. 实现过程 话不多说&#xff0c;直接上代码 /**index.wxml */ <view class"title">旋转大转盘</view> <view class"rote-box fcc"><view class"box fcc"><image class"bg" src"/stat…

亚马逊跨境电商,如何制作产品安装视频二维码?

对于海外电商平台的商家来说&#xff0c;售后的客服工作也非常重要。产品破损、物流延误&#xff0c;或者使用体验不好、产品安装太复杂、缺少一个零件、发错颜色……任何一个新增的差评都够商家头疼好久&#xff0c;说服买家修改或者删除差评总要费很大工夫。 所以&#xff0…

【Python贪吃蛇】:编码技巧与游戏设计的完美结合

文章目录 &#x1f525;一、运行效果&#x1f4a5;二、游戏教程✈1. 导入模块❤️2. 初始化游戏元素☔3. 改变蛇移动的方向&#x1f44a;4. 绘制方块&#x1f680;5. 检查蛇头是否在游戏区域内&#x1f308;6. 定义蛇的移动函数&#x1f3ac;7. 绑定键盘事件 ⭐三、完整代码 &a…

探索美国动态IP池:技术赋能下的网络安全新篇章

在数字化飞速发展的今天&#xff0c;网络安全成为了各行各业关注的焦点。特别是在跨国业务中&#xff0c;如何保障数据的安全传输和合规性成为了企业面临的重要挑战。美国动态IP池作为一种新兴的网络技术&#xff0c;正逐渐走进人们的视野&#xff0c;为网络安全提供新的解决方…

LeetCode 0994.腐烂的橘子:广度优先搜索(BFS)

【LetMeFly】994.腐烂的橘子&#xff1a;广度优先搜索(BFS) 力扣题目链接&#xff1a;https://leetcode.cn/problems/rotting-oranges/ 在给定的 m x n 网格 grid 中&#xff0c;每个单元格可以有以下三个值之一&#xff1a; 值 0 代表空单元格&#xff1b;值 1 代表新鲜橘子…

韵搜坊(全栈开发)-- 项目介绍

文章目录 项目介绍技术栈前端后端 业务流程 后端地址&#xff1a; https://github.com/IMZHEYA/zhesou-backend 前端地址&#xff1a; https://github.com/IMZHEYA/zhesou-frontend 图标设计&#xff08;AI生成&#xff09;&#xff1a; 项目介绍 一个聚合搜素平台&#xff…

SaToken框架实现在Rpc上下文的login处理逻辑

最近在工作中遇到一个需求&#xff0c;需要在项目A中实现一个rpc接口供其他项目调用&#xff0c;接口返回登录token&#xff0c;从而实现其他项目的用户能免密登录到项目A。 项目A是用了SaToken来做的鉴权&#xff0c;原本我的打算是直接在rpc中调用StpUtil.login()方法来实现登…

速锐得深入解析吉利几何CAN总线数据通信网络的拓扑层级框架技术

在现代汽车工业中&#xff0c;车辆的电子控制单元&#xff08;ECU&#xff09;之间的通信至关重要。这种通信大多通过控制器局域网络&#xff08;CAN&#xff09;总线实现&#xff0c;它是德国BOSCH公司于20世纪80年代初开发的一种串行数据通信协议。随着技术的不断进步&#x…

【数据结构】之栈的应用——有效的括号

文章目录 有效的括号 有效的括号 原题链接&#xff1a;有效的括号 详解栈的链接 这道题可以利用栈来解决 1.左括号入栈 2.右括号与出栈顶左括号匹配 //创建一个动态的栈 typedef char STDateType; typedef struct Stack {STDateType* a;//储存指定数据类型的数组int top…

Verilog中信号发生器的代码实现

目录 描述 输入描述&#xff1a; 输出描述&#xff1a; 描述 题目描述&#xff1a; 请编写一个信号发生器模块&#xff0c;根据波形选择信号wave_choise发出相应的波形&#xff1a;wave_choice0时&#xff0c;发出方波信号&#xff1b;wave_choice1时&#xff0c;发出锯齿…