FreeRTOS 信号量使用详解:从概念到代码示例
FreeRTOS信号量分三类核心类型:二进制信号量(同步)、计数信号量(多资源)、互斥信号量(解决优先级反转的互斥)。中断操作信号量必须用FromISR后缀的API,并处理完成上下文切换。互斥信号量是资源互斥的首选方案,二进制信号量更适合同步场景,计数信号量用于多相同资源管理。
·
你希望系统且详细地学习FreeRTOS中信号量的使用方法,我会从信号量的基本概念入手,区分不同类型的信号量,并结合具体的代码示例,讲解每种信号量的创建、获取、释放流程和实际应用场景,帮助你彻底掌握信号量的使用。
一、信号量的核心概念
信号量是FreeRTOS中实现任务间同步、任务与中断同步、资源互斥访问的核心机制,可通俗理解为:
- 二进制信号量:像卫生间门锁,只有“锁上(0)”和“打开(1)”两种状态,用于简单同步/单资源互斥。
- 计数信号量:像停车场车位,有N个可用车位(初始计数值),占用-1、释放+1,用于多资源管理。
- 互斥信号量(互斥体):特殊的二进制信号量,通过“优先级继承”解决优先级反转问题,专用于资源互斥。
前置条件
使用信号量前需在FreeRTOS配置文件FreeRTOSConfig.h中开启对应宏:
#define configUSE_SEMAPHORES 1 // 开启二进制/计数信号量
#define configUSE_MUTEXES 1 // 开启互斥信号量
#define configUSE_RECURSIVE_MUTEXES 1 // 可选:开启递归互斥信号量
所有信号量API都包含在semphr.h头文件中。
二、不同类型信号量的使用方法
1. 二进制信号量(Binary Semaphore)
核心作用
- 任务间同步:一个任务完成后通知另一个任务执行。
- 中断-任务同步:中断中释放信号量,唤醒等待的任务(最常用场景)。
关键API
| 函数 | 作用 | 适用场景 |
|---|---|---|
xSemaphoreCreateBinary() |
创建二进制信号量 | 任务中(返回句柄,失败为NULL) |
xSemaphoreTake(句柄, 超时时间) |
获取信号量(计数值→0) | 任务中(portMAX_DELAY=永久等待) |
xSemaphoreGive(句柄) |
释放信号量(计数值→1) | 任务中 |
xSemaphoreGiveFromISR(句柄, &xHigherPriorityTaskWoken) |
释放信号量 | 中断中(ISR专用) |
代码示例(中断与任务同步)
场景:按键中断触发后,释放信号量唤醒任务处理按键事件(基于STM32):
#include "FreeRTOS.h"
#include "semphr.h"
#include "stm32f1xx_hal.h"
// 1. 定义二进制信号量句柄(全局/任务可访问)
SemaphoreHandle_t xBinarySem = NULL;
// 2. 按键中断服务函数(EXTI0为例)
void EXTI0_IRQHandler(void)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE; // 标记是否需要切换任务
if(EXTI_GetITStatus(EXTI_Line0) != RESET)
{
// 中断中释放信号量(必须用FromISR版本)
if(xBinarySem != NULL)
{
xSemaphoreGiveFromISR(xBinarySem, &xHigherPriorityTaskWoken);
// 若唤醒更高优先级任务,立即切换上下文
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
EXTI_ClearITPendingBit(EXTI_Line0); // 清除中断标志
}
}
// 3. 按键处理任务
void vKeyTask(void *pvParameters)
{
for(;;) // 任务无限循环
{
// 获取信号量:永久等待,直到中断释放
if(xSemaphoreTake(xBinarySem, portMAX_DELAY) == pdPASS)
{
// 信号量获取成功,执行按键逻辑
printf("按键按下!翻转LED\r\n");
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5); // 控制LED
}
}
}
// 4. 主函数初始化
int main(void)
{
// 硬件初始化(GPIO、中断、串口等)
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_USART1_UART_Init();
// 创建二进制信号量
xBinarySem = xSemaphoreCreateBinary();
if(xBinarySem != NULL)
{
// 创建按键任务:名称、栈大小、参数、优先级、任务句柄
xTaskCreate(vKeyTask, "KeyTask", 128, NULL, 2, NULL);
// 启动FreeRTOS调度器
vTaskStartScheduler();
}
// 调度器启动失败(如栈不足),进入死循环
for(;;);
}
2. 计数信号量(Counting Semaphore)
核心作用
管理多个相同资源(如3个串口缓冲区、5个打印队列),计数值代表可用资源数。
关键API
| 函数 | 作用 |
|---|---|
xSemaphoreCreateCounting(最大计数值, 初始计数值) |
创建计数信号量 |
uxSemaphoreGetCount(句柄) |
获取当前计数值(调试用) |
xSemaphoreTake/Give |
同二进制信号量(Take=资源-1,Give=资源+1) |
代码示例(多资源管理)
场景:3个串口资源,4个任务竞争使用:
#include "FreeRTOS.h"
#include "semphr.h"
// 计数信号量句柄:最大3个资源,初始3个可用
SemaphoreHandle_t xCountSem = xSemaphoreCreateCounting(3, 3);
// 串口使用任务
void vUartTask(void *pvParameters)
{
uint32_t taskID = (uint32_t)pvParameters; // 任务编号
for(;;)
{
// 获取资源:等待500ms,超时则放弃
if(xSemaphoreTake(xCountSem, pdMS_TO_TICKS(500)) == pdPASS)
{
// 成功获取资源
printf("任务%d:占用串口,剩余资源:%d\r\n",
taskID, uxSemaphoreGetCount(xCountSem));
vTaskDelay(pdMS_TO_TICKS(1000)); // 模拟使用串口
// 释放资源
xSemaphoreGive(xCountSem);
printf("任务%d:释放串口,剩余资源:%d\r\n",
taskID, uxSemaphoreGetCount(xCountSem));
}
else
{
// 获取超时
printf("任务%d:获取串口超时!\r\n", taskID);
}
vTaskDelay(pdMS_TO_TICKS(500)); // 任务延时,让出CPU
}
}
// 主函数创建任务
int main(void)
{
// 硬件初始化...
if(xCountSem != NULL)
{
// 创建4个任务竞争3个资源
xTaskCreate(vUartTask, "Uart1", 128, (void*)1, 1, NULL);
xTaskCreate(vUartTask, "Uart2", 128, (void*)2, 1, NULL);
xTaskCreate(vUartTask, "Uart3", 128, (void*)3, 1, NULL);
xTaskCreate(vUartTask, "Uart4", 128, (void*)4, 1, NULL);
vTaskStartScheduler();
}
for(;;);
}
3. 互斥信号量(Mutex Semaphore)
核心作用
解决优先级反转问题(低优先级任务占用资源,高优先级任务等待,中优先级任务抢占CPU),专用于独占式资源访问(如LCD、SPI总线)。
优先级继承:低优先级任务占用互斥体时,临时继承高优先级任务的优先级,避免中优先级任务抢占。
关键API
| 函数 | 作用 | 注意事项 |
|---|---|---|
xSemaphoreCreateMutex() |
创建互斥信号量 | 无FromISR版本,不能在中断中使用 |
xSemaphoreTake/Give |
获取/释放互斥体 | 必须在同一个任务中操作 |
代码示例(解决优先级反转)
#include "FreeRTOS.h"
#include "semphr.h"
// 互斥信号量句柄
SemaphoreHandle_t xMutex = xSemaphoreCreateMutex();
// 低优先级任务(优先级1)
void vLowTask(void *pvParameters)
{
for(;;)
{
xSemaphoreTake(xMutex, portMAX_DELAY); // 获取互斥体
printf("低优先级任务:占用LCD\r\n");
vTaskDelay(pdMS_TO_TICKS(2000)); // 长时间占用资源
xSemaphoreGive(xMutex); // 释放互斥体
printf("低优先级任务:释放LCD\r\n");
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
// 中优先级任务(优先级2)
void vMidTask(void *pvParameters)
{
for(;;)
{
printf("中优先级任务:执行中...\r\n");
vTaskDelay(pdMS_TO_TICKS(500));
}
}
// 高优先级任务(优先级3)
void vHighTask(void *pvParameters)
{
for(;;)
{
printf("高优先级任务:尝试获取LCD\r\n");
xSemaphoreTake(xMutex, portMAX_DELAY); // 获取互斥体
printf("高优先级任务:占用LCD\r\n");
vTaskDelay(pdMS_TO_TICKS(500)); // 短时间使用
xSemaphoreGive(xMutex); // 释放互斥体
printf("高优先级任务:释放LCD\r\n");
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
// 主函数创建任务
int main(void)
{
// 硬件初始化...
if(xMutex != NULL)
{
xTaskCreate(vLowTask, "LowTask", 128, NULL, 1, NULL);
xTaskCreate(vMidTask, "MidTask", 128, NULL, 2, NULL);
xTaskCreate(vHighTask, "HighTask", 128, NULL, 3, NULL);
vTaskStartScheduler();
}
for(;;);
}
三、信号量使用注意事项
- 中断中仅能调用
xSemaphoreGiveFromISR,不能调用xSemaphoreTakeFromISR(中断不允许等待)。 - 互斥信号量仅能在任务中使用,且获取/释放必须在同一个任务中。
- 避免使用
portMAX_DELAY永久等待,否则可能导致任务死锁,建议设置合理超时。 - 信号量句柄需全局或通过参数传递,确保任务/中断能访问到。
总结
- FreeRTOS信号量分三类核心类型:二进制信号量(同步)、计数信号量(多资源)、互斥信号量(解决优先级反转的互斥)。
- 中断操作信号量必须用
FromISR后缀的API,并处理xHigherPriorityTaskWoken完成上下文切换。 - 互斥信号量是资源互斥的首选方案,二进制信号量更适合同步场景,计数信号量用于多相同资源管理。
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐



所有评论(0)