一、信号量的概念
1、信号量的基本概念
消息队列是实现任务与任务或任务与中断间通信的数据结构,可类比裸机编程中的数组
信号量是实现任务与任务或任务与中断间通信的机制,可以类比裸机编程中的标志位
信号量 (semaphore) 可以实现任务与任务或任务与中断间的同步功能 (二值信号量)、资源管理(计数信号量)、临界资源的互斥访问(互斥信号量) 等
信号量是一个非负正数,二值信号量与互斥信号量取值范围为 0-1,计数信号量取值范围是 0-N(N>1)
0: 信号量为空,所有试图获取它的任务都将处于阻塞状态,直到超时退出或其他任务释放信号量
正数: 表示有一个或多个信号量供获取
2、信号量的分类
- 二值信号量 (重点讲解同步应用)
- 技术信号量 (重点讲解资源管理)
- 互斥信号量 (重点讲解互斥访问)
- 递归互斥信号量 (简要了解即可)
二、计数信号量的定义与应用
1、计数信号量的定义
取值只有 0 与 1 两种状态的信号量称之为二值信号量
取值大于 1 的信号量称之为计数信号量
Note:计数信号量的取值也可以为 1,但通常大于 1,如果取值为 1,相当于只有 0 与 1 两种状态,用二值信号量即可。
创建计数信号量时,系统会为创建的计数信号量分配内存,计数信号量创建完成后的示意图如下:
从上图可以看出,计数信号量是一种长度大于 1,消息大小为 0 的特殊消息队列。
因为这个队列的消息大小为 0,因此在运用时,只需要知道队列中是否有消息即可,而无需关注消息是什么。
2、计数信号量的应用
在嵌入式操作系统中,计数信号量是资源管理的重要手段,主要用于任务与任务间。
应用场景:
计数信号量允许多个任务对其进行操作,但限制了任务的数量。比如有一个停车场,里面只有 50 个车位,那么只能停 50 辆车,相当于我们的信号量有 50 个。假如一开始停车场的车位还有 50 个,那么每进去一辆车就要消耗一个停车位,车位的数量就要减 1,相应地,我们的信号量在使用之后也需要减 1。当停车场停满了 50 辆车时,此时的停车位数量为 0,再来的车就不能停进去了,否则将没法停车了,也相当于我们的信号量为 0,后面的任务对这个停车场资源的访问也无法进行。当有车从停车场离开时,车位又空余出来了,那么后面的车就能停进去了。信号量操作也是一样的,当我们释放了这个资源,后面的任务才能对这个资源进行访问。
三、计数信号量的运作机制
FreeRTOS 任务间计数信号量的实现
任务间信号量的实现是指各个任务之间使用信号量实现任务的同步或者资源共享功能。下面我们通过如下的框图来说明一下 FreeRTOS 计数信号量的实现,让大家有一个形象的认识。
运行条件:
- 创建 任务 Task1 和 Task2 至 N。
- 创建计数信号量可用资源为 N。
运行过程描述如下:
-
任务 Task1 运行过程中调用函数 xSemaphoreTake 获取信号量资源,如果信号量大于 0,Task1 将直接获取资源。如果信号量为 0,任务 Task1 将由运行态转到阻塞状态,等待资源可用。一旦获取了资源并使用完毕后会通过函数 xSemaphoreGive 释放掉资源。
-
任务 Task2 至 N 运行过程中调用函数 xSemaphoreTake 获取信号量资源,如果信号量大于 0,Task2 至 N 将直接获取资源。如果信号量为 0,任务 Task2 至 N 将由运行态转到阻塞状态,等待资源可用。一旦获取了资源并使用完毕后会通过函数 xSemaphoreGive 释放掉资源。
上面就是一个简单的 FreeRTOS 任务间计数信号量的使用过程。
四、计数信号量常用的 API 函数
1、使用计数信号量的典型流程如下:
- 创建计数信号量
- 释放计数信号量
- 获取计数信号量
- 删除计数信号量
2、常用 API 函数如下:
- xSemaphoreCreateCounting()
- xSemaphoreGive() 与 xSemaphoreGiveFromISR()
- xSemaphoreTake()
- vSemaphoreDelete()
3、二值信号量创建与删除
二值信号量控制块 (句柄)
如下图:计数信号量的句柄为消息队列的句柄,因为计数信号量是一种长度大于 1,消息大小为 0 的特殊消息队列
计数信号量创建
函数原型:
SemaphoreHandle_t xSemaphoreCreateCounting(UBaseType_t uxMaxCount, /* 支持的最大计数值 */ UBaseType_t uxInitialCount); /* 初始计数值 */
函数描述:
函数 xSemaphoreCreateCounting 用于创建计数信号量。
- 第 1 个参数是设置此计数信号量支持的最大计数值。
- 第 2 个参数是设置计数信号量的初始值。
- 返回值,如果创建成功会返回消息队列的句柄,如果由于 FreeRTOSConfig.h 文件中 heap 大小不足,无法为此消息队列提供所需的空间会返回 NULL
说明:此函数基于消息队列函数实现:
应用举例:
计数信号量删除
函数原型:
void vSemaphoreDelete(void)
函数描述:
函数 vSemaphoreDelete 可用于删除计数信号量。
4、任务中计数信号量释放
函数原型:
xSemaphoreGive(SemaphoreHandle_t xSemaphore); /* 信号量句柄 */
函数描述:
函数 xSemaphoreGive 用于在任务代码中释放信号量。
- 第 1 个参数是信号量句柄。
- 返回值,如果信号量释放成功返回 pdTRUE,否则返回 pdFALSE,因为信号量的实现是基于消息队列,返回失败的主要原因是消息队列已经满了。
使用这个函数要注意以下问题:
- 此函数是用于任务代码中调用的,故不可以在中断服务程序中调用此函数,中断服务程序中使用的是 xSemaphoreGiveFromISR。
- 使用此函数前,一定要保证用函数 xSemaphoreCreateBinary(), xSemaphoreCreateMutex() 或者 xSemaphoreCreateCounting() 创建了信号量。
- 此函数不支持使用 xSemaphoreCreateRecursiveMutex() 创建的信号量。
应用举例:
5、中断中计数信号量释放
函数原型:
xSemaphoreGiveFromISR ( SemaphoreHandle_t xSemaphore, /* 信号量句柄 */signed BaseType_t *pxHigherPriorityTaskWoken /* 高优先级任务是否被唤醒的状态保存 */ )
函数描述:
函数 xSemaphoreGiveFromISR 用于中断服务程序中释放信号量。
- 第 1 个参数是信号量句柄。
- 第 2 个参数用于保存是否有高优先级任务准备就绪。如果函数执行完毕后,此参数的数值是 pdTRUE,说明有高优先级任务要执行,否则没有。
- 返回值,如果信号量释放成功返回 pdTRUE,否则返回 errQUEUE_FULL。
使用这个函数要注意以下问题:
- 此函数是基于消息队列函数 xQueueGiveFromISR 实现的:#define xSemaphoreGiveFromISR(xSemaphore, pxHigherPriorityTaskWoken) \xQueueGiveFromISR( ( QueueHandle_t ) ( xSemaphore ), ( pxHigherPriorityTaskWoken ) )
- 此函数是用于中断服务程序中调用的,故不可以任务代码中调用此函数,任务代码中中使用的是 xSemaphoreGive。
- 使用此函数前,一定要保证用函数 xSemaphoreCreateBinary() 或者 xSemaphoreCreateCounting() 创建了信号量。
- 此函数不支持使用 xSemaphoreCreateMutex () 创建的信号量。
6、计数信号量获取
函数原型:
xSemaphoreTake( SemaphoreHandle_t xSemaphore, /* 信号量句柄 */ TickType_t xTicksToWait ); /* 等待信号量可用的最大等待时间 */
函数描述:
函数 xSemaphoreTake 用于在任务代码中获取信号量。
- 第 1 个参数是信号量句柄。
- 第 2 个参数是没有信号量可用时,等待信号量可用的最大等待时间,单位系统时钟节拍。
- 返回值,如果创建成功会获取信号量返回 pdTRUE,否则返回 pdFALSE。
使用这个函数要注意以下问题:
- 此函数是用于任务代码中调用的,故不可以在中断服务程序中调用此函数,中断服务程序使用的是 xSemaphoreTakeFromISR。
- 如果消息队列为空且第 2 个参数为 0,那么此函数会立即返回。
- 如果用户将 FreeRTOSConfig.h 文件中的宏定义 INCLUDE_vTaskSuspend 配置为 1 且第 2 个参数配置为 portMAX_DELAY,那么此函数会永久等待直到信号量可用。
应用举例:
五、技术信号量的应用编程
视频讲解
串口输出信息:
STM32cubeMX 配置:
代码: