一、队列
1、队列的特点
数据的操作采用先进先出的方法(FIFO),写数据时放到尾部,读数据时从头部开始读取。
也可以强制写队列头部,但不会覆盖头部数据。
队列中每个数据的大小时固定的。
创建队列时要指定长度,数据大小。
2、队列的使用
在创建队列时,会创建两个链表,分别存放等待数据的任务和等待向队列中写的数据,buf中存放队列的数据。
1)创建队列
#define xQueueCreate( uxQueueLength, uxItemSize ) xQueueGenericCreate( ( uxQueueLength ), ( uxItemSize ), ( queueQUEUE_TYPE_BASE ) )
//uxQueueLength 队列长度
//uxItemSize 每个项大小
可以看到xQueueXCreat()最后调用的是xQueueGenericCreate()。
创建队列时,会创建几个指针,pcHead始终指向头部,不会移动;pcWriteto在初始化时指向头部,后面指向要写入的下一个位置;pcReadFrom指向上一次读的位置,使用前要指向这一次读的位置。
2)向队列中写入数据
//向队列中写数据
#define xQueueSend( xQueue, pvItemToQueue, xTicksToWait ) \
xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_BACK )
//向队列尾部写数据
#define xQueueSendToBack( xQueue, pvItemToQueue, xTicksToWait ) \
xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_BACK )
//向队列首部写数据
#define xQueueSendToFront( xQueue, pvItemToQueue, xTicksToWait ) \
xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), ( xTicksToWait ), queueSEND_TO_FRONT )
//向队列尾部写数据,可以在中断函数中使用,不可阻塞
#define xQueueSendToBackFromISR( xQueue, pvItemToQueue, pxHigherPriorityTaskWoken ) \
xQueueGenericSendFromISR( ( xQueue ), ( pvItemToQueue ), ( pxHigherPriorityTaskWoken ), queueSEND_TO_BACK )
//向队列头部写数据,可以在中断函数中使用,不可阻塞
#define xQueueSendToFrontFromISR( xQueue, pvItemToQueue, pxHigherPriorityTaskWoken ) \
xQueueGenericSendFromISR( ( xQueue ), ( pvItemToQueue ), ( pxHigherPriorityTaskWoken ), queueSEND_TO_FRONT )
向队列中写数据,默认就是向队列尾部写数据;向队列头部写数据实际上是队列其他数据不变,然后写到pcReadFrom的位置,pcReadFrom向后移动一个位置,这样就写到了头部,下一次读取的就是这个数据。
3)从队列中读数据
BaseType_t xQueueReceive( QueueHandle_t xQueue,
void * const pvBuffer,
TickType_t xTicksToWait )
BaseType_t xQueueReceiveFromISR( QueueHandle_t xQueue,
void * const pvBuffer,
BaseType_t * const pxHigherPriorityTaskWoken )
4)查询
查询队列中有多少个数据,有多少剩余空间。
//返回队列中可用数据的个数
UBaseType_t uxQueueMessagesWaiting( const QueueHandle_t xQueue )
//返回队列中可用空间的个数
UBaseType_t uxQueueSpacesAvailable( const QueueHandle_t xQueue )
5)覆盖/偷看
当队列长度为1时,可以使用xQueueOverwrite()或xQueueOverwriteFromISR()来覆盖数据。队列长度必须为1,当队列满时,这些函数会覆盖里面的数据,这也意味着这些函数不会被阻塞。
#define xQueueOverwrite( xQueue, pvItemToQueue ) \
xQueueGenericSend( ( xQueue ), ( pvItemToQueue ), 0, queueOVERWRITE )
#define xQueueOverwriteFromISR( xQueue, pvItemToQueue, pxHigherPriorityTaskWoken ) \
xQueueGenericSendFromISR( ( xQueue ), ( pvItemToQueue ), ( pxHigherPriorityTaskWoken ), queueOVERWRITE )
偷看时不会移除数据,这些函数会从队列中复制出数据。这样意味着如果队列中没有数据,这些函数将会一直阻塞,一旦队列中有数据,每次偷看都会成功。
BaseType_t xQueuePeek( QueueHandle_t xQueue,
void * const pvBuffer,
TickType_t xTicksToWait )
BaseType_t xQueuePeekFromISR( QueueHandle_t xQueue,
void * const pvBuffer )
3、队列集
顾名思义,队列集就是队列的集合,当接收程序想从多个队列中读取数据的时候,如果采用轮训的方式会导致效率低下,可以假设要从mouse,key,touch,三个队列中读取数据,当三个队列中某个队列由数据写入的时候,就会将其句柄放入到Queue_Set中,当从队列集中读取数据的时候,只需获取到其句柄,然后从其对应的队列中读取数据即可。
使用队列集的步骤如下:
//1.创建队列集
xQueueSet = xQueueCreateSet(4);
//2.添加联系
xQueueAddToSet(xQueueHandle1,xQueueSet);
xQueueAddToSet(xQueueHandle2,xQueueSet);
//3.创建三个任务
xTaskCreate(Task1Function, "Task1", 100, NULL, 1, &xHandleTask1);
xTaskCreate(Task2Function, "Task2", 100, NULL, 1, NULL);
xTaskCreate(Task3Function, "Task3", 100, NULL, 1, NULL);
//4.读取QueueSet
handle = xQueueSelectFromSet(xQueueSet,portMAX_DELAY)
//5.读取队列
xQueueReceive(handle,&i,0);
二、信号量
1、特点
只传递状态,不传递具体的信息。
相较于队列,更节省内存。
2、信号量和队列的区别,信号量的分类如下
二进制信号量 | 技术型信号量 |
被创建时初始值为0 | 被创建时初始值可以设定 |
其他操作一样 | 其它操作一样 |
3、信号量函数
1)创建信号量
//动态创建一个二进制信号量,返回它的句柄,可以看到实际上创建了一个长度为1,无符号字符型的队列
#define xSemaphoreCreateBinary() xQueueGenericCreate( ( UBaseType_t ) 1, semSEMAPHORE_QUEUE_ITEM_LENGTH, queueQUEUE_TYPE_BINARY_SEMAPHORE )
//静态创建一个二进制信号量,返回它的句柄,可以看到实际上创建了一个长度为1,无符号字符型的队列
#define xSemaphoreCreateBinaryStatic( pxStaticSemaphore ) xQueueGenericCreateStatic( ( UBaseType_t ) 1, semSEMAPHORE_QUEUE_ITEM_LENGTH, NULL, pxStaticSemaphore, queueQUEUE_TYPE_BINARY_SEMAPHORE )
//动态创建一个计数型信号量
//返回它的句柄,
//uxMaxCount:最大计数值
//uxInitialCount :计数初始值
#define xSemaphoreCreateCounting( uxMaxCount, uxInitialCount ) xQueueCreateCountingSemaphore( ( uxMaxCount ), ( uxInitialCount ) )
//静态创建一个技术型信号量
//返回它的句柄,非NULL代表成功
//uxMaxCount:最大计数值
//uxInitialCount:计数初始值
//pxSemaphoreBuffer :StaticSemaphore_t结构体指针
#define xSemaphoreCreateCountingStatic( uxMaxCount, uxInitialCount, pxSemaphoreBuffer ) xQueueCreateCountingSemaphoreStatic( ( uxMaxCount ), ( uxInitialCount ), ( pxSemaphoreBuffer ) )
2)删除
//删除一个信号量
//xSemaphore :信号量句柄
//实际上删除一个队列
#define vSemaphoreDelete( xSemaphore ) vQueueDelete( ( QueueHandle_t ) ( xSemaphore ) )
3)give/take
在任务中使用 | 在ISR中使用 | |
give | xSemaphoreGive | xSemaphoreGiveFromISR |
take | xSemaphoreTake | xSemaphoreTakeFromISR |
//释放信号量
//xSemaphore:释放哪个信号量
//返回值:pdTRUE,如果二进制信号量的计数值是1,再次调用则返回失败
如果计数型信号量的计数值已是最大值,再次调用则返回失败
#define xSemaphoreGive( xSemaphore ) xQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ), NULL, semGIVE_BLOCK_TIME, queueSEND_TO_BACK )
//在ISR中释放信号量
//xSemaphore:信号量句柄,释放哪个信号量
//pxHigherPriorityTaskWoken :如果释放信号量导致更高优先级的任务变为了就绪态,则*pxHigherPriorityTaskWoken = pdTRUE
//返回值 pdTRUE表示成功,如果二进制信号量的计数值是1,再次调用则返回失败
如果计数型信号量的计数值已是最大值,再次调用则返回失败
#define xSemaphoreGiveFromISR( xSemaphore, pxHigherPriorityTaskWoken ) xQueueGiveFromISR( ( QueueHandle_t ) ( xSemaphore ), ( pxHigherPriorityTaskWoken ) )
//拿走信号量
//xSemaphore:拿走哪个信号量
//xBlockTime:等待时间,0:不阻塞,马上返回portMAX_DELAY:一直阻塞到成功
//其他值,阻塞的Tick个数,可以使用pdMS_TO_TICKS()来指定阻塞时间为若干ms
//返回值,pdTRUE表示成功
#define xSemaphoreTake( xSemaphore, xBlockTime ) xQueueSemaphoreTake( ( xSemaphore ), ( xBlockTime ) )
//在ISR中拿走信号量
//xSemaphore:信号量句柄,拿走哪个信号量
//pxHigherPriorityTaskWoken 如果获取信号量导致更高优先级的任务变为了就绪态,
则*pxHigherPriorityTaskWoken = pdTRUE
//返回值:返回pdTRTUE代表成功
#define xSemaphoreTakeFromISR( xSemaphore, pxHigherPriorityTaskWoken ) xQueueReceiveFromISR( ( QueueHandle_t ) ( xSemaphore ), NULL, ( pxHigherPriorityTaskWoken ) )
三、互斥量(互斥锁)
1、互斥量主要来解决两个问题:
信号量导致的优先级继承:假设一个场景,有三个信号量,A,B,C.优先级顺序是C>B>A,A执行后,A获得锁后执行,B到达就绪态,B执行,C到达就绪态,C执行,C想要获得锁,但是由于A拿走了锁,导致C阻塞,从而B执行,但是由于C阻塞,A的优先级小于B,所以B会一直执行。
优先级继承指的是,当C到达就绪态后,C想要获得锁,但是锁在A那里,往后,A获得C的优先级,A执行完后,释放锁,C继续执行。
谁拿走锁,谁释放锁。
但是FreeRTOS的一般的互斥量并没有实现这一点,而要靠递归锁实现。
2、函数原型
互斥量不能在ISR中使用,互斥量是一种特殊的信号量,获得和释放基本和信号量一致。
//动态创建一个互斥量,返它的句柄
//函数内部会分配一个互斥量的结构体
//返回值:返回句柄,非NULL表示成功
#define xSemaphoreCreateMutex() xQueueCreateMutex( queueQUEUE_TYPE_MUTEX )
//静态创建一个互斥量,返回它的句柄
//此函数无需动态分配内存,传入一个pxMutexBuffer 即可
//返回值:非NULL代表成功
#define xSemaphoreCreateMutexStatic( pxMutexBuffer ) xQueueCreateMutexStatic( queueQUEUE_TYPE_MUTEX, ( pxMutexBuffer ) )
//删除一个互斥量
//xSemaphore :信号量的句柄结构体
//#define vSemaphoreDelete( xSemaphore ) vQueueDelete( ( QueueHandle_t ) ( xSemaphore )
//释放互斥量
//xSemaphore:信号量的句柄结构体
#define xSemaphoreGive( xSemaphore ) xQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ), NULL, semGIVE_BLOCK_TIME, queueSEND_TO_BACK )
//获得互斥量
//xSemaphore:互斥量的句柄结构体
//xBlockTime :等待时间
#define xSemaphoreTake( xSemaphore, xBlockTime ) xQueueSemaphoreTake( ( xSemaphore ), ( xBlockTime ) )
3、递归锁
递归锁的函数和一般互斥量的函数名不一样,参数类型一样,列表如下
递归锁 | 一般互斥量 | |
创建 | xSemaphoreCreateRecursiveMutex | xSemaphoreCreateMutex |
获得 | xSemaphoreTakeRecursive | xSemaphoreTake |
释放 | xSemaphoreGiveRecursive | xSemaphoreGive |
函数原型
//创建一个递归锁
//返回值:返回句柄,非NULL代表成功
#define xSemaphoreCreateRecursiveMutex() xQueueCreateMutex( queueQUEUE_TYPE_RECURSIVE_MUTEX )
//释放互斥锁
//xMutex:递归锁句柄
//返回值:成功返回pdTRUE
#define xSemaphoreGiveRecursive( xMutex ) xQueueGiveMutexRecursive( ( xMutex ) )
//获得互斥锁
//xMutex:互斥锁句柄结构体
//xBlockTime:等待时间
//返回值:成功返回pdTRUE,超过xBlockTime时间,返回pdFALSE
#define xSemaphoreTakeRecursive( xMutex, xBlockTime ) xQueueTakeMutexRecursive( ( xMutex ), ( xBlockTime ) )
四、事件组
1、特点
1)事件组可以认为是一个整数,每一位表示一个事件,每一位事件的含义由程序员决定,
2)一个或多个任务,ISR都可以去写这些位,一个或多个任务,ISR都可以去读这些位
3)可以等待某一位,某些位中的任意一个,也可以等待多位
4)能表示事件组的整数是多少位的,取决于系统是多少位的。
5)和队列和信号量的对比如下
队列、信号量 | 事件组 |
事件发生时,只会唤醒一个任务 | 时间发生时们可以唤醒所有符合条件的任务 |
是消耗型的资源,队列数据读走就没了,信号量获取后就减少了 | 被唤醒的任务有两个选择,可以让事件保持不动,也可以清除事件 |
2、函数原型
//静态创建一个事件组
//成功返回它的句柄结构体
EventGroupHandle_t xEventGroupCreate( void );
//动态创建一个事件组
EventGroupHandle_t xEventGroupCreateStatic( StaticEventGroup_t * pxEventGroupBuffer );
//删除事件组
//xEventGroup:要删除事件组的句柄结构体
void vEventGroupDelete( EventGroupHandle_t xEventGroup );
//设置事件组中的位
//xEventGroup:事件组的句柄结构体
//uxBitsToSet:设置哪些位
//返回值:返回原来的事件值,意义不大
EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet );
//在ISR中设置事件组的位
//pxHigherPriorityTaskWoken:有没有导致更高优先级的任务进入就绪态,pdTRUE-有,pdFALSE-没有
//返回值,paTRUE-成功,pdFALSE失败
BaseType_t xEventGroupSetBitsFromISR( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet,
BaseType_t * pxHigherPriorityTaskWoken )
//等待事件
//uxBitsToWaitFor:等待哪些位,哪些位要被测试
//xClearOnExit:函数退出前是否要清除事件,pdTRUE,清除uxBitsToWaitFor的位,pdFALSE,不清除
//xClearOnExit:怎么测试,是AND还是OR,pdTRUE,等待的位全为1,pdFALSE,等待的位某一个为1
//xTicksToWait :等待多久
//返回值:返回的是事件,即如果期待的时间发生了,则返回发生的事件。如果超时退出,则返回超时时刻的事件值
EventBits_t xEventGroupWaitBits( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToWaitFor,
const BaseType_t xClearOnExit,
const BaseType_t xWaitForAllBits,
TickType_t xTicksToWait )
3、同步点
假设有一个事情需要多个任务协同。
任务A:炒菜
任务B:买酒
任务C:摆台
ABC做好自己的任务后,还需等待别人做完才可以开饭
这个时候用同步点会更加方便
函数原型如下:
//xEventGroup:哪个事件组
//uxBitsToSet:自己完成了哪些事件
//uxBitsToWaitFor:要等待哪些事件
//xTicksToWait:等待多久
//返回值:如果期待的事件发生了,返回的是“非阻塞条件成立”时事件值
//如果超时退出,返回的是超时时刻的事件值
EventBits_t xEventGroupSync( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet,
const EventBits_t uxBitsToWaitFor,
TickType_t xTicksToWait )
五、任务通知
1、任务通知的优点和缺点如下
优点 | 缺点 |
效率更高 | 不能发送数据给ISR |
更节省内存,不需要创建额外的结构体 | 数据只能给该任务独享 |
无法缓冲数据 | |
无法广播给多个任务 | |
如果发送受阻,发送方无法进入阻塞状态等待 |
任务通知中,值和状态保存在这里。
//configTASK_NOTIFICATION_ARRAY_ENTRIES 是1
// ucNotifyState:taskNOT_WAITING_NOTIFICATION:任务没有在等待通知
//taskWAITING_NOTIFICATION:任务在等待通知
//taskNOTIFICATION_RECEIVED:任务接受到了通知
//通知值也有很多类型:计数值,位(类似事件组),任意数值
#if ( configUSE_TASK_NOTIFICATIONS == 1 )
volatile uint32_t ulNotifiedValue[ configTASK_NOTIFICATION_ARRAY_ENTRIES ];
volatile uint8_t ucNotifyState[ configTASK_NOTIFICATION_ARRAY_ENTRIES ];
#endif
也就是说一个uint32_t类型,用来表示通知值,一个uint8_t 类型,用来表示状态。
2、函数原型
有两套,一套专业版,一套简化版
简化版:
使用xTaskNotifyGive()时:
使得通知值加1,并使得通知状态变为“pending”。
使用ulTaskNotifyTake()取出通知值时:
如果通知值等于0,则阻塞
当通知值大于0时。任务从阻塞态进入就绪态
在ulTaskNotifyTake()返回前,还可以做些清理工作,如把通知值减一,或者把通知值清零。
//xTaskToNotify:任务句柄,创建任务时得到,
//返回值:必定返回pdPASS
#define xTaskNotifyGive( xTaskToNotify ) \
xTaskGenericNotify( ( xTaskToNotify ), ( tskDEFAULT_INDEX_TO_NOTIFY ), ( 0 ), eIncrement, NULL )
#define vTaskNotifyGiveFromISR( xTaskToNotify, pxHigherPriorityTaskWoken ) \
vTaskGenericNotifyGiveFromISR( ( xTaskToNotify ), ( tskDEFAULT_INDEX_TO_NOTIFY ), ( pxHigherPriorityTaskWoken ) );
//xClearCountOnExit:退出前是否清零,pdTRUE:把通知值清零,pdFALSE:不把通知值清零
//xTicksToWait:等待时间
//返回值:函数返回之前,在清零或减一之前的通知值
//如果xTicksToWait非零,则返回值有两种情况 大于0,:在超时前,通知值被增加了,等于0,一直没有其他任
//务增加通知值,最后超时返回。
#define ulTaskNotifyTake( xClearCountOnExit, xTicksToWait ) \
ulTaskGenericNotifyTake( ( tskDEFAULT_INDEX_TO_NOTIFY ), ( xClearCountOnExit ), ( xTicksToWait ) )
专业版:
使用xTaskNotify(),可以设置参数,实现不同的功能。
1)让接收任务的通知值加1,:这时xTaskNotify()等同于xTaskNotifyGive()。
2)设置接收任务通知的某一位,某些位,这就是一个轻量级的更高效的事件组。
3)把一个新值写入接收任务的通知值:上一次被读走后写入才成功。这就是轻量级的,长度为1的队列。
4)用一个新值覆盖接收任务的通知值:无论上一次的通知值是否被读走,覆盖都成功。
xTaskNotifyWait(),
1)可以让任务等待(可以加上超时时间),等到任务状态为pending
2)可以在函数进入,退出时,清除通知值的指定位。
函数原型如下:
BaseType_t xTaskNotify( TaskHandle_t xTaskToNotify, uint32_t ulValue,
eNotifyAction eAction );
BaseType_t xTaskNotifyFromISR( TaskHandle_t xTaskToNotify,
uint32_t ulValue,eNotifyAction eAction,
BaseType_t *pxHigherPriorityTaskWoken );
BaseType_t xTaskNotifyWait( uint32_t ulBitsToClearOnEntry,uint32_t ulBitsToClearOnExit,
uint32_t *pulNotificationValue,TickType_t xTicksToWait );
参数说明如下:
xTaskNotify()
xTaskToNotify | 任务句柄 |
ulValue | 如何使用,有eAction决定 |
eAction | 见下表 |
返回值 |
paPASS:大部分都会成功 pdFAIL:只有一种情况会失败,当eAction为eSetValueWithoutOverwrite,并且通知状态为pending时,失败 |
eAction参数说明
eNoAction | 仅仅是更新通知状态为“pending”,未使用value,这个相当于轻量级、更高效的二进制信号量 |
eSetBits |
通知值 = 原来的通知值 | ulValue,按位或 相当于轻量级的、更高效的事件组 |
eIncrement |
通知值 = 原来的通知值 + 1,未使用ulValue 相当于轻量级的、更高效的二进制信号量、技术型信号量 |
eSetValueWithoutOverwrite | 不覆盖。 如果通知状态为"pending"(表示有数据未读), 则此次调用xTaskNotify不做任何事,返回pdFAIL。 如果通知状态不是"pending"(表示没有新数据), 则:通知值 = ulValue。 |
eSetValueWithOverwrite | 覆盖,无论如何,不管通知状态是否为“pending”,通知值为value |
xTaskNotifyWait()
ulBitsToClearOnEntry |
在xTaskNotifyWait()入口处,要清除通知值的哪些位?通知状态不是“pending”的情况下,才会清除。 它的本意是:我想等待某些事件的发生,所以先把“旧数据”的某些位清零 能清零的话,通知值 = 通知值 & ~(ulBitsToClearOnEntry) 比如传入0x01,表示清除通知值的bit0,传入0xffffffff,表示清除所有值 |
ulBitsToClearOnExit |
在xTaskNotifyWait()出口处,如果不是因为超时退出,而是因为得到了数据退出: 通知值 = 通知值 & ~(ulBitsToClearOnExit) 在清除某些位之前,通知值先被赋给*pulNotificationValue 比如传入0x03,表示清除通知值的bit0,bit1 传入0xffffffff,表示清除所有位,即把值设置为0 |
pulNotificationValue |
用来取出通知值。在函数退出时,使用ulBitsToClearOnExit清除之前,把通知值赋给*pulNotificationValue 如果不需要取出通知值,可设为NULL |
xTicksToWait | 等待时间 |
返回值 |
pdPASS:表示xTaskNotifyWait()成功获得了通知,可能是在调用函数之前,通知状态变为了“pending” 也可能是在阻塞期间,通知状态变为了“pending” pdFAIL:没有得到通知 |
任务通知和队列的区别如下:
任务通知 | 队列 |
只有一个数据,数据是32位的 | 可以容纳多个数据,数据大小可以指定 |
写队列时,不可以阻塞 | 写队列时,可以阻塞 |
可以覆盖,也可以不覆盖 | 如果队列长度是1,可以选择覆盖队列 |
任务通知不能真正意义上实现事件组,因为
它不能等待指定的事件
它不能等待若干个事件中的任意一个
一旦有事件,总会唤醒任务
总结
内核对象 | 生产者 | 消费者 | 数据/状态 | 说明 |
队列 | ALL | ALL |
数据:若干个数据 谁都可以向队列里扔数据 谁都可以从队列中读取数据 |
用来传递数据 发送者、接受者无限制,一个数据只能唤醒一个接收者 |
信号量 | ALL | ALL |
数量:0-n 谁都可以增加一个数量 谁都可以消耗一个数量 | 用来维持资源的个数,生产者、消费者无限制。1个资源只能唤醒1个接收者 |
互斥量 | 只能A开锁 | A上锁 |
位:0、1 我上锁:1变为0 只能由我开锁:0变为1 |
就向一个空厕所,谁使用谁上锁, 也只能由他开锁 |
事件组 | ALL | ALL |
多个位:或、与 谁都可以设置(生产)多个位,谁都可以等待某个位、若干个位 |
用来传递事件, 发送者接收者无限制 可以唤醒多个接收者 |
任务通知 | ALL | 只有我 |
数据、状态都可以传输 使用任务通知时,必须指定接收者 |
N对1的关系,发送者无限制 接受者只能是这个任务 |
以上参考了韦东山老师的FreeRTOS完全开发手册。