目录
3.1 二值信号量创建函数 xSemaphoreCreateBinary()
3.2 计数信号量创建函数 xSemaphoreCreateCounting()
3.3 信号量删除函数 vSemaphoreDelete()
一. 前言
大家好,我是旭辉君,一个智能硬件领域深度探索的技术博主。
在上篇文章中,我们理解了在FreeRTOS中如何使用消息队列进行任务间的数据传递,链接如下:
FreeRTOS系列教程(三):如何使用消息队列-CSDN博客文章浏览阅读689次,点赞20次,收藏23次。本文主要探索了消息队列的原理及其使用方法,包括队列的数据特征,消息队列的运行与阻塞机制,消息队列API函数,以及消息队列使用实验等。看完本文,我们将会知道:如何创建消息队列?如何发送数据到队列?如何从队列接收数据?消息队列运行的原理是什么?消息队列的阻塞机制是什么?队列的数据特征是什么?
https://blog.csdn.net/weixin_42434952/article/details/138004790?spm=1001.2014.3001.5501本文我们就一起来探索信号量的使用。所谓信号量,可以简单的理解为就是一个状态标志,我们可以用这个状态标志来进行任务间的同步,有序访问,或者互斥访问。从这些对于信号量不同的应用,常用的信号量可以分为:
- 二值信号量:只有两个状态值0和1的状态标记,常用来作为任务间的同步。
- 计数信号量:有多个状态值的状态标记,多个状态值可用来作为一定数目任务对数据的访问。
- 互斥信号量:具有优先级继承机制,保证同一时刻只有一个任务能访问限定资源。
本文我们将重点讲述二值信号量与计数信号量的使用。互斥·信号量在下一篇文章讲解。通过本文,我们将会知道:
- 如何创建和使用二值信号量?
- 如何创建和使用计数信号量?
- 信号量与消息队列有什么异同?
- 为什么要使用信号量?
- 信号量的运行机制是什么?
接下来让我们一起,进入信号量的探索之旅!
二. 信号量的原理
2.1 信号量的引入
如前文所述,信号量可以提供任务间数据的同步机制。我们假设有两个任务TaskA和TaskB,其中TaskB等待TaskA产生的数据并进行处理,按照之前我们在裸机编程时候的思路,一般都是设置一个全局变量,然后在while1中轮流执行这两个任务,若TaskA产生的数据让这个全局变量发生改变,TaskB在轮询到之后就能处理这些数据,但是,如果TaskA里面的数据久久不发生改变,那么一直轮询TaskB就是无效的,CPU做了许多的无用功。
所以应该怎么优化呢?
假若在TaskA数据发生不改变的时候,TaskB进入阻塞态不执行,当TaskA数据发生改变的时候才去执行TaskB,这样就不会啊浪费CPU的资源。为此,FreeRTOS引入了信号量(Semaphore)概念,通过信号量的同步机制可以使任务在数据还没到达的时候进入阻塞状态,在数据到达之后才得以执行,提高系统资源利用率。
二进制信号量只有两个状态,只能用于两个任务间的同步;计数信号量中信号量的数目可以自定义设定为多个,可用于多个任务间的同步。
2.2 信号量的运行机制
2.2.1 二值信号量运行机制
创建信号量时, 系统会为创建的信号量对象分配内存, 二值信号量的最大可用信号量个数为 1。
创建成功后,任何任务都可以从创建的二值信号量资源中获取这个二值信号量,获取成功则任务继续运行, 否则任务会根据用户指定的阻塞超时时间来等待其它任务或者中断释放信号量。 在等待这段时间,系统将任务变成阻塞态, 任务将被挂到该信号量的阻塞等待列表中。
下图为任务获取信号量时的示意图:获取信号量无效时任务进入阻塞,其他任务释放信号量后,信号量有效,该任务恢复为就绪态。


2.2.2 计数信号量运行机制
相比于二值信号量,计数信号量允许多个任务获取同一个信号量,这多个任务的数目可以由我们设定。比如我们设定,某个资源只能有 3 个任务访问,那么第 4 个任务访问的时候,会因为获取不到信号量而进入阻塞,等到有任务(比如任务 1)释放掉该资源的时候,第 4 个任务才能获取到信号量从而进行资源的访问,其运作的机制具体见下图:

