STM32F1+HAL库+FreeTOTS学习13——二值信号量
- 1. 信号量
- 2. 二值信号量
- 3. 相关API函数
- 3.1 创建二值信号量
- 3.2 获取信号量
- 3.3 释放信号量
- 3.4 删除信号量
- 4. 二值信号量操作实验
- 1. 实验内容
- 2. 代码实现:
- 3. 运行结果
上一期我们学习了FreeRTOS中的队列,这一期我们学习信号量中的——二值信号量
1. 信号量
在学习信号量之前,我们先来回顾一下队列:
- 队列:队列是一种任务到任务、任务到中断、中断到任务数据交流的一种机制、
随之,我们给出信号量的定义:
- 信号量:信号量是一种解决同步问题的机制,可以实现对共享资源的有序访问。其中,“同步”指的是任务间的同步,即信号量可以使得一个任务等待另一个任务完成某件事情后,才继续执行;而“有序访问”指的是对被多任务或中断访问的共享资源(如全局变量)的管理,当一个任务在访问(读取或写入)一个共享资源时,信号量可以防止其他任务或中断在这期间访问(读取或写入)这个共享资源。
我们来举一个简单的例子,比如说现在有一个停车场,停车场上有5个车位,如果现在小明需要去停车,那么他应该先要看一下是否还有车位(判断信号量是否有资源),如果有车位,就直接停车(获取信号成功,这个时候车位-1),如果没有车位,那么可以直接选择不停车(获取信号量失败),或者原地等待(任务阻塞),直到有人开车出停车场(释放信号量,车位+1)。
在上述的例子中:一共5个车位(信号资源数为5),有人开出停车场,信号量释放,信号量计数值(资源数)加1,反之则减1。
- 因为有5个车位,计数值最大值为5,所以这个是计数信号量。
- 如果只有一个车位,那么计数值只有0和1,这个就是二值信号量。
- 类似的,还有互斥信号量,这个我们后面会一一介绍。
下面我们来看一下队列和信号量的对比
队列 | 信号量 |
---|---|
可以容纳多个数据;创建队列有两部分内存:队列结构体+队列项存储空间 | 仅存放计数值,无法存放其他数据;创建信号量,只需分配信号量结构体 |
写入队列:当队列满时,可阻塞; | 释放信号量:不可阻塞,计数值++,当计数值为最大值时,返回失败 |
读取队列:当队列为空时,可阻塞; | 获取信号量:计数值–,当没有资源时,可阻塞 |
2. 二值信号量
二值信号量是信号量的一种,前面我们提到过,信号量是基于队列实现的,二值信号量也是一样,二值信号量实际上就是一个队列长度为 1 的队列,队列中只有空和满两种情况,常用于互斥访问和任务同步,与互斥信号量比较类似,但是二值信号量可能会导致优先级翻转的问题(关于优先级翻转,我们稍后做出介绍),所以二值信号量更适合用于同步。
优先级翻转问题:当一个高优先级任务因获取一个被低优先级任务获取而处于没有资源状态的二值信号量时,这个高优先级的任务将被阻塞,直到低优先级的任务释放二值信号量,而在这之前,如果有一个优先级介于这个高优先级任务和低优先级任务之间的任务就绪,那么这个中等优先级的任务就会抢占低优先级任务的运行,这么一来,这三个任务中优先级最高的任务反而要最后才运行,这就是二值信号量带来的优先级翻转问题。
以上就是二值信号量的内容,下面我们看二值信号量的相关API函数
3. 相关API函数
常用的二值信号量API函数如下表:
函数 | 描述 |
---|---|
xSemaphoreCreateBinary() | 使用动态方式创建二值信号量 |
xSemaphoreCreateBinaryStatic() | 使用静态方式创建二值信号量 |
xSemaphoreTake() | 获取信号量 |
xSemaphoreTakeFromISR() | 在中断中获取信号量 |
xSemaphoreGive() | 释放信号量 |
xSemaphoreGiveFromISR() | 在中断中释放信号量 |
vSemaphoreDelete() | 删除信号量 |
3.1 创建二值信号量
- xSemaphoreCreateBinary()
此函数用于动态方式创建二值信号量,创建所需要的内存,有FreeRTOS自动分配,该函数实际上是一个宏定义,在semphr.h文件中有定义,具体的代码如下:
#define xSemaphoreCreateBinary() \
xQueueGenericCreate( ( UBaseType_t ) 1, \
semSEMAPHORE_QUEUE_ITEM_LENGTH, \
queueQUEUE_TYPE_BINARY_SEMAPHORE)
可以看到xSemaphoreCreateBinary() 内部是调用了xQueueGenericCreate() ,该函数在 STM32F1+HAL库+FreeTOTS学习12——队列 中有介绍,我们这里不赘述。所以我们直接把xSemaphoreCreateBinary() 当作一个函数介绍(实际上是一个宏,我们当作函数来使用,FreeRTOS的官方文档也是这样的),下面是函数原型:
/**
* @brief xSemaphoreCreateBinary
* @param 无
* @retval 返回值为NULL,表示创建失败,其他值表示为创建二值信号量的句柄
*/
SemaphoreHandle_t xSemaphoreCreateBinary( void );
- xSemaphoreCreateBinaryStatic()
此函数用于静态方式创建二值信号量,创建二值信号量所需要的内存,需要用户手动分配,该函数实际上是一个宏定义,在semphr.h文件中有定义,具体的代码如下:
#define xSemaphoreCreateBinaryStatic(pxStaticSemaphore) \
xQueueGenericCreateStatic( ( UBaseType_t ) 1, \
semSEMAPHORE_QUEUE_ITEM_LENGTH, \
NULL, \
pxStaticSemaphore, \
queueQUEUE_TYPE_BINARY_SEMAPHORE)
可以看到xSemaphoreCreateBinaryStatic() 内部是调用了xQueueGenericCreateStatic() ,有用该函数不经常使用,我们这里不赘述。所以我们直接把xSemaphoreCreateBinaryStatic() 当作一个函数介绍(实际上是一个宏,我们当作函数来使用,FreeRTOS的官方文档也是这样的),下面是函数原型:
/**
* @brief xSemaphoreCreateBinaryStatic
* @param pxSemaphoreBuffer:必须指向 StaticSemaphore_t 类型的变量,该变量将用于保存信号量的状态。
* @retval 返回值为NULL,表示创建失败,其他值表示为创建二值信号量的句柄
*/
SemaphoreHandle_t xSemaphoreCreateBinaryStatic(
StaticSemaphore_t *pxSemaphoreBuffer );
3.2 获取信号量
- xSemaphoreTake()
此函数用于获取信号量,如果信号量处于没有资源的状态,那么可以选择将任务进入阻塞状态,如果成功获取到了信号量,那么信号的资源数减1,该函数实际上是一个宏定义,在semphr.h文件中有定义,具体的代码如下:
#define xSemaphoreTake( xSemaphore, \
xBlockTime) \
xQueueSemaphoreTake( ( xSemaphore ), \
( xBlockTime ))
可以看到xSemaphoreTake() 内部是调用了xQueueSemaphoreTake() ,关于xQueueSemaphoreTake函数的定义和使用,我们这里不赘述。所以我们直接把xSemaphoreTake() 当作一个函数介绍(实际上是一个宏,我们当作函数来使用,FreeRTOS的官方文档也是这样的),下面是函数原型:
/**
* @brief xSemaphoreTake
* @param xSemaphore:需要获取二值信号量的句柄
* @param xTicksToWait:阻塞时间
* @retval 返回值为pdTRUE,表示获取成功,如果返回值为pdFALSE,表示获取失败。
*/
BaseType_t xSemaphoreTake( SemaphoreHandle_t xSemaphore,
TickType_t xTicksToWait );
- xSemaphoreTakeFromISR()
此函数用于在中断中获取信号量,该函数实际上是一个宏定义,在semphr.h文件中有定义,具体的代码如下:
#define xSemaphoreTakeFromISR( xSemaphore, \
pxHigherPriorityTaskWoken) \
xQueueReceiveFromISR( ( QueueHandle_t ) \
( xSemaphore ), \
NULL, \
( pxHigherPriorityTaskWoken ))
可以看到xSemaphoreTakeFromISR() 内部是调用了xQueueReceiveFromISR() ,关于xQueueReceiveFromISR函数的定义和使用,我们这里不赘述。所以我们直接把xSemaphoreTakeFromISR() 当作一个函数介绍(实际上是一个宏,我们当作函数来使用,FreeRTOS的官方文档也是这样的),下面是函数原型:
/**
* @brief xSemaphoreTakeFromISR
* @param xSemaphore:需要获取二值信号量的句柄
* @param pxHigherPriorityTaskWoken:获取信号量之后是否需要任务切换,需要切换则 *pxHigherPriorityTaskWoken = pdTRUE
* @retval 返回值为pdTRUE,表示获取成功,如果返回值为pdFALSE,表示获取失败。
*/
BaseType_t xSemaphoreTakeFromISR ( SemaphoreHandle_t xSemaphore, signed BaseType_t *pxHigherPriorityTaskWoken );
需要注意的是:
- xSemaphoreTake()可以用来获取二值信号量、计数信号量、互斥信号量。
- xSemaphoreTakeFromISR() 只能用来获取二值信号量和计数信号量,不能用于互斥信号量。
3.3 释放信号量
- xSemaphoreGive()
此函数用于释放信号量,如果信号量处于资源满的状态,那么可以选择将任务进入阻塞状态,如果成功释放了信号量,那么信号的资源数加1,该函数实际上是一个宏定义,在semphr.h文件中有定义,具体的代码如下:
#define xSemaphoreGive( xSemaphore) \
xQueueGenericSend( ( QueueHandle_t ) ( xSemaphore ), \
NULL, \
semGIVE_BLOCK_TIME, \
queueSEND_TO_BACK)
可以看到xSemaphoreGive() 内部是调用了xQueueGenericSend() ,该函数在 STM32F1+HAL库+FreeTOTS学习12——队列 中有介绍,我们这里不赘述。所以我们直接把xSemaphoreGive() 当作一个函数介绍(实际上是一个宏,我们当作函数来使用,FreeRTOS的官方文档也是这样的),下面是函数原型:
/**
* @brief xSemaphoreGive
* @param xSemaphore:需要释放二值信号量的句柄
* @retval 返回值为pdTRUE,表示释放成功,如果返回值为pdFALSE,表示释放失败。
*/
BaseType_t xSemaphoreGive( SemaphoreHandle_t xSemaphore );
- xSemaphoreGiveFromISR()
此函数用于在中断中释放信号量,该函数实际上是一个宏定义,在semphr.h文件中有定义,具体的代码如下:
#define xSemaphoreGiveFromISR( xSemaphore, \
pxHigherPriorityTaskWoken) \
xQueueGiveFromISR( ( QueueHandle_t ) ( xSemaphore ), \
( pxHigherPriorityTaskWoken ))
可以看到xSemaphoreGiveFromISR() 内部是调用了xQueueGiveFromISR() ,关于xQueueGiveFromISR函数的定义和使用,我们这里不赘述。所以我们直接把xSemaphoreGiveFromISR() 当作一个函数介绍(实际上是一个宏,我们当作函数来使用,FreeRTOS的官方文档也是这样的),下面是函数原型:
/**
* @brief xSemaphoreGiveFromISR
* @param xSemaphore:需要释放二值信号量的句柄
* @param pxHigherPriorityTaskWoken:释放信号量之后是否需要任务切换,需要切换则 *pxHigherPriorityTaskWoken = pdTRUE
* @retval 返回值为pdTRUE,表示释放成功,如果返回值为pdFALSE,表示释放失败。
*/
BaseType_t xSemaphoreGiveFromISR ( SemaphoreHandle_t xSemaphore, signed BaseType_t *pxHigherPriorityTaskWoken )
需要注意的是:
- xSemaphoreGive()可以用来释放二值信号量、计数信号量、互斥信号量。
- xSemaphoreGiveFromISR () 只能用来释放二值信号量和计数信号量,不能用于互斥信号量。因为互斥信号量会有优先级继承的处理,而中断不属于任务,没有办法进行优先级继承。
3.4 删除信号量
- vSemaphoreDelete()
此函数用于删除已创建的信号量。该函数实际上是一个宏定义,在 semphr.h 文件中有定义,具体的代码如下所示
#define vSemaphoreDelete(xSemaphore) \
vQueueDelete ( QueueHandle_t ) \
( xSemaphore ))
可以看到vSemaphoreDelete() 内部是调用了vQueueDelete () ,关于vQueueDelete 函数的定义和使用,我们这里不赘述。所以我们直接把vSemaphoreDelete() 当作一个函数介绍(实际上是一个宏,我们当作函数来使用,FreeRTOS的官方文档也是这样的),下面是函数原型:
/**
* @brief vSemaphoreDelete
* @param xSemaphore :需要删除信号量的句柄
* @retval 无
*/
void vSemaphoreDelete( SemaphoreHandle_t xSemaphore );
4. 二值信号量操作实验
1. 实验内容
在STM32F103RCT6上运行FreeRTOS,通过按键控制,完成对应的二值信号量读写,具体要求如下:
- 定义一个二值信号量,
- 定义任务1:每次按下按键0,释放信号量
- 定义任务2:获取二值信号量
2. 代码实现:
- 由于本期内容涉及到按键扫描,会用到: STM32框架之按键扫描新思路 ,这里不做过多介绍。我们直接来看代码:
- freertos_demo.c
#include "freertos_demo.h"
#include "main.h"
#include "queue.h" //需要包含队列和任务相关的头文件
#include "task.h"
#include "key.h" //包含按键相关头文件
/*FreeRTOS*********************************************************************************************/
/******************************************************************************************************/
/*FreeRTOS配置*/
/* TASK1 任务 配置
* 包括: 任务句柄 任务优先级 堆栈大小
*/
#define TASK1_PRIO 1 /* 任务优先级 */
#define TASK1_STK_SIZE 128 /* 任务堆栈大小 */
TaskHandle_t Task1Task_Handler; /* 任务句柄 */
void task1(void *pvParameters); /*任务函数*/
/* TASK2 任务 配置
* 包括: 任务句柄 任务优先级 堆栈大小
*/
#define TASK2_PRIO 2 /* 任务优先级 */
#define TASK2_STK_SIZE 128 /* 任务堆栈大小 */
TaskHandle_t Task2Task_Handler; /* 任务句柄 */
void task2(void *pvParameters); /*任务函数*/
QueueHandle_t SemaphoreBinary; /* 定义二值信号量 */
/******************************************************************************************************/
/**
* @brief FreeRTOS例程入口函数
* @param 无
* @retval 无
*/
void freertos_demo(void)
{
taskENTER_CRITICAL(); /* 进入临界区,关闭中断,此时停止任务调度*/
SemaphoreBinary = xSemaphoreCreateBinary();
if(SemaphoreBinary != NULL)
{
printf("二值信号量创建成功!!!\r\n");
}
else
{
printf("二值信号量创建失败!!!\r\n");
}
/* 创建任务1 */
xTaskCreate((TaskFunction_t )task1,
(const char* )"task1",
(uint16_t )TASK1_STK_SIZE,
(void* )NULL,
(UBaseType_t )TASK1_PRIO,
(TaskHandle_t* )&Task1Task_Handler);
/* 创建任务2 */
xTaskCreate((TaskFunction_t )task2,
(const char* )"task2",
(uint16_t )TASK2_STK_SIZE,
(void* )NULL,
(UBaseType_t )TASK2_PRIO,
(TaskHandle_t* )&Task2Task_Handler);
taskEXIT_CRITICAL(); /* 退出临界区,重新开启中断,开启任务调度 */
vTaskStartScheduler(); //开启任务调度
}
/**
* @brief task1
* @param pvParameters : 传入参数(未用到)
* @retval 无
*/
void task1(void *pvParameters)
{
while(1)
{
Key_One_Scan(Key_Name_Key0,Key0_Up_Task,Key0_Down_Task); /* 按键0扫描,按下后释放二值信号量 */
}
}
/**
* @brief task2
* @param pvParameters : 传入参数(未用到)
* @retval 无
*/
void task2(void *pvParameters)
{
BaseType_t errMessage; /* 错误信息 */
while(1)
{
errMessage = xSemaphoreTake(SemaphoreBinary,portMAX_DELAY); /* 获取二值信号量 */
if(errMessage == pdTRUE)
{
printf("获取信号量成功\r\n");
}
else
{
printf("获取信号量失败\r\n");
}
}
}
- key.c
/* USER CODE BEGIN 2 */
#include "freertos_demo.h"
#include "key.h"
#include "usart.h"
extern QueueHandle_t SemaphoreBinary; /* 声明外部的二值信号量 */
void Key0_Down_Task(void)
{
BaseType_t errMessage; /* 错误信息 */
errMessage = xSemaphoreGive(SemaphoreBinary); /* 释放二值信号量 */
if(errMessage == pdTRUE)
{
printf("二值信号量释放成功\r\n");
}
else
{
printf("二值信号量释放失败\r\n");
}
}
void Key0_Up_Task(void)
{
}
void Key1_Down_Task(void)
{
}
void Key1_Up_Task(void)
{
}
void Key2_Down_Task(void)
{
}
void Key2_Up_Task(void)
{
}
void WKUP_Down_Task(void)
{
}
void WWKUP_Up_Task(void)
{
}
void Key_One_Scan(uint8_t KeyName ,void(*OnKeyOneUp)(void), void(*OnKeyOneDown)(void))
{
static uint8_t Key_Val[Key_Name_Max]; //按键值的存放位置
static uint8_t Key_Flag[Key_Name_Max]; //KEY0~2为0时表示按下,为1表示松开,WKUP反之
Key_Val[KeyName] = Key_Val[KeyName] <<1; //每次扫描完,将上一次扫描的结果左移保存
switch(KeyName)
{
case Key_Name_Key0: Key_Val[KeyName] = Key_Val[KeyName] | (HAL_GPIO_ReadPin(KEY0_GPIO_Port, KEY0_Pin)); //读取Key0按键值
break;
case Key_Name_Key1: Key_Val[KeyName] = Key_Val[KeyName] | (HAL_GPIO_ReadPin(KEY1_GPIO_Port, KEY1_Pin)); //读取Key1按键值
break;
case Key_Name_Key2: Key_Val[KeyName] = Key_Val[KeyName] | (HAL_GPIO_ReadPin(KEY2_GPIO_Port, KEY2_Pin)); //读取Key2按键值
break;
// case Key_Name_WKUP: Key_Val[KeyName] = Key_Val[KeyName] | (HAL_GPIO_ReadPin(WKUP_GPIO_Port, WKUP_Pin)); //读取WKUP按键值
// break;
default:
break;
}
// if(KeyName == Key_Name_WKUP) //WKUP的电路图与其他按键不同,所以需要特殊处理
// {
// //WKUP特殊情况
// //当按键标志为1(松开)是,判断是否按下,WKUP按下时为0xff
// if(Key_Val[KeyName] == 0xff && Key_Flag[KeyName] == 1)
// {
// (*OnKeyOneDown)();
// Key_Flag[KeyName] = 0;
// }
// //当按键标志位为0(按下),判断按键是否松开,WKUP松开时为0x00
// if(Key_Val[KeyName] == 0x00 && Key_Flag[KeyName] == 0)
// {
// (*OnKeyOneUp)();
// Key_Flag[KeyName] = 1;
// }
// }
// else //Key0~2按键逻辑判断
// {
//Key0~2常规判断
//当按键标志为1(松开)是,判断是否按下
if(Key_Val[KeyName] == 0x00 && Key_Flag[KeyName] == 1)
{
(*OnKeyOneDown)();
Key_Flag[KeyName] = 0;
}
//当按键标志位为0(按下),判断按键是否松开
if(Key_Val[KeyName] == 0xff && Key_Flag[KeyName] == 0)
{
(*OnKeyOneUp)();
Key_Flag[KeyName] = 1;
}
}
//}
/* USER CODE END 2 */
3. 运行结果
这里解释一下,为什么按键0按下,先出现获取信号量成功,而不是直接打印二值信号量释放成功:
- 在按键0没有按下之前,任务2处于阻塞状态,
- 按键0按下之后,信号量释放,任务2立刻进入就绪状态。
- 由于任务2的优先级比任务1高,所以先会立刻获取信号量,并打印获取信号量成功
- 答应成功后,任务2再次进入阻塞,进入任务1,打印二值信号量释放成功。
以上就是本期使用到的核心代码,其他部分我这里就不做展示,直接去看我往期的内容,源代码都有的。至于按键的配置,可以参考:夜深人静学32系列9——GPIO驱动数码管/蜂鸣器/按键/LED