韦东山freeRTOS系列教程之【第五章】队列(queue)
文章目录
- 系列教程总目录
- 概述
- 5.1 队列的特性
- 5.1.1 常规操作
- 5.1.2 传输数据的两种方法
- 5.1.3 队列的阻塞访问
- 5.2 队列函数
- 5.2.1 创建
- 5.2.2 复位
- 5.2.3 删除
- 5.2.4 写队列
- 5.2.5 读队列
- 5.2.6 查询
- 5.2.7 覆盖/偷看
- 5.3 示例8: 队列的基本使用
- 5.4 示例9: 分辨数据源
- 5.5 示例10: 传输大块数据
- 5.6 示例11: 邮箱(Mailbox)
需要获取更好阅读体验的同学,请访问我专门设立的站点查看,地址:http://rtos.100ask.net/
系列教程总目录
本教程连载中,篇章会比较多,为方便同学们阅读,点击这里可以查看文章的 目录列表,目录列表页面地址:https://blog.csdn.net/thisway_diy/article/details/121399484
概述
队列(queue)可以用于"任务到任务"、“任务到中断”、"中断到任务"直接传输信息。
本章涉及如下内容:
- 怎么创建、清除、删除队列
- 队列中消息如何保存
- 怎么向队列发送数据、怎么从队列读取数据、怎么覆盖队列的数据
- 在队列上阻塞是什么意思
- 怎么在多个队列上阻塞
- 读写队列时如何影响任务的优先级
5.1 队列的特性
5.1.1 常规操作
队列的简化操如入下图所示,从此图可知:
- 队列可以包含若干个数据:队列中有若干项,这被称为"长度"(length)
- 每个数据大小固定
- 创建队列时就要指定长度、数据大小
- 数据的操作采用先进先出的方法(FIFO,First In First Out):写数据时放到尾部,读数据时从头部读
- 也可以强制写队列头部:覆盖头部数据
更详细的操作入下图所示:
5.1.2 传输数据的两种方法
使用队列传输数据时有两种方法:
- 拷贝:把数据、把变量的值复制进队列里
- 引用:把数据、把变量的地址复制进队列里
FreeRTOS使用拷贝值的方法,这更简单:
局部变量的值可以发送到队列中,后续即使函数退出、局部变量被回收,也不会影响队列中的数据
无需分配buffer来保存数据,队列中有buffer
局部变量可以马上再次使用
发送任务、接收任务解耦:接收任务不需要知道这数据是谁的、也不需要发送任务来释放数据
如果数据实在太大,你还是可以使用队列传输它的地址
队列的空间有FreeRTOS内核分配,无需任务操心
对于有内存保护功能的系统,如果队列使用引用方法,也就是使用地址,必须确保双方任务对这个地址都有访问权限。使用拷贝方法时,则无此限制:内核有足够的权限,把数据复制进队列、再把数据复制出队列。
5.1.3 队列的阻塞访问
只要知道队列的句柄,谁都可以读、写该队列。任务、ISR都可读、写队列。可以多个任务读写队列。
任务读写队列时,简单地说:如果读写不成功,则阻塞;可以指定超时时间。口语化地说,就是可以定个闹钟:如果能读写了就马上进入就绪态,否则就阻塞直到超时。
某个任务读队列时,如果队列没有数据,则该任务可以进入阻塞状态:还可以指定阻塞的时间。如果队列有数据了,则该阻塞的任务会变为就绪态。如果一直都没有数据,则时间到之后它也会进入就绪态。
既然读取队列的任务个数没有限制,那么当多个任务读取空队列时,这些任务都会进入阻塞状态:有多个任务在等待同一个队列的数据。当队列中有数据时,哪个任务会进入就绪态?
- 优先级最高的任务
- 如果大家的优先级相同,那等待时间最久的任务会进入就绪态
跟读队列类似,一个任务要写队列时,如果队列满了,该任务也可以进入阻塞状态:还可以指定阻塞的时间。如果队列有空间了,则该阻塞的任务会变为就绪态。如果一直都没有空间,则时间到之后它也会进入就绪态。
既然写队列的任务个数没有限制,那么当多个任务写"满队列"时,这些任务都会进入阻塞状态:有多个任务在等待同一个队列的空间。当队列中有空间时,哪个任务会进入就绪态?
- 优先级最高的任务
- 如果大家的优先级相同,那等待时间最久的任务会进入就绪态
5.2 队列函数
使用队列的流程:创建队列、写队列、读队列、删除队列。
5.2.1 创建
队列的创建有两种方法:动态分配内存、静态分配内存,
- 动态分配内存:xQueueCreate,队列的内存在函数内部动态分配
函数原型如下:
QueueHandle_t xQueueCreate( UBaseType_t uxQueueLength, UBaseType_t uxItemSize );
参数 | 说明 |
---|---|
uxQueueLength | 队列长度,最多能存放多少个数据(item) |
uxItemSize | 每个数据(item)的大小:以字节为单位 |
返回值 |
非0:成功,返回句柄,以后使用句柄来操作队列 NULL:失败,因为内存不足 |
- 静态分配内存:xQueueCreateStatic,队列的内存要事先分配好
函数原型如下:
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_SIZExQueue1 = xQueueCreateStatic( QUEUE_LENGTH,ITEM_SIZE,ucQueueStorage,&xQueueBuffer ); }
5.2.2 复位
队列刚被创建时,里面没有数据;使用过程中可以调用xQueueReset()
把队列恢复为初始状态,此函数原型为:
/* pxQueue : 复位哪个队列;* 返回值: pdPASS(必定成功)*/
BaseType_t xQueueReset( QueueHandle_t pxQueue);
5.2.3 删除
删除队列的函数为vQueueDelete()
,只能删除使用动态方法创建的队列,它会释放内存。原型如下:
void vQueueDelete( QueueHandle_t xQueue );
5.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:写入失败,因为队列满了。 |
5.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 | 队列句柄,要读哪个队列 |
pvBuffer |
bufer指针,队列的数据会被复制到这个buffer 复制多大的数据?在创建队列时已经指定了数据大小 |
xTicksToWait |
果队列空则无法读出数据,可以让任务进入阻塞状态, xTicksToWait表示阻塞的最大时间(Tick Count)。 如果被设为0,无法读出数据时函数会立刻返回; 如果被设为portMAX_DELAY,则会一直阻塞直到有数据可写 |
返回值 |
pdPASS:从队列读出数据入 errQUEUE_EMPTY:读取失败,因为队列空了。 |
5.2.6 查询
可以查询队列中有多少个数据、有多少空余空间。函数原型如下:
/** 返回队列中可用数据的个数*/
UBaseType_t uxQueueMessagesWaiting( const QueueHandle_t xQueue );/** 返回队列中可用空间的个数*/
UBaseType_t uxQueueSpacesAvailable( const QueueHandle_t xQueue );
5.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,);
5.3 示例8: 队列的基本使用
本节代码为:FreeRTOS_08_queue
。
本程序会创建一个队列,然后创建2个发送任务、1个接收任务:
- 发送任务优先级为1,分别往队列中写入100、200
- 接收任务优先级为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" );}}
}
程序运行结果如下:
任务调度情况如下图所示:
5.4 示例9: 分辨数据源
本节代码为:FreeRTOS_09_queue_datasource
。
当有多个发送任务,通过同一个队列发出数据,接收任务如何分辨数据来源?数据本身带有"来源"信息,比如写入队列的数据是一个结构体,结构体中的lDataSouceID用来表示数据来源:
typedef struct {ID_t eDataID;int32_t lDataValue;
}Data_t;
不同的发送任务,先构造好结构体,填入自己的eDataID
,再写队列;接收任务读出数据后,根据eDataID
就可以知道数据来源了,如下图所示:
- CAN任务发送的数据:eDataID=eMotorSpeed
- HMI任务发送的数据:eDataID=eSpeedSetPoint
FreeRTOS_09_queue_datasource
程序会创建一个队列,然后创建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任务都在等待空闲空间,只有接收任务可以继续执行。
5.5 示例10: 传输大块数据
本节代码为:FreeRTOS_10_queue_bigtransfer
。
FreeRTOS的队列使用拷贝传输,也就是要传输uint32_t时,把4字节的数据拷贝进队列;要传输一个8字节的结构体时,把8字节的数据拷贝进队列。
如果要传输1000字节的结构体呢?写队列时拷贝1000字节,读队列时再拷贝1000字节?不建议这么做,影响效率!
这时候,我们要传输的是这个巨大结构体的地址:把它的地址写入队列,对方从队列得到这个地址,使用地址去访问那1000字节的数据。
使用地址来间接传输数据时,这些数据放在RAM里,对于这块RAM,要保证这几点:
- RAM的所有者、操作者,必须清晰明了
这块内存,就被称为"共享内存"。要确保不能同时修改RAM。比如,在写队列之前只有由发送者修改这块RAM,在读队列之后只能由接收者访问这块RAM。 - RAM要保持可用
这块RAM应该是全局变量,或者是动态分配的内存。对于动然分配的内存,要确保它不能提前释放:要等到接收者用完后再释放。另外,不能是局部变量。
FreeRTOS_10_queue_bigtransfer
程序会创建一个队列,然后创建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); /* 得到buffer地址,只是4字节 */if( xStatus == pdPASS ){/* 读到了数据 */printf("Get: %s", buffer);}else{/* 没读到数据 */printf( "Could not receive from the queue.\r\n" );}}
}
运行结果如下图所示:
5.6 示例11: 邮箱(Mailbox)
本节代码为:FreeRTOS_11_queue_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"
运行结果如下图所示:
韦东山freeRTOS系列教程之【第五章】队列(queue)相关推荐
- 韦东山freeRTOS系列教程之【第二章】内存管理
文章目录 教程目录 2.1 为什么要自己实现内存管理 2.2 FreeRTOS的5种内存管理方法 2.2.1 Heap_1 2.2.2 Heap_2 2.2.3 Heap_3 2.2.4 Heap_4 ...
- 韦东山freeRTOS系列教程之【第十章】软件定时器(software timer)
文章目录 系列教程总目录 概述 10.1 软件定时器的特性 10.2 软件定时器的上下文 10.2.1 守护任务 10.2.2 守护任务的调度 10.2.3 回调函数 10.3 软件定时器的函数 10 ...
- Java NIO系列教程(十 五)Java NIO Path
转载自 Java NIO系列教程(十 五)Java NIO Path 译文链接 译者:章筱虎 Java的Path接口是Java NIO2 的一部分,是对Java6 和Java7的 NIO的更新.J ...
- SpringBoot 系列教程(八十五):Spring Boot使用MD5加盐验签Api接口之前后端分离架构设计
加密算法参考: 浅谈常见的七种加密算法及实现 加密算法参考: 加密算法(DES,AES,RSA,MD5,SHA1,Base64)比较和项目应用 目的: 通过对API接口请求报文签名,后端进行验签处理, ...
- 【学习笔记】韦东山freertos直播学习笔记
韦东山FreeRTOS学习 如果刚好有大佬看到发现笔记中有什么写错了的,欢迎大佬指点,我十分乐意受到大佬的指点哈哈哈,顺便我还想问一下csdn的编辑器有没有保存的快捷键啊,CTRL+S好像没有用啊. ...
- 【爆肝更新】Python基础教程:第五章_Python函数
原创:公众号 数据说话 [爆肝更新]Python基础教程:第五章_Python函数 函数介绍 学习目标: 快速体验一下函数的使用 了解函数的作用. 函数:是组织好的,可重复使用的,用来实现特定功能的代 ...
- ArcGIS for Desktop入门教程_第五章_ArcCatalog使用 - ArcGIS知乎-新一代ArcGIS问答社区
原文:ArcGIS for Desktop入门教程_第五章_ArcCatalog使用 - ArcGIS知乎-新一代ArcGIS问答社区 1 ArcCatalog使用 1.1 GIS数据 地理信息系统, ...
- D3.js的v5版本入门教程(第五章)—— 选择、插入、删除元素
D3.js的v5版本入门教程(第五章) 1.选择元素 现在我们已经知道,d3.js中选择元素的函数有select()和selectAll(),下面来详细讲解一下 假设我们的<body>中有 ...
- 推荐系统系列教程之十五:一网打尽协同过滤、矩阵分解和线性模型
编者按:之前推出了<推荐系统系列教程>,反响不错,前面已经推出了十四期,今天按约推出第十五期:一网打尽协同过滤.矩阵分解和线性模型.希望朋友们多点"在看",多多转发,我 ...
- 韦东山鸿蒙开发教程02 - 资料下载方法
在线课堂:https://www.100ask.net/index(课程观看) 论 坛:http://bbs.100ask.net/(学术答疑) 开 发 板:https://100ask.taoba ...
最新文章
- 字符串最长子串匹配-dp矩阵[转载]
- BZOJ2118墨墨的等式[数论 最短路建模]
- ubuntu16.04下面git与github对应起来
- apache camel 相关配置_使用apache camel从表中选择数据-问答-阿里云开发者社区-阿里云...
- LinkedList专题3
- Android Glide图片加载框架(三)缓存机制
- python转json的函数_pandas.DataFrame.to_json按行转json的方法
- 北京环球度假区:尚未发布票务信息,未面向公众销售任何门票
- Python 字符串、列表、字典 操作方法大全 正则re
- 斐讯盒子t1 刷Android,斐讯盒子T1/N1刷机流水账
- Surface Pro 7 综合评测——购买感受和使用重点【品控、性能、投影】
- u盘启动盘恢复计算机,怎么还原u盘启动盘为普通u盘?
- 【专项研究】支付宝会员积分体系
- matlab实现对图像的简单幻方置乱
- Vue 背景音乐点击随机播放
- php 简明现代魔法,【转】WinCacheGrind配合XDebug分析PHP程序性能 — 简明现代魔法...
- rockchip的yocto编译环境的搭建
- CentOS7防火墙配置,重启
- RT-Thread Studio中ST-LINK下载报错“Old ST-LINK firmware version“解决
- 大三学弟的学习面试经验,他很成熟
热门文章
- 计算机双面打印设置,双面打印怎么设置?双面打印设置方法步骤
- Json对象与Json字符串互转(四种转换方式)
- im源码 php,thinkphpim即时通讯源码
- php gmssl,centos7 php7 gmssl编译安装
- 产品市场调研分析报告、竞品分析报告、产品体验报告的区别
- oppok3如何刷机_数码知识:oppok3怎么刷机刷机教程
- 这款桌面分区软件太好用了
- 一款安全性测试工具Appscan
- 海思烧写工具需要java_HiTool(海思芯片烧录工具)下载
- 日本家用电器技术标准及IEC对照介绍