2.3 信号量与消息队列的异同
观察信号量控制块结构体以及信号量创建函数的源码,我们就会惊奇的发现:FreeRTOS 的信号量控制块结构体与消息队列结构体是一模一样的!信 号 量 的 创 建 实 际 调用 的 函 数
xQueueGenericCreate()也与消息队列一样!只是参数或者其代表的意义有一些差异。
所以我们可以理解为:信号量就是一种特殊的消息队列!由于我们只关注信号状态,不关注消息内容,这个队列就没有设置消息存储空间。
三. 信号量的相关API函数
3.1 二值信号量创建函数 xSemaphoreCreateBinary()
SemaphoreHandle_t xSemaphoreCreateBinary( void );
其中,
| 返回值 | 有两种可能的返回值: 1,NULL: 创建信号量失败, 可能因为堆栈 不足。 2,其他值:信号量已成功创建。 返回值是一个句柄,通过该句柄可以引用信号量。 |
其实,xSemaphoreCreateBinary()是一个宏定义,展开后调用xQueueGenericCreate(),也就是上一篇文章我们创建队列时候使用的函数,只是传递的参数不同。
#define xSemaphoreCreateBinary() xQueueGenericCreate( ( UBaseType_t ) 1, semSEMAPHORE_QUEUE_ITEM_LENGTH, queueQUEUE_TYPE_BINARY_SEMAPHORE )
二值信号量创建成功后,其在内存中的排布图示如下,对比上一篇文章,创建队列后在内存的图示,我们可以看到,由于没有了存储消息内容的空间,信号量创建后其pcHead和pcTail的指针指向起始地址。因为在信号量里,我们并不注重消息内容是什么,我们只注重有没有消息就行了。

3.2 计数信号量创建函数 xSemaphoreCreateCounting()
SemaphoreHandle_t xSemaphoreCreateCounting( UBaseType_t uxMaxCount,
UBaseType_t uxInitialCount);
其中,
| uxMaxCount | 计数信号量的最大值。 当信号量达到此值时,信号就不能再被释放。 |
| uxInitialCount | 创建信号量时分配给信号量的初始值。 |
| 返回值 | 如果已成功创建信号量,则将返回该信号量的句柄 。 如果无法分配RAM 而无法创建信号量, 则会返回 NULL。 |
与二值信号量一样,xSemaphoreCreateCounting()展开后也是调用xQueueGenericCreate(),创建的计数信号量只有消息队列控制块结构体存储空间而没有消息存储空间 。
3.3 信号量删除函数 vSemaphoreDelete()
void vSemaphoreDelete( SemaphoreHandle_t xSemaphore );
其中,
| xSemaphore | 被删除的信号量的句柄 |
删除信号量函数vSemaphoreDelete()是一个宏定义,其调用的是vQueueDelete()函数。删除信号量过程其实就是删除消息队列过程, 因为信号量其实就是特殊的,无法存储消息的消息队列。
3.4 信号量释放函数 xSemaphoreGive()
xSemaphoreGive( SemaphoreHandle_t xSemaphore );
其中,
| xSemaphore | 要释放的信号量的句柄,也就是创建信号量时返回的句柄。 |
| 返回值 | 如果信号量释放成功,则返回 pdPASS; 如果发生错误,则返回 errQUEUE_FULL。信号量的实现基于队列,信号量释放时,如果队列上没有空间,那么就会发生错误。 |
xSemaphoreGive()是一个用于释放信号量的宏, 真正的实现该过程是调用消息队列通用发送函数xQueueGenericSend()。释放信号量实际上是一次入队操作,并且阻塞时间为0,也就是释放信号量时,如果信号量计数值已满,就返回信号量释放错误。
3.5 信号量获取函数 xSemaphoreTake()
xSemaphoreTake( SemaphoreHandle_t xSemaphore,
TickType_t xTicksToWait );
其中,
| xSemaphore | 要获取的信号量的句柄 |
| xTicksToWait | 等待信号量变为可用的时间(以滴答为单位)。将阻塞时间指定为 portMAX_DELAY 会导致任务无限期地阻塞(没有超时)。 |
| 返回值 | 如果获得信号量,则返回 pdTRUE; 如果在指定的超时时间内没有获取成功则返回errQUEUE_EMPTY |
xSemaphoreTake()是一个用于获取信号量的宏, 真正的实现该过程是调用消息队列通用接收函数xQueueGenericReceive()。信号量获取实际上就是一次消息出队操作,所以我们也可以按照消息队列的接收机制来理解信号量的获取:当有任务试图获取信号量的时候,当且仅当信号量有效,也就是队列中存在可用信号量的时候,任务才能获取到信号量。如果信号量无效,在用户指定的阻塞超时时间中,该任务将保持阻塞状态以等待信号量有效。在阻塞超时等待的时间内,如果有其它任务或中断释放了有效的信号量,该任务将自动由阻塞态转移为就绪态。如果任务等待的时间超过了指定的阻塞时间,即使信号量中还是没有可用信号量,任务也会自动从阻塞态转移为就绪态。
四. 信号量的相关实验
4.1 二值信号量实验
创建三个任务,task_example_1,task_example_2和task_example_3。其中task_example_1用于计时,每3s让task_example_2释放二值信号量,task_example_3用于信号量获取,在获取不到信号量的时候一直死等。主体代码如下:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "driver/gpio.h"
#include "freertos/semphr.h"
#define QUEUE_LEN 5 /* 队列的长度,最大可包含多少个消息 */
#define QUEUE_SIZE (sizeof(Data_t)) /* 队列中每个消息大小(字节) */
TaskHandle_t task_example_1_handle = NULL;
TaskHandle_t task_example_2_handle = NULL;
TaskHandle_t task_example_3_handle = NULL;
QueueHandle_t Test_Queue =NULL;
SemaphoreHandle_t BinarySem_Handle =NULL;
TickType_t xWakeTime_task1;
TickType_t xWakeTime_task2;
TickType_t xWakeTime_task3;
static int32_t cnt = 0;
static void task_example_1(void* arg)
{
while (1)
{
cnt ++;
printf("cnt: %d\n", cnt);
vTaskDelay(1000 / portTICK_RATE_MS); /* 延时1s */
}
}
static void task_example_2(void* arg)
{
BaseType_t xReturn = pdPASS; /* 定义一个创建信息返回值,默认为pdPASS */
while (1)
{
if( cnt % 3 == 0)
{
xReturn = xSemaphoreGive( BinarySem_Handle );//释放二值信号量
if( xReturn == pdTRUE ) printf("BinarySem_Handle二值信号量释放成功!\r\n");
else printf("BinarySem_Handle二值信号量释放失败!\r\n");
}
vTaskDelay(1000 / portTICK_RATE_MS);
}
}
static void task_example_3(void* arg)
{
BaseType_t xReturn = pdPASS;
while (1)
{
xReturn = xSemaphoreTake(BinarySem_Handle,/* 二值信号量句柄 */
portMAX_DELAY); /* 等待时间 */
if(pdTRUE == xReturn) printf("BinarySem_Handle二值信号量获取成功!\n\n");
else printf("BinarySem_Handle二值信号量获取失败!\r\n");
}
}
void app_main(void)
{
BaseType_t xReturn;
/* 创建 BinarySem */
BinarySem_Handle = xSemaphoreCreateBinary();
if(NULL != BinarySem_Handle)
printf("BinarySem_Handle二值信号量创建成功!\r\n");
xWakeTime_task1 = xTaskGetTickCount();
printf("app_main is Running at %d\r\n",xWakeTime_task1);
//start task
xReturn = xTaskCreate(task_example_1, "task_example_1", 2048, NULL, 3, &task_example_1_handle);
if(xReturn == pdPASS) printf("创建 task_example_1 任务成功!\r\n");
else printf("创建 task_example_1 任务失败!\r\n");
xReturn = xTaskCreate(task_example_2, "task_example_2", 2048, NULL, 2, &task_example_2_handle);
if(xReturn == pdPASS) printf("创建 task_example_2 任务成功!\r\n");
else printf("创建 task_example_2 任务失败!\r\n");
xReturn = xTaskCreate(task_example_3, "task_example_3", 2048, NULL, 1, &task_example_3_handle);
if(xReturn == pdPASS) printf("创建 task_example_3 任务成功!\r\n");
else printf("创建 task_example_3 任务失败!\r\n");
printf("Minimum free heapconfigMINIMAL_STACK_SIZE size: %d bytes\n", esp_get_minimum_free_heap_size());
}
下图为运行后的串口输出结果:可以看到,cnt计数的时候,每3s释放一个二值信号量,然后立即就能被获取到,实现了Task2与Task3两个任务间的同步。

4.2 计数信号量实验
创建计数信号量,xSemaphoreCreateCounting(5,3),表示最大值为5,最大可容纳五个状态,任务每获取一个信号量,信号量计数减一,每释放一个信号量,信号量计数加一。初始值为3,表示初始里面已经有了三个信号量。
同样三个任务,task_example_3每500ms获取一个信号量, task_example_2每2s释放一个信号量,task_example_1用于每1s的计时显示。程序主体代码如下:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "driver/gpio.h"
#include "freertos/semphr.h"
TaskHandle_t task_example_1_handle = NULL;
TaskHandle_t task_example_2_handle = NULL;
TaskHandle_t task_example_3_handle = NULL;
QueueHandle_t Test_Queue =NULL;
SemaphoreHandle_t CountSem_Handle =NULL;
TickType_t xWakeTime_task1;
TickType_t xWakeTime_task2;
TickType_t xWakeTime_task3;
static int32_t cnt = 0;
static void task_example_1(void* arg)
{
while (1)
{
cnt ++;
printf("cnt: %d\n", cnt);
vTaskDelay(1000 / portTICK_RATE_MS); /* 延时1s */
}
}
static void task_example_2(void* arg)
{
BaseType_t xReturn = pdPASS; /* 定义一个创建信息返回值,默认为pdPASS */
while (1)
{
xReturn = xSemaphoreGive( CountSem_Handle );//释放二值信号量,入队
if( xReturn == pdTRUE ) printf("CountSem_Handle计数信号量释放一个成功!\r\n");
else printf("CountSem_Handle计数信号量释放失败!\r\n");
vTaskDelay(2000 / portTICK_RATE_MS);
}
}
static void task_example_3(void* arg)
{
BaseType_t xReturn = pdPASS;
while (1)
{
xReturn = xSemaphoreTake(CountSem_Handle,/* 计数信号量句柄 */
0); /* 等待时间 */
if(pdTRUE == xReturn) printf("CountSem_Handle计数信号量获取成功!\n\n");
else printf("CountSem_Handle计数信号量获取失败!\r\n");
printf("信号量的计数值为:%d\r\n",(int)uxSemaphoreGetCount(CountSem_Handle));
vTaskDelay(500 / portTICK_RATE_MS);
}
}
void app_main(void)
{
BaseType_t xReturn;
/* 创建 BinarySem */
CountSem_Handle = xSemaphoreCreateCounting(5,3);/* 创建计数信号量,最大为5,起始为3 */
if(NULL != CountSem_Handle)
printf("CountSem_Handle计数信号量创建成功!\r\n");
xWakeTime_task1 = xTaskGetTickCount();
printf("app_main is Running at %d\r\n",xWakeTime_task1);
//start task
xReturn = xTaskCreate(task_example_1, "task_example_1", 2048, NULL, 3, &task_example_1_handle);
if(xReturn == pdPASS) printf("创建 task_example_1 任务成功!\r\n");
else printf("创建 task_example_1 任务失败!\r\n");
xReturn = xTaskCreate(task_example_2, "task_example_2", 2048, NULL, 2, &task_example_2_handle);
if(xReturn == pdPASS) printf("创建 task_example_2 任务成功!\r\n");
else printf("创建 task_example_2 任务失败!\r\n");
xReturn = xTaskCreate(task_example_3, "task_example_3", 2048, NULL, 1, &task_example_3_handle);
if(xReturn == pdPASS) printf("创建 task_example_3 任务成功!\r\n");
else printf("创建 task_example_3 任务失败!\r\n");
printf("Minimum free heapconfigMINIMAL_STACK_SIZE size: %d bytes\n", esp_get_minimum_free_heap_size());
}
运行程序后串口输出如下:可以看到,初始计数信号量有3个,随着不断获取,计数信号量为空获取失败,只有计数信号量释放后才能继续获取。

五. 小结
本文主要探索了二值信号量与计数信号量的原理及其使用方法,包括信号量的原理,信号量的运行机制,信号量与消息队列的比较,信号量的相关API函数,以及信号量的使用实验等。通过本文,不知道大家对第一节的几个问题,有没有自己的答案。如果自己的答案不太清晰,或者对文中某些地方不太明白的同学,欢迎留言交流。原创不易,大家的点赞和关注是对我持续更新最大的鼓励,谢谢!也为坚持看到系列文章此处的你点赞!

明天就是五一假期,码完这篇文章,也就开启我的假期,祝大家玩的开心!
想要完整源码的同学,可以扫码,或者微信搜索:硬件电子与嵌入式小栈 ,关注我的微信公众号,留言即可获取,公众号里面也会有丰富的干货文章哦。



:如何使用信号量&spm=1001.2101.3001.5002&articleId=138291510&d=1&t=3&u=58261f0572334827b4e8d7ad0dae1bd2)
2016

被折叠的 条评论
为什么被折叠?



