FreeRTOS教程5 信号量
1、准备材料
STM32CubeMX软件(Version 6.10.0)
Keil µVision5 IDE(MDK-Arm)
一个滑动变阻器
2、学习目标
本文主要学习 FreeRTOS 信号量的相关知识,包括创建/删除信号量、释放信号量、获取信号量等知识
3、前提知识
3.1、信号量概述
信号量是进程间用于通信的一种手段,其是基于队列实现的,信号量更适用于进程间同步,信号量包括二值信号量(Binary Semaphores)和计数信号量(Counting Semaphores)
二值信号量就是只有一个项的队列,该队列不为空则为满(所谓二值),二值信号量就像一个标志,适和用于进程间同步的通信
举个例子:ADC 的周期采集中断负责采集完成后将采集到的 ADC 的值写入数据缓存区中并且释放信号量,总是尝试获取信号量的数据处理任务在 ADC 采集中断释放信号量之后成功获取,然后退出阻塞状态对写入数据缓存区中采集到的 ADC 值进行处理,上述过程如下图所示:(注释1)
如下图所示为使用二值信号量来同步任务和中断的工作流程 (注释2)
计数信号量就是有固定长度的队列,队列中每个单元都是一个标志,其通常用于对多个共享资源的访问进行控制
举个例子:一家餐馆有 4 张可供用餐的桌子,我们创建一个长度为 4 ,初值为 4 的计数信号量来表示当前可供用餐的桌子数量,当有客人进来用餐时会 “获取” 一张餐桌,这时用来表示可供用餐的桌子数量的计数信号量就会减少一个,当有客人离开时会 “释放” 一张餐桌,这时用来表示可供用餐的桌子数量的计数信号量就会增加一个,上述过程如下图所示:
如下图所示为计数信号量的工作流程
3.2、创建信号量
信号量在使用之前也必须先创建,信号量被创建完之后是无效的,也即为 0 ,而由于信号量分为二值信号量和计数信号量两种,因此FreeRTOS也提供了不同的API函数,具体如下所述
/**
* @brief 动态分配内存创建二值信号量函数
* @param xSemaphore:创建的二值信号量句柄
* @retval None
*/
void vSemaphoreCreateBinary(SemaphoreHandle_t xSemaphore);
/**
* @brief 静态分配内存创建二值信号量函数
* @param pxSemaphoreBuffer:指向一个StaticSemaphore_t类型的变量,该变量将用于保存信号量的状态
* @retval 返回创建成功的信号量句柄,如果返回NULL则表示因为pxSemaphoreBuffer为空无法创建
*/
SemaphoreHandle_t xSemaphoreCreateBinaryStatic(
StaticSemaphore_t *pxSemaphoreBuffer);
/**
* @brief 动态分配内存创建计数信号量函数
* @param uxMaxCount:可以达到的最大计数值
* @param uxInitialCount:创建信号量时分配给信号量的计数值
* @retval 返回创建成功的信号量句柄,如果返回NULL则表示内存不足无法创建
*/
SemaphoreHandle_t xSemaphoreCreateCounting(UBaseType_t uxMaxCount,
UBaseType_t uxInitialCount);
/**
* @brief 静态分配内存创建计数信号量函数
* @param uxMaxCount:可以达到的最大计数值
* @param uxInitialCount:创建信号量时分配给信号量的计数值
* @param pxSempahoreBuffer:指向StaticSemaphore_t类型的变量,该变量然后用于保存信号量的数据结构体
* @retval 返回创建成功的信号量句柄,如果返回NULL则表示因为pxSemaphoreBuffer为空无法创建
*/
SemaphoreHandle_t xSemaphoreCreateCountingStatic(
UBaseType_t uxMaxCount,
UBaseType_t uxInitialCount,
StaticSemaphore_t pxSempahoreBuffer);
3.3、释放信号量
以下两个函数不仅仅可以用于释放二值信号量,还可以用于释放计数信号量和互斥量,具体如下所示
/**
* @brief 释放信号量函数
* @param xSemaphore:要释放的信号量的句柄
* @retval 如果信号量释放成功,则返回pdTRUE;如果发生错误,则返回pdFALSE
*/
BaseType_t xSemaphoreGive(SemaphoreHandle_t xSemaphore);
/**
* @brief 释放信号量的中断安全版本函数
* @param pxHigherPriorityTaskWoken:用于通知应用程序编写者是否应该执行上下文切换
* @retval 如果成功给出信号量,则返回pdTRUE,否则errQUEUE_FULL
*/
BaseType_t xSemaphoreGiveFromISR(SemaphoreHandle_t xSemaphore,
BaseType_t *pxHigherPriorityTaskWoken);
3.4、获取信号量
以下两个函数不仅仅可以用于获取二值信号量,还可以用于获取计数信号量和互斥量,具体如下所示
/**
* @brief 获取信号量函数
* @param xSemaphore:正在获取的信号量的句柄
* @param xTicksToWait:等待信号量变为可用的时间
* @retval 成功获得信号量则返回pdTRUE;如果xTicksToWait过期,信号量不可用,则返回pdFALSE
*/
BaseType_t xSemaphoreTake(SemaphoreHandle_t xSemaphore, TickType_t xTicksToWait);
/**
* @brief 获取信号量的中断安全版本函数
* @param xSemaphore:正在获取的信号量的句柄
* @param pxHigherPriorityTaskWoken:用于通知应用程序编写者是否应该执行上下文切换
* @retval 成功获取则返回pdTRUE,未成功获取则返回pdFALSE
*/
BaseType_t xSemaphoreTakeFromISR(SemaphoreHandle_t xSemaphore,
signed BaseType_t *pxHigherPriorityTaskWoken);
3.5、删除信号量
/**
* @brief 删除信号量,包括互斥锁型信号量和递归信号量
* @param xSemaphore:被删除的信号量的句柄
* @retval None
*/
void vSemaphoreDelete(SemaphoreHandle_t xSemaphore);
3.6、工具函数
/**
* @brief 获取信号量计数
* @param xSemaphore:正在查询的信号量的句柄
* @retval 如果信号量是计数信号量,则返回信号量的当前计数值。如果信号量是二进制信号量,则当信号量可用时,返回1,当信号量不可用时,返回 0
*/
UBaseType_t uxSemaphoreGetCount(SemaphoreHandle_t xSemaphore);
4、实验一:二值信号量的应用
4.1、实验目标
- 创建一个二值信号量 BinarySem_ADC
- 配置 ADC1 IN5 在 500ms 的定时器驱动下周期采集 ADC 值,采集完成后将采集值写入缓存数组,然后释放二值信号量 BinarySem_ADC
- 创建一个任务 TASK_ADC,该任务总是尝试获取二值信号量 BinarySem_ADC,获取成功之后,将写入缓存数组的 ADC 采集值进行转换,然后通过 USART1 输出给用户
4.2、CubeMX相关配置
首先读者应按照 "FreeRTOS教程1 基础知识" 章节配置一个可以正常编译通过的 FreeRTOS 空工程,然后在此空工程的基础上增加本实验所提出的要求
本实验需要初始化 USART1 作为输出信息渠道,具体配置步骤请阅读“STM32CubeMX教程9 USART/UART 异步通信”,如下图所示
本实验需要设置 TIM3 作为 ADC1 IN5 触发源的单通道 ADC 采集,采集周期为 500ms ,因此需要配置 ADC1 和 TIM3,感兴趣读者可以阅读”STM32CubeMX教程13 ADC - 单通道转换“实验,如下图所示
由于我们将要在 ADC 采集完成中断中使用 FreeRTOS 的释放二值信号量的函数,因此需要将其优先级设置在 15~5 之间,在这里设置为 7,具体如下图所示
单击 Middleware and Software Packs/FREERTOS,在 Configuration 中单击 Tasks and Queues 选项卡双击默认任务修改其参数,具体如下图所示
然后在 Configuration 中单击 Timers and Semaphores ,在 Binary Semaphores 中单击 Add 按钮新增加一个名为 BinarySem_ADC 的二值信号量,具体如下图所示
配置 Clock Configuration 和 Project Manager 两个页面,接下来直接单击 GENERATE CODE 按钮生成工程代码即可
4.3、添加其他必要代码
按照 “STM32CubeMX教程9 USART/UART 异步通信” 实验 “6、串口printf重定向” 小节增加串口 printf 重定向代码,具体不再赘述
首先应该在 freertos.c 中添加信号量的头文件,如下所述
/*freertos.c中添加头文件*/
#include "semphr.h"
然后在该文件中重新实现 ADC 采集完成中断回调函数,在该函数中获取采集完成的 ADC 值,将其保存在全局变量 adc_value 中,然后释放二值信号量 BinarySem_ADC ,如下所述
/*转换完成中断回调*/
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
/*定时器中断启动单通道转换*/
if(hadc->Instance == ADC1)
{
adc_value = HAL_ADC_GetValue(hadc);
BaseType_t highTaskWoken = pdFALSE;
if(BinarySem_ADCHandle != NULL)
{
xSemaphoreGiveFromISR(BinarySem_ADCHandle, &highTaskWoken);
portYIELD_FROM_ISR(highTaskWoken);
}
}
}
接下来仍然在该文件中实现任务 TASK_ADC 的函数体内容,该任务函数总是尝试获取二值信号量,一旦获取成功表示 ADC 转换完成,就将 ADC 转换完成的值变为电压值,然后通过 USART1 输出给用户显示,如下所述
/*ADC任务函数*/
void TASK_ADC(void *argument)
{
/* USER CODE BEGIN TASK_ADC */
/* Infinite loop */
for(;;)
{
if(xSemaphoreTake(BinarySem_ADCHandle, portMAX_DELAY) == pdTRUE)
{
uint32_t Volt = (3300 * adc_value)>>12;
printf("val:%d, Volt:%d\r\n", adc_value, Volt);
}
}
/* USER CODE END TASK_ADC */
}
最后在 main.c 文件主函数 main() 中以中断方式启动 ADC 转换即可,如下所述
//以中断方式启动ADC1
HAL_ADC_Start_IT(&hadc1);
//启动ADC1触发源定时器TIM3
HAL_TIM_Base_Start(&htim3);
4.4、烧录验证
烧录程序,打开串口助手,可以发现每隔一段时间就会输出当前 ADC1 IN5 通道采集到的 ADC 的值,将其接入一个滑动变阻器,当滑动变阻器从一端滑动到另一端时,串口输出的采集值也在从 0 逐渐变为最大值 4095 ,整个过程串口输出信息如下图所示
5、实验二:计数信号量的应用
5.1、实验目标
- 创建一个计数信号量 CountingSem_Tables ,设置最大值为 5 ,初始值设为 5 ,表示饭店内初始有 5 张桌子
- 启动 RTC 周期唤醒中断,唤醒周期为 3s ,在 RTC 唤醒中断中释放信号量,模拟有客人离开饭店
- 创建任务 TASK_KEY2 ,当按键 KEY2 按下时尝试获取信号量,模拟客人进店,同时调用获取信号量计数查询函数,查询当前可用餐桌个数
5.2、CubeMX相关配置
首先读者应按照 "FreeRTOS教程1 基础知识" 章节配置一个可以正常编译通过的 FreeRTOS 空工程,然后在此空工程的基础上增加本实验所提出的要求
本实验需要初始化开发板上 KEY2 用户按键做普通输入,具体配置步骤请阅读 “STM32CubeMX教程3 GPIO输入 - 按键响应” ,注意虽开发板不同但配置原理一致,如下图所示
本实验需要初始化 USART1 作为输出信息渠道,具体配置步骤请阅读 “STM32CubeMX教程9 USART/UART 异步通信” ,如下图所示
本实验需要配置 RTC 周期唤醒中断,具体配置步骤和参数介绍读者可阅读”STM32CubeMX教程10 RTC 实时时钟 - 周期唤醒、闹钟A/B事件和备份寄存器“实验,此处不再赘述,这里参数、时钟配置如下图所示
由于需要在 RTC 周期唤醒中断中使用 FreeRTOS 的 API 函数,因此 RTC 周期唤醒中断的优先级应该设置在 15~5 之间,此处设置为 7 ,具体如下图所示
单击 Middleware and Software Packs/FREERTOS,在 Configuration 中单击 Tasks and Queues 选项卡,双击默认任务修改其参数,具体如下图所示
然后在 Configuration 中单击 Timers and Semaphores 选项卡,在最下方的 Counting Semaphores 中单击右下角的 Add 按钮增加一个计数信号量,具体如下图所示
配置 Clock Configuration 和 Project Manager 两个页面,接下来直接单击 GENERATE CODE 按钮生成工程代码即可
5.3、添加其他必要代码
按照 “STM32CubeMX教程9 USART/UART 异步通信” 实验 “6、串口printf重定向” 小节增加串口 printf 重定向代码,具体不再赘述
首先应该在 freertos.c 中添加信号量的头文件,如下所述
/*freertos.c中添加头文件*/
#include "stdio.h"
#include "semphr.h"
然后在该文件中重新实现周期唤醒回调函数,该函数用于周期释放计数信号量,具体如下所示
/*周期唤醒回调函数*/
void HAL_RTCEx_WakeUpTimerEventCallback(RTC_HandleTypeDef *hrtc)
{
if(CountingSem_TablesHandle != NULL)
{
BaseType_t highTaskWoken = pdFALSE;
//释放计数信号量
xSemaphoreGiveFromISR(CountingSem_TablesHandle, &highTaskWoken);
portYIELD_FROM_ISR(highTaskWoken);
}
}
最后仍然在该文件中实现任务 TASK_KEY2 ,该任务负责当按键 KEY2 按下时尝试获取计数信号量,当无任何按键按下时不断输出当前计数信号量可用数量,具体如下所示
void TASK_KEY2(void *argument)
{
/* USER CODE BEGIN TASK_KEY2 */
/* Infinite loop */
for(;;)
{
if(HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin) == GPIO_PIN_RESET)
{
//获取计数信号量
BaseType_t result = xSemaphoreTake(CountingSem_TablesHandle, pdMS_TO_TICKS(100));
if(result == pdTRUE) printf("Check In OK\r\n");
else printf("Check In Fail\r\n");
//按键消抖
osDelay(pdMS_TO_TICKS(300));
}
else
{
UBaseType_t AvailableTables = uxSemaphoreGetCount(CountingSem_TablesHandle);
printf("Now AvailableTables is : %d\r\n", (uint16_t)AvailableTables);
osDelay(pdMS_TO_TICKS(10));
}
}
/* USER CODE END TASK_KEY2 */
}
5.4、烧录验证
烧录程序,打开串口助手,发现不断输出当前可用计数信号量数量,当按住按键 KEY2 不松开,连续模拟客人进店,可以发现在模拟 3 个客人进店之后,剩余可用计数信号量的数量变为了 2 个,然后每隔一段时间计算信号量的数量慢慢增加直到最大值 5 ,接着按住按键 KEY2 不松开,连续模拟 5 个客人进店,当 5 个客人进店之后,再次按下 KEY2 按键可以发现串口输出 ”Check In Fail“,表示当前已无剩余可用计数信号量,上述整个过程如下图所示
6、注释详解
注释1:图片来源于 STM32Cube高效开发教程(高级篇) 第五章
注释2:图片来源于 Mastering_the_FreeRTOS_Real_Time_Kernel-A_Hands-On_Tutorial_Guide.pdf
参考资料
Mastering_the_FreeRTOS_Real_Time_Kernel-A_Hands-On_Tutorial_Guide.pdf