我们在前面单独介绍过FreeRTOS的任务通知和消息队列,
但是在FreeRTOS中任务间的通讯还有信号量,邮箱,事件组标志等可以使用
这篇文章就这些成员与消息队列和任务通知的关系进行说明分析
说明:FreeRTOS 专栏与我的 RT-Thread 专栏不同,我的 RT-Thread 专栏是从理论学习一步一步循序渐进,从 0 起步的 完整教学,而 FreeRTOS 更偏向于 我直接拿来使用,需要用到什么,然后引出知识点,在使用中发现问题,解然后再解决问题,
本 FreeRTOS 专栏记录的开发环境:
FreeRTOS记录(一、熟悉开发环境以及CubeMX下FreeRTOS配置)
FreeRTOS记录(二、FreeRTOS任务API认识和源码简析)
FreeRTOS记录(三、RTOS任务调度原理解析_Systick、PendSV、SVC)
FreeRTOS记录(四、FreeRTOS任务堆栈溢出问题和临界区)
FreeRTOS记录(五、FreeRTOS任务通知)
FreeRTOS记录(六、FreeRTOS消息队列—Enocean模块串口通讯、RAM空间不足问题分析)
对于操作系统初学者,估计面对于这么多功能,一下子蒙圈了,虽然照着资料视频对于单独的功能都能够正常的操作,但是实际运用到项目中的时候,往往会犯选择困难症,感觉这种也可以,那种也可以,到底哪种好哪种不好?
其实使用中并没有绝对的界限,需要看自己使用的场景,对于普通小项目而言,所谓的合适不合适并没有那么明显。我们在裸机中使用全局变量当做标志位尚且能够应付很多小项目,加入操作系统为的是保证实时性,对于任务之间的通讯的这些功能模块不需要太“刻意”的最求最优的方式,但是对于这些不同类型的功能至少也得有个基本的了解
推荐一篇博文:[FreeRTOS消息队列、信号量、事件标志组、任务通知],下图转至此博文:
一、任务通知与信号量、事件标志组
1.1 基本概念
信号量是用于任务与任务之间的同步,不能传递消息。类似于我们裸机程序中使用来做标记为的全局变量。
两个任务之间或者中断函数跟任务之间的同步功能,这个与事件标志组是类似的。其实就是共享资源为 1 的时候。
多个共享资源的管理。正在使用的资源有多少,信号量就减多少。
1.1.1 任务通知
任务通知的基础知识请参考博文:FreeRTOS记录(五、FreeRTOS任务通知)
在任务通知函数表格中,有如下介绍:
说明我们可以使用任务通知来实现二值信号量和计数信号量,后面我们会使用Demo说明问题。
1.1.2 二值信号量 Binary Semaphores
仅”空“(0)和”非空“(1)两种状态的信号量,信号量资源被获取了,信号量值就是 0,信号量资源被释放,信号量值就是 1。类似一个标志位,裸机中使用bool类型的全局变量TRUE
和FALSE
。
在CubeMX中可以直接设置,如下:
在freertos.c中:
创建信号量的函数为osSemaphoreCreate
,在程序中如果参数2 count 的值为1,即为穿件2值信号量,如果不为1,则可能是计数信号量(下文有说明),源码如下:
/******************** Semaphore Management Functions **************************/
#if (defined (osFeature_Semaphore) && (osFeature_Semaphore != 0))
/**
* @brief Create and Initialize a Semaphore object used for managing resources
* @param semaphore_def semaphore definition referenced with \ref osSemaphore.
* @param count number of available resources.
* @retval semaphore ID for reference by other functions or NULL in case of error.
* @note MUST REMAIN UNCHANGED: \b osSemaphoreCreate shall be consistent in every CMSIS-RTOS.
*/
osSemaphoreId osSemaphoreCreate (const osSemaphoreDef_t *semaphore_def, int32_t count)
{
#if( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
osSemaphoreId sema;
if (semaphore_def->controlblock != NULL){
if (count == 1) {
return xSemaphoreCreateBinaryStatic( semaphore_def->controlblock );
}
else {
#if (configUSE_COUNTING_SEMAPHORES == 1 )
return xSemaphoreCreateCountingStatic( count, count, semaphore_def->controlblock );
#else
return NULL;
#endif
}
}
else {
if (count == 1) {
vSemaphoreCreateBinary(sema);
return sema;
}
else {
#if (configUSE_COUNTING_SEMAPHORES == 1 )
return xSemaphoreCreateCounting(count, count);
#else
return NULL;
#endif
}
}
#elif ( configSUPPORT_STATIC_ALLOCATION == 1 ) // configSUPPORT_DYNAMIC_ALLOCATION == 0
if(count == 1) {
return xSemaphoreCreateBinaryStatic( semaphore_def->controlblock );
}
else
{
#if (configUSE_COUNTING_SEMAPHORES == 1 )
return xSemaphoreCreateCountingStatic( count, count, semaphore_def->controlblock );
#else
return NULL;
#endif
}
#else // configSUPPORT_STATIC_ALLOCATION == 0 && configSUPPORT_DYNAMIC_ALLOCATION == 1
osSemaphoreId sema;
if (count == 1) {
vSemaphoreCreateBinary(sema);
return sema;
}
else {
#if (configUSE_COUNTING_SEMAPHORES == 1 )
return xSemaphoreCreateCounting(count, count);
#else
return NULL;
#endif
}
#endif
}
1.1.3 计数信号量 Counting Semaphores
用来事件计数和资源管理,myCountingSem01Handle = osSemaphoreCreate(osSemaphore(myCountingSem01), 3);
上例中3是信号量的值,信号量值代表当前资源的可用数量,osSemaphoreRelease
(信号量+1)osSemaphoreWait
(信号量-1)。
优先级翻转:在很多场合中,某些资源只有一个,当低优先级任务正在占用该资源的时候,即便高优先级任务也只能乖乖的等待低优先级任务使用完该资源后释放资源。这里高优先级任务无法运行而低优先级任务可以运行的现象称为“优先级翻转”。
ex:低优先级任务获取信号量后,被中优先级打断,中优先级任务执行时间较长,因为低优先级任务还未释放信号量,高优先级任务就无法获取信号量继续运行
在CubeMX中如果需要使用,需要先使能:
然后才能创建:
在freertos.c中:
创建函数和二值信号量一样为osSemaphoreCreate
。
1.1.4 互斥信号量 Mutexes
拥有优先级继承的二值信号量,互斥量具有优先级继承机制,而信号量没有。
优先级继承:某个临界资源受到一个互斥量保护,如果这个资源正在被一个低优先级任务使用,那么此时的互斥量是闭锁状态,也代表了没有任务能申请到这个互斥量,如果此时一个高优先级任务想要对这个资源进行访问,去申请这个互斥量,那么高优先级任务会因为申请不到互斥量而进入阻塞态,那么系统会将现在持有该互斥量的任务的优先级临时提升到与高优先级任务的优先级相同,这个优先级提升的过程叫做优先级继承。
互斥量不能用于中断中:
- 互斥信号量有优先级继承的机制,所以只能用在任务中,不能用于中断服务函数。
- 中断服务函数中不能因为要等待互斥信号量而设置阻塞时间进入阻塞态。
在CubeMX中互斥量的设置如下:
在freertos.c中:
void MX_FREERTOS_Init(void) {
/* USER CODE BEGIN Init */
/* USER CODE END Init */
/* Create the mutex(es) */
/* definition and creation of myMutex01 */
osMutexDef(myMutex01);
myMutex01Handle = osMutexCreate(osMutex(myMutex01));
互斥量的相关API函数:
/**************************** Mutex Management ********************************/
/**
创建互斥量
* @brief Create and Initialize a Mutex object
* @param mutex_def mutex definition referenced with \ref osMutex.
* @retval mutex ID for reference by other functions or NULL in case of error.
* @note MUST REMAIN UNCHANGED: \b osMutexCreate shall be consistent in every CMSIS-RTOS.
*/
osMutexId osMutexCreate (const osMutexDef_t *mutex_def)
{
#if ( configUSE_MUTEXES == 1)
#if( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
if (mutex_def->controlblock != NULL) {
return xSemaphoreCreateMutexStatic( mutex_def->controlblock );
}
else {
return xSemaphoreCreateMutex();
}
#elif ( configSUPPORT_STATIC_ALLOCATION == 1 )
return xSemaphoreCreateMutexStatic( mutex_def->controlblock );
#else
return xSemaphoreCreateMutex();
#endif
#else
return NULL;
#endif
}
/*等待互斥量*/
osStatus osMutexWait (osMutexId mutex_id, uint32_t millisec)
/*释放互斥量*/
osStatus osMutexRelease (osMutexId mutex_id)
/*删除互斥量*/
osStatus osMutexDelete (osMutexId mutex_id)
关于递归互斥量, 递归互斥信号量解决死锁问题 :
MutexLock mutex;
void add()
{
mutex.lock();
// do something
mutex.unlock();
}
void count_all()
{
mutex.lock();
// do something
add();
mutex.unlock();
}
1.1.5 事件标志组 Events
事件是一种实现任务间通信的机制,主要用于实现多任务间的同步,但事件通信只能是事件类型的通信,无数据传输。与信号量不同的是,它可以实现一对多,多对多的同步。 即一个任务可以等待多个事件的发生:可以是任意一个事件发生时唤醒任务进行事件处理;也可以是几个事件都发生后才唤醒任务进行事件处理。同样,也可以是多个任务同步多个事件。
在程序中使用configUSE_16_BIT_TICKS
这个宏定义来确定用户可以使用的事件标志位是8个还是24个:
事件标志组 就是 裸机中的标志位,裸机中需要对每一个需要判断的事件自行定义一个标志位,然后需要用户自行管理,而事件标志组,把这个功能集中起来方便管理,用户可以使用24个(configUSE_16_BIT_TICKS == 1)事件管理位。
在CubeMX中,好像对事件标志组的支持不太完善:
我两台电脑上的CubeMX版本有一个有 Events选项,另一个没有。这里不用在意,我们在生成的工程代码中,可以看到有对应的事件组标志的文件:
在文件中,我们能够了解到和事件组有关的API函数:
注意中断中设置事件组xEventGroupSetBitsFromISR
的注释,后面会有应用Demo
/*事件标志组句柄*/
typedef void * EventGroupHandle_t;
/*创建事件标志组,动态*/
#if( configSUPPORT_DYNAMIC_ALLOCATION == 1 )
EventGroupHandle_t xEventGroupCreate( void ) PRIVILEGED_FUNCTION;
#endif
/*创建事件标志组,静态*/
#if( configSUPPORT_STATIC_ALLOCATION == 1 )
EventGroupHandle_t xEventGroupCreateStatic( StaticEventGroup_t *pxEventGroupBuffer ) PRIVILEGED_FUNCTION;
#endif
/*等待事件标志位,可以在阻塞状态下等待一个或者多个事件位*/
EventBits_t xEventGroupWaitBits( EventGroupHandle_t xEventGroup, //事件组名称
const EventBits_t uxBitsToWaitFor, //等待的位
const BaseType_t xClearOnExit, //读取之后是否清除,pdFALSE不清除,pdTRUE 清除
const BaseType_t xWaitForAllBits, //等待的位是与还是或,pdTRUE,则要等待全部的位都置1
TickType_t xTicksToWait ) //阻塞时间
/*清除标志位*/
EventBits_t xEventGroupClearBits( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToClear )
/*中断中清除标志位*/
BaseType_t xEventGroupClearBitsFromISR( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet )
/*设置标志位*/
EventBits_t xEventGroupSetBits( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet )
/*
中断中设置标志位
标记退出此函数以后是否进行任务切换,这个变量的值函数会自 动设置的,用户不用进行设置,
用户只需要提供一个变量来保存 这个值就行了。
当此值为 pdTRUE 的时候在退出中断服务函数之 前一定要进行一次任务切换。
返回值:
pdPASS : 事件位置 1 成功。
pdFALSE: 事件位置 1 失败。
*/
BaseType_t xEventGroupSetBitsFromISR( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet,
BaseType_t *pxHigherPriorityTaskWoken )
/*
多个事件的同步,在文件中官方给出了示例,需要了解请参考
任务A接收事件,将事件所需的一些处理委托给任务B、任务C、任务D三个任务,如果任务A在其他三个任务没有完成当前事件的处理时无法接收下一个事件,此时四个任务就需要彼此同步。每个任务执行到同步点后将在此等待其他任务完成处理并到达相应的同步点后才能继续执行,如此处的任务A只能在其他任务都达到同步点后才能接收另一个事件
必须为每个参与同步的任务分配唯一的事件位。
每个任务在到达同步点时设置自己的事件位。
设置自己的事件位后,事件组上的每个任务都会阻塞,以等待代表其他同步任务的事件位被设置。
*/
EventBits_t xEventGroupSync( EventGroupHandle_t xEventGroup,
const EventBits_t uxBitsToSet,
const EventBits_t uxBitsToWaitFor,
TickType_t xTicksToWait )
#define xEventGroupGetBits( xEventGroup ) xEventGroupClearBits( xEventGroup, 0 )
EventBits_t xEventGroupGetBitsFromISR( EventGroupHandle_t xEventGroup )
void vEventGroupDelete( EventGroupHandle_t xEventGroup )
1.2 Demo测试
1.2.1 二值信号量
从二值信号量开始测试,逻辑: 在按键按下的时候释放信号量,在THread
任务中等待信号量(阻塞方式),等到了就读取一次温湿度打印出来:
按键函数中,使用osSemaphoreRelease
释放信号量:
在温湿度读取任务中,使用osSemaphoreWait
一直等待信号量,收到信号量就执行一次温湿度读取,这里我完全是和任务通知使用的同样的方式:
测试结果:
可以看到,在使用一个任务叫醒另外一个任务的时候,任务通知和二值信号量是一样的,我们在前面说过任务通知更加高效,占用RAM空间更小,所以这种情况,任务通知是有优势的。
1.2.2 计数信号量
计数信号量的测试和以上一样,通过按键释放计数信号量,但是在测试过程中,其实发现计数信号量的使用不应该是这样,计数信号量典型的应用场合就是停车位模型,总共有多少个车位,就是多少个信号量,入口进入一辆车信号量-1,出口离开一辆车信号量+1,这里我依然把这么使用的程序和结果放出来,做一个参考。
测试时候发现(有待分析):信号量在创建的时候就已经存在了,如果某一个任务等待信号量使用阻塞方式osWaitForever
,那么在程序运行开始的时候,就会直接接受到该信号量,如果是计数信号量,那么创建的时候有多少个数值,那么等待信号量的任务就会运行多少次
在KeyTask中:
读取温湿度任务:
测试结果:
1.2.3 互斥信号量(优先级继承)
互斥信号量我们做一个 优先级继承的测试。
新建两个任务作为测试任务:
在KeyTask中:
在两个测试任务中:
测试结果:
接下来,我们加入互斥信号量,在按键中需要获得互斥量,然后释放,然后再使用优先级低的测试任务获得互斥量,然后释放。理论上来说,因为有高优先级的任务(按键任务)一直需要等待互斥量,所以在进行测试任务1 和测试任务2调度的时候,会先运行 获得互斥量的原本优先级低的测试任务,具体过程如下。
在KeyTask中:
在低优先级的测试任务中:
测试结果如下:
从上图结果和前面结果可以看出,testtask01的优先级继承了 高优先级的按键任务的优先级。
1.2.4 事件标志组
我们测试使用几个事件标志组来触发温湿度读取的任务:
(记录使用的两台电脑用的平台不一样,一个是STM32F103,一个是STM32L051,所以细心的朋友能够看出有些区别,但是整体上我还是尽量做到一致)
1、定时器定时10S以后发送一个标志位;
2、按钮按下发送一个标志位;
3、收到一个无线报文发送一个标志位;
先定义一下事件组的位:
创建事件标志组:
在KeyTask中 (任务中设置标志位):
在无线报文接收任务中(任务中设置标志位)::
在定时器中断函数中 (中断中设置标志位):
但是注意,需要使用xEventGroupSetBitsFromISR
函数需要如下的宏定义:
上面的3个宏定义可以自己在config里面添加,也可以在CubeMX里面设置,下面给出在CubeMX中对应的设置:
读取温湿度任务:
测试结果:
我们再修改一下接收事件任务中xEventGroupWaitBits
的参数,使得需要等到所有的事件标志位置位才能执行:
在温湿度读取任务最后加上一个判断语句:
测试结果:
二、消息队列与邮箱
任务与任务之间的消息传递,可以使用消息队列,邮箱。任务通知也是可以的(下一节)。
消息队列的使用在我上一篇博文中有过使用示例:FreeRTOS记录(六、FreeRTOS消息队列—Enocean模块串口通讯、RAM空间不足问题分析)
2.1 邮箱
邮箱可以指定发送对象,也可以全部task访问,队列则是全部task都可以访问。
邮箱只能发送一条消息,所以可以认为邮箱就是长度为1的消息队列。消息邮箱一次只能发一条会覆盖原来消息。
邮箱队列控制块结构体如下:
typedef struct os_mailQ_cb {
const osMailQDef_t *queue_def;
QueueHandle_t handle;
osPoolId pool;
} os_mailQ_cb_t;
在CubeMX中好像不能直接生成邮箱的程序
在cmsis_os.c
中找到关于邮箱的函数:
创建osMailCreate
,发送osMailPut
,接收osMailGet
,分别如下:
/**
* @brief Create and Initialize mail queue
* @param queue_def reference to the mail queue definition obtain with \ref osMailQ
* @param thread_id thread ID (obtained by \ref osThreadCreate or \ref osThreadGetId) or NULL.
* @retval mail queue ID for reference by other functions or NULL in case of error.
* @note MUST REMAIN UNCHANGED: \b osMailCreate shall be consistent in every CMSIS-RTOS.
*/
osMailQId osMailCreate (const osMailQDef_t *queue_def, osThreadId thread_id)
{
#if (configSUPPORT_DYNAMIC_ALLOCATION == 1)
(void) thread_id;
osPoolDef_t pool_def = {queue_def->queue_sz, queue_def->item_sz, NULL};
/* Create a mail queue control block */
*(queue_def->cb) = pvPortMalloc(sizeof(struct os_mailQ_cb));
if (*(queue_def->cb) == NULL) {
return NULL;
}
(*(queue_def->cb))->queue_def = queue_def;
/* Create a queue in FreeRTOS */
(*(queue_def->cb))->handle = xQueueCreate(queue_def->queue_sz, sizeof(void *));
if ((*(queue_def->cb))->handle == NULL) {
vPortFree(*(queue_def->cb));
return NULL;
}
/* Create a mail pool */
(*(queue_def->cb))->pool = osPoolCreate(&pool_def);
if ((*(queue_def->cb))->pool == NULL) {
//TODO: Delete queue. How to do it in FreeRTOS?
vPortFree(*(queue_def->cb));
return NULL;
}
return *(queue_def->cb);
#else
return NULL;
#endif
}
...
osStatus osMailPut (osMailQId queue_id, void *mail){...}
...
osEvent osMailGet (osMailQId queue_id, uint32_t millisec){...}
...
2.2 邮箱Demo
我们在程序中定义一个邮箱消息,然后通过按钮发送,还是通过温湿度读取任务接收,因为不能自动创建,我们需要自己创建,从上面内容我们知道邮箱和消息队列类似,所以我们根据消息队列的定义,来试着定义一个邮箱。
消息队列的定义
/* Create the queue(s) */
/* definition and creation of myQueue01 */
osMessageQDef(myQueue01, 16, uint8_t);
myQueue01Handle = osMessageCreate(osMessageQ(myQueue01), NULL);
邮箱的定义
结合消息队列的定义,再根据下图我们可以试着定义一个邮箱
定义如下:
2.2.1 基本测试
在KeyTask中:
在温湿度读取任务中:
测试结果:
2.2.1 问题测试
说明,上面的例子定义邮箱的消息类型为uint8_t
,而我发送的邮箱消息为0x1234
,但是还是能够正常的接收和发送,后来我把定义改成 osMailQDef(myMail01,1,uint8_t);
长度改为1,还是能够正常接收发送0x1234
,说明我对里对邮箱的使用和理解还是不到位,对于uint8_t
类型能发0x1234 暂时没有深入研究,为了多了解一点邮箱中这个长度的意思,我继续做了测试
测试一:
在KeyTask中:
在温湿度读取任务中(这里是为了验证邮箱,所以打印出邮箱内容):
测试一结果:
测试二:
测试二的KeyTask中的代码和温湿度读取任务中的代码不变,只变了一个地方osMailQDef(myMail01,1,uint8_t);
Mail的长度变为1。
测试二结果:
测试三:
在KeyTask中 for 循环前后加入临界区保护:
测试三结果:
经过这几个测试,我们对Cubem中邮箱定义的这个长度有了清楚的认识,
同时也知道了邮箱发送的时候会产生任务调度,这个在邮箱发送函数BaseType_t xQueueGenericSend( QueueHandle_t xQueue, const void * const pvItemToQueue, TickType_t xTicksToWait, const BaseType_t xCopyPosition )
源码中也能看出来到确实会发生调度。
当然,除了长度这个参数,第三个类型这个参数才是邮箱和消息队列更加核心和有意思的地方,除了基本的数据类型,我们还可以自定义一个结构体,我们把类型定义为我们定义的结构体,比如:
/* add queues, ... */
typedef struct
{
uint8_t value1;
uint32_t value2;
uint16_t value3;
uint8 *addr1;
}My_Mail;
osMailQDef(myMail01,16,My_Mail);
myMail01Handle = osMailCreate(osMailQ(myMail01),NULL);
后续如果遇到使用再来更新。
三、任务通知与消息队列
通过前面的博文介绍,消息队列和邮箱我们都知道,可以传递消息值,任务通知我们也已经测试过了,关键在FreeRTOS 的每个任务都有一个 32 位的通知值pxTCB->ulNotifiedValue
。
我们把这个通知值当做一个需要传递的消息值,那么他就是长度为1 的消息队列了。这个很好理解。
我们也可以把这个通知值变成一个地址,传入一个数组地址,就等于通过任务通知传送了一串消息。
下面举个例子做个测试。
3.1 任务通知发送消息Demo
新建一个字符串数组测试:
在KeyTask中使用osSignalSet
将数组mystring
的地址当做任务通知发送给THread
任务:
在THread
任务中把字符串打印出来:
上面例子中的mystring
是数组,所以mystring
的值和&mystring
值是一样的(C语言的基础知识)。
测试结果如下:
起初看到mystring
地址为0x20000000
还以为出错了,这不是RAM的起始地址吗?后来测试了一下,同时看了一下.map文件,确实没错: