一、引言
在嵌入式系统开发中,多任务处理是提升系统性能和效率的重要手段。STM32 系列微控制器凭借其丰富的外设资源和强大的处理能力,成为嵌入式开发的热门选择。FreeRTOS 作为一款轻量级、开源且功能强大的实时操作系统,广泛应用于 STM32 平台。信号量作为 FreeRTOS 中实现任务间通信和同步的重要机制,能够有效解决资源共享、任务同步等问题。本文将详细介绍基于 STM32 HAL 库和 FreeRTOS 信号量的使用方法,结合实际代码示例,帮助开发者深入理解和掌握这一关键技术。
二、FreeRTOS 信号量概述
2.1 信号量的定义与作用
信号量(Semaphore)是一种用于控制多个任务对共享资源访问的机制,本质上是一个拥有固定数量资源的计数器。当任务需要访问共享资源时,需要获取信号量,如果信号量计数器大于 0,任务获取成功,计数器减 1;若计数器为 0,任务将被阻塞,直到其他任务释放信号量使计数器增加。
信号量主要有以下几种用途:
- 资源管理:控制对共享资源(如外设、缓冲区)的访问,避免多个任务同时操作导致冲突。
- 任务同步:实现任务之间的同步,确保任务按照特定顺序执行。
- 事件标志:用于标识特定事件的发生,通知其他任务进行处理。
2.2 信号量的类型
FreeRTOS 支持以下几种类型的信号量:
- 二进制信号量:计数器值只能为 0 或 1,适用于控制对单个资源的访问,也可用于任务同步。
- 计数信号量:计数器值可以是 0 到最大值之间的任意整数,常用于管理多个相同资源的访问。
- 互斥信号量:一种特殊的二进制信号量,具有优先级继承机制,用于解决优先级反转问题,确保高优先级任务不会因等待低优先级任务释放资源而被长时间阻塞。
- 递归互斥信号量:允许同一个任务多次获取信号量,每次获取计数器加 1,释放时计数器减 1,直到计数器为 0 时才真正释放资源。
三、基于 STM32 HAL 库的 FreeRTOS 工程搭建
3.1 环境准备
在开始使用 FreeRTOS 信号量之前,需要准备好开发环境:
- 硬件平台:选择一款基于 STM32 的开发板,如 STM32F407ZGT6。
- 开发工具:安装好 STM32CubeMX、Keil MDK 或其他支持 STM32 开发的 IDE。
- 软件资源:确保 STM32 HAL 库和 FreeRTOS 源码已正确添加到工程中。
3.2 使用 STM32CubeMX 配置工程
- 新建工程:打开 STM32CubeMX,选择对应的 STM32 芯片型号创建新工程。
- 配置外设:根据需求配置 GPIO、USART、定时器等外设,例如配置 USART 用于调试输出。
- 启用 FreeRTOS:在 “Middleware” 选项卡中选择 FreeRTOS,配置任务栈大小、优先级等参数。
- 生成代码:完成配置后,生成基于 HAL 库的 STM32 工程代码,并选择对应的 IDE(如 Keil MDK)。
3.3 工程结构与关键文件
生成的工程包含以下关键文件:
- main.c:主函数入口,初始化 HAL 库和 FreeRTOS 任务。
- FreeRTOSConfig.h:FreeRTOS 配置文件,定义系统参数(如任务栈大小、最大优先级等)。
- tasks.c:用户自定义任务函数的实现文件。
- cmsis_os2.c/h:FreeRTOS 与 CMSIS-RTOS 2 接口文件,提供统一的 API 调用方式。
四、二进制信号量的使用
4.1 二进制信号量的创建
在 FreeRTOS 中,使用xSemaphoreCreateBinary()
函数创建二进制信号量,示例代码如下:
#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"
// 定义二进制信号量句柄
SemaphoreHandle_t xBinarySemaphore;
void vSetupBinarySemaphore(void)
{
// 创建二进制信号量
xBinarySemaphore = xSemaphoreCreateBinary();
if (xBinarySemaphore != NULL)
{
// 初始化信号量为可用状态(释放信号量)
xSemaphoreGive(xBinarySemaphore);
}
}
4.2 获取和释放二进制信号量
任务通过xSemaphoreTake()
函数获取二进制信号量,xSemaphoreGive()
函数释放信号量:
// 任务1:尝试获取二进制信号量
void vTask1(void *pvParameters)
{
for (;;)
{
if (xSemaphoreTake(xBinarySemaphore, portMAX_DELAY) == pdPASS)
{
// 获取成功,执行任务逻辑
// 例如访问共享资源
// 释放信号量
xSemaphoreGive(xBinarySemaphore);
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
// 任务2:释放二进制信号量
void vTask2(void *pvParameters)
{
for (;;)
{
// 模拟事件发生,释放信号量
xSemaphoreGive(xBinarySemaphore);
vTaskDelay(pdMS_TO_TICKS(5000));
}
}
4.3 二进制信号量用于任务同步
二进制信号量常用于实现任务间的同步。例如,任务 A 等待任务 B 完成某个操作后再继续执行:
// 任务A:等待任务B释放信号量
void vTaskA(void *pvParameters)
{
for (;;)
{
if (xSemaphoreTake(xBinarySemaphore, portMAX_DELAY) == pdPASS)
{
// 任务B已完成操作,执行后续逻辑
xSemaphoreGive(xBinarySemaphore);
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
// 任务B:完成操作后释放信号量
void vTaskB(void *pvParameters)
{
for (;;)
{
// 执行任务B的操作
// 操作完成后释放信号量
xSemaphoreGive(xBinarySemaphore);
vTaskDelay(pdMS_TO_TICKS(3000));
}
}
五、计数信号量的使用
5.1 计数信号量的创建
使用xSemaphoreCreateCounting()
函数创建计数信号量,示例代码如下:
// 定义计数信号量句柄
SemaphoreHandle_t xCountingSemaphore;
void vSetupCountingSemaphore(void)
{
// 创建计数信号量,初始计数值为3,最大计数值为5
xCountingSemaphore = xSemaphoreCreateCounting(5, 3);
if (xCountingSemaphore != NULL)
{
// 信号量创建成功
}
}
5.2 获取和释放计数信号量
任务获取和释放计数信号量的方式与二进制信号量类似,但计数信号量的计数器可以大于 1:
// 任务3:获取计数信号量
void vTask3(void *pvParameters)
{
for (;;)
{
if (xSemaphoreTake(xCountingSemaphore, portMAX_DELAY) == pdPASS)
{
// 获取成功,执行任务逻辑
// 释放信号量
xSemaphoreGive(xCountingSemaphore);
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
// 任务4:释放计数信号量
void vTask4(void *pvParameters)
{
for (;;)
{
// 模拟资源可用,释放信号量
xSemaphoreGive(xCountingSemaphore);
vTaskDelay(pdMS_TO_TICKS(3000));
}
}
5.3 计数信号量用于资源管理
计数信号量适用于管理多个相同资源的访问。例如,假设有 3 个打印机资源,多个任务可以通过获取计数信号量来使用打印机:
// 任务5:使用打印机资源
void vTask5(void *pvParameters)
{
for (;;)
{
if (xSemaphoreTake(xCountingSemaphore, portMAX_DELAY) == pdPASS)
{
// 使用打印机
// 释放打印机资源
xSemaphoreGive(xCountingSemaphore);
}
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
// 任务6:使用打印机资源
void vTask6(void *pvParameters)
{
for (;;)
{
if (xSemaphoreTake(xCountingSemaphore, portMAX_DELAY) == pdPASS)
{
// 使用打印机
// 释放打印机资源
xSemaphoreGive(xCountingSemaphore);
}
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
六、互斥信号量的使用
6.1 互斥信号量的创建
使用xSemaphoreCreateMutex()
函数创建互斥信号量,示例代码如下:
// 定义互斥信号量句柄
SemaphoreHandle_t xMutexSemaphore;
void vSetupMutexSemaphore(void)
{
// 创建互斥信号量
xMutexSemaphore = xSemaphoreCreateMutex();
if (xMutexSemaphore != NULL)
{
// 信号量创建成功
}
}
6.2 获取和释放互斥信号量
互斥信号量的获取和释放与二进制信号量类似,但具有优先级继承特性:
// 任务7:获取互斥信号量
void vTask7(void *pvParameters)
{
for (;;)
{
if (xSemaphoreTake(xMutexSemaphore, portMAX_DELAY) == pdPASS)
{
// 获取成功,执行任务逻辑
// 释放信号量
xSemaphoreGive(xMutexSemaphore);
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
// 任务8:获取互斥信号量
void vTask8(void *pvParameters)
{
for (;;)
{
if (xSemaphoreTake(xMutexSemaphore, portMAX_DELAY) == pdPASS)
{
// 获取成功,执行任务逻辑
// 释放信号量
xSemaphoreGive(xMutexSemaphore);
}
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
6.3 优先级继承机制
当低优先级任务持有互斥信号量,而高优先级任务尝试获取该信号量时,低优先级任务的优先级会临时提升到与高优先级任务相同,以确保高优先级任务尽快获取信号量,避免优先级反转问题。
七、递归互斥信号量的使用
7.1 递归互斥信号量的创建
使用xSemaphoreCreateRecursiveMutex()
函数创建递归互斥信号量,示例代码如下:
// 定义递归互斥信号量句柄
SemaphoreHandle_t xRecursiveMutexSemaphore;
void vSetupRecursiveMutexSemaphore(void)
{
// 创建递归互斥信号量
xRecursiveMutexSemaphore = xSemaphoreCreateRecursiveMutex();
if (xRecursiveMutexSemaphore != NULL)
{
// 信号量创建成功
}
}
7.2 获取和释放递归互斥信号量
递归互斥信号量允许同一个任务多次获取,每次获取计数器加 1,释放时计数器减 1:
// 任务9:多次获取递归互斥信号量
void vTask9(void *pvParameters)
{
for (;;)
{
if (xSemaphoreTakeRecursive(xRecursiveMutexSemaphore, portMAX_DELAY) == pdPASS)
{
// 第一次获取成功
if (xSemaphoreTakeRecursive(xRecursiveMutexSemaphore, portMAX_DELAY) == pdPASS)
{
// 第二次获取成功
// 执行任务逻辑
// 释放两次信号量
xSemaphoreGiveRecursive(xRecursiveMutexSemaphore);
xSemaphoreGiveRecursive(xRecursiveMutexSemaphore);
}
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
八、信号量使用注意事项
- 内存管理:创建信号量会占用堆内存,需确保堆内存足够,避免内存溢出。
- 死锁预防:合理设计任务逻辑,避免多个任务相互等待对方释放信号量导致死锁。
- 优先级反转:使用互斥信号量时,需了解优先级继承机制,避免因优先级反转影响系统实时性。
- 超时处理:在获取信号量时设置合理的超时时间,防止任务长时间阻塞。
九、总结
本文详细介绍了基于 STM32 HAL 库和 FreeRTOS 信号量的使用方法,包括二进制信号量、计数信号量、互斥信号量和递归互斥信号量的创建、获取和释放,以及它们在任务同步和资源管理中的应用。通过实际代码示例,展示了如何在 STM32 工程中正确使用 FreeRTOS 信号量。在实际开发中,开发者应根据具体需求选择合适的信号量类型,并注意信号量使用过程中的常见问题,以确保系统的稳定性和实时性。掌握 FreeRTOS 信号量的使用,将为嵌入式多任务系统开发提供强大的技术支持。