【FreeRTOS】 二值信号量与互斥量(CMSIS-RTOS v2 版本)
摘要:本文分析了CubeMX配置FreeRTOS(CMSIS-RTOSv2接口)中二值信号量与互斥量的区别与使用方法。二值信号量主要用于任务间同步,无所有权概念;互斥量则用于资源保护,具有严格所有权和优先级继承特性。
目录
对于使用 CubeMX 配置 FreeRTOS(CMSIS-RTOS v2 接口)的开发者,二值信号量和互斥量的底层逻辑与原生 FreeRTOS 一致,但函数接口、参数格式有标准化差异。本文从 “功能用途”“核心特性”“使用场景” 三个维度,结合 CMSIS-RTOS v2 函数(适配 CubeMX 生成的 Keil 工程),梳理两者的区别与使用方法。
一、基本概念(CMSIS-RTOS v2 视角)
CMSIS-RTOS v2 是 ARM 推出的标准化 RTOS 接口,CubeMX 生成的 FreeRTOS 工程默认采用这套接口,核心是通过统一函数名和参数格式,降低跨 RTOS 移植成本。
1.二值信号量(Binary Semaphore)
类比 “开关”,仅两种状态:“有信号”(可用) 和 “无信号”(不可用),核心用于 任务间 / 任务与 ISR 间的同步(如 “事件通知”“唤醒任务”)。
-
CMSIS-RTOS v2 中,二值信号量通过
osSemaphoreId_t类型句柄管理,创建后默认状态为 “无信号”。 -
核心操作:
-
释放(发信号):
osSemaphoreRelease()(任务中释放)、osSemaphoreReleaseFromISR()(中断中释放); -
获取(等信号):
osSemaphoreAcquire()。
-
2.互斥量(Mutex)
类比 “钥匙”,用于 保护共享资源(如串口、传感器、全局变量),确保同一时间只有一个任务能访问资源。
-
CMSIS-RTOS v2 中,互斥量通过
osMutexId_t类型句柄管理,创建后默认状态为 “可用”(资源未被占用)。 -
核心操作:
-
加锁(拿钥匙):
osMutexAcquire(); -
解锁(还钥匙):
osMutexRelease()。
-
二、核心区别(CMSIS-RTOS v2 特性)

关键区别详解
1.所有权(核心差异)
-
二值信号量:无归属关系。例如: 任务 A 调用
osSemaphoreRelease()释放信号量(设为 “有信号”),任务 B 可调用osSemaphoreAcquire()获取,之后任务 C 还能再次释放 —— 谁都能 “发信号”,谁都能 “等信号”。 -
互斥量:严格的所有权。例如: 任务 A 调用
osMutexAcquire()加锁(拿到钥匙),则 只有任务 A 能调用osMutexRelease()解锁;若任务 B 试图释放任务 A 持有的互斥量,会返回osErrorResource错误,导致程序异常。
2.优先级反转处理(互斥量核心特性)
这是互斥量与二值信号量最关键的区别,结合 CMSIS-RTOS v2 行为举例:
假设有 3 个任务:高优先级任务 H、中优先级任务 M、低优先级任务 L,均需访问同一共享资源。
(1)用二值信号量保护资源(会出现优先级反转)
-
步骤:
-
低优先级任务 L 调用
osSemaphoreAcquire()获取二值信号量,开始访问资源; -
高优先级任务 H 就绪,调用
osSemaphoreAcquire()拿信号量失败,进入阻塞态; -
中优先级任务 M 就绪(优先级高于 L),抢占 L 的 CPU,导致 L 暂停访问资源;
-
M 运行期间,H 持续阻塞(因 L 无法释放信号量),最终 最高优先级的 H 被中优先级的 M 拖累—— 这就是优先级反转。
-
-
原因:二值信号量无 “优先级继承” 特性,无法阻止低优先级任务被中优先级任务抢占。
(2)用互斥量保护资源(避免优先级反转)
-
步骤:
-
低优先级任务 L 调用
osMutexAcquire()加锁,开始访问资源; -
高优先级任务 H 就绪,调用
osMutexAcquire()加锁失败,进入阻塞态; -
CMSIS-RTOS v2 检测到 “高优先级任务 H 等待 L 持有的互斥量”,自动将 L 的优先级临时提升至与 H 相同;
-
中优先级任务 M 就绪后,因优先级低于 “临时提权的 L”,无法抢占 L,L 可快速完成资源访问并解锁;
-
互斥量释放后,H 立即加锁访问资源,避免优先级反转。
-
-
原因:互斥量的 “优先级继承” 特性,确保持有资源的低优先级任务不被中优先级任务打断。
三、使用方法(适配 CubeMX + Keil 工程)
CubeMX 配置 FreeRTOS 后,会自动生成 CMSIS-RTOS v2 相关头文件(cmsis_os2.h)和初始化框架,以下代码可直接嵌入工程使用。
1.二值信号量(同步场景)
核心用于 “事件通知”(如中断唤醒任务、任务间同步),使用流程分 “创建 → 获取 → 释放” 三步。
(1)创建二值信号量
通过 osSemaphoreNew() 创建,参数需指定 “信号量计数”(二值信号量计数固定为 1)、“初始值”(0 表示默认无信号)和 “属性配置”(可选,如名称)。
#include "cmsis_os2.h"
// 定义二值信号量句柄(全局可见,供任务/ISR 访问)
osSemaphoreId_t xBinarySem;
// 信号量属性配置(可选,用于设置名称,调试用)
const osSemaphoreAttr_t BinarySem_Attr = {
.name = "BinarySem_Key" // 信号量名称,可在调试工具中识别
};
// 初始化二值信号量(通常在 main 函数或 RTOS 初始化钩子中调用)
void Sem_Init(void) {
// 创建二值信号量:计数=1(二值),初始值=0(无信号),属性=BinarySem_Attr
xBinarySem = osSemaphoreNew(1, 0, &BinarySem_Attr);
// 检查创建是否成功(CubeMX 生成的工程中,堆不足会返回 NULL)
if (xBinarySem == NULL) {
// 错误处理:如通过 SEGGER_RTT 打印日志
SEGGER_RTT_printf(0, "二值信号量创建失败!\r\n");
}
}
(2)获取信号量(任务中等待事件)
通过 osSemaphoreAcquire() 等待信号量,参数为 “信号量句柄” 和 “超时时间(ms)”,超时后返回错误。
// 等待信号量的任务(例如:按键事件处理任务)
void Task_WaitSem(void *argument) {
osStatus_t status; // 存储函数返回状态
for (;;) {
// 等待信号量,无限期阻塞(osWaitForever),直到有信号
status = osSemaphoreAcquire(xBinarySem, osWaitForever);
if (status == osOK) {
// 信号量获取成功:处理事件(如按键逻辑、数据解析)
SEGGER_RTT_printf(0, "收到信号,执行事件处理...\r\n");
} else {
// 异常处理(如超时、信号量无效)
SEGGER_RTT_printf(0, "获取信号量失败,状态:%d\r\n", status);
}
}
}
(3)释放信号量(任务 / ISR 中发通知)
-
任务中释放:直接调用
osSemaphoreRelease(); -
中断中释放:必须调用
osSemaphoreReleaseFromISR()(ISR 安全版本),避免调度错误。
// 示例1:任务中释放信号量(如数据采集完成后通知处理任务)
void Task_SendSem(void *argument) {
for (;;) {
// 模拟数据采集(如读取传感器)
SEGGER_RTT_printf(0, "数据采集完成,发送信号...\r\n");
// 释放信号量(设为“有信号”)
if (osSemaphoreRelease(xBinarySem) != osOK) {
SEGGER_RTT_printf(0, "任务中释放信号量失败!\r\n");
}
osDelay(1000); // 每隔1秒发送一次信号
}
}
// 示例2:中断中释放信号量(如按键中断唤醒任务)
void EXTI0_IRQHandler(void) {
// 检查中断标志(硬件相关,CubeMX 生成的 HAL 函数)
if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_0) != RESET) {
osStatus_t status;
// 中断中释放信号量,需传入“是否需要任务切换”的标志
status = osSemaphoreReleaseFromISR(xBinarySem, NULL);
if (status != osOK) {
// 中断中无法打印(可选通过全局变量标记错误)
}
// 清除中断标志
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);
}
}
2.互斥量(互斥场景)
核心用于 “保护共享资源”,使用流程分 “创建 → 加锁 → 解锁” 三步,需严格遵守 “谁加锁谁解锁” 的规则。
(1)创建互斥量
通过 osMutexNew() 创建,参数为 “属性配置”(可选,如名称、是否支持递归锁),默认支持优先级继承。
#include "cmsis_os2.h"
// 定义互斥量句柄(全局可见,保护共享资源如串口)
osMutexId_t xMutex;
// 互斥量属性配置(可选)
const osMutexAttr_t Mutex_Attr = {
.name = "Mutex_UART", // 互斥量名称,调试用
.attr_bits = osMutexRecursive // 可选:设置为递归互斥量(允许同一任务多次加锁)
};
// 初始化互斥量(同信号量,在 RTOS 初始化时调用)
void Mutex_Init(void) {
// 创建互斥量,传入属性配置(NULL 表示默认配置)
xMutex = osMutexNew(&Mutex_Attr);
if (xMutex == NULL) {
SEGGER_RTT_printf(0, "互斥量创建失败!\r\n");
}
}
(2)加锁(任务中访问资源前)
通过 osMutexAcquire() 加锁,参数为 “互斥量句柄” 和 “超时时间(ms)”,超时后无法访问资源。
// 任务1:使用串口打印(共享资源)
void Task_UART1(void *argument) {
osStatus_t status;
for (;;) {
// 加锁:最多等待 100ms,超时则放弃访问
status = osMutexAcquire(xMutex, 100);
if (status == osOK) {
// 加锁成功:安全访问共享资源(如串口打印)
SEGGER_RTT_printf(0, "任务1:正在使用串口...\r\n");
osDelay(50); // 模拟串口操作耗时
// 解锁:必须在同一任务中释放
osMutexRelease(xMutex);
} else {
// 加锁失败(超时或互斥量无效)
SEGGER_RTT_printf(0, "任务1:获取串口锁失败,状态:%d\r\n", status);
}
osDelay(200); // 任务周期
}
}
// 任务2:同样需要使用串口(与任务1互斥)
void Task_UART2(void *argument) {
osStatus_t status;
for (;;) {
status = osMutexAcquire(xMutex, 100);
if (status == osOK) {
SEGGER_RTT_printf(0, "任务2:正在使用串口...\r\n");
osDelay(50);
osMutexRelease(xMutex);
} else {
SEGGER_RTT_printf(0, "任务2:获取串口锁失败,状态:%d\r\n", status);
}
osDelay(300);
}
}
(3)解锁(任务中访问资源后)
通过 osMutexRelease() 解锁,必须由加锁的任务调用,否则返回 osErrorResource 错误,甚至导致系统异常。
// 错误示例:任务A加锁,任务B解锁
void Task_Error(void *argument) {
// 任务A加锁
osMutexAcquire(xMutex, osWaitForever);
SEGGER_RTT_printf(0, "任务A加锁成功\r\n");
// 任务B尝试解锁(错误!)
osMutexRelease(xMutex); // 若在任务B中调用,返回 osErrorResource
}
四、核心注意事项(CubeMX 工程适配)
1.二值信号量注意事项
-
初始化状态:
osSemaphoreNew(1, 0, ...)中 “初始值 = 0” 表示默认无信号,需先调用osSemaphoreRelease()才能让osSemaphoreAcquire()获取成功。 -
中断中必须用
FromISR版本:中断中释放信号量时,必须使用osSemaphoreReleaseFromISR(),不能用普通osSemaphoreRelease()(会破坏调度器状态)。 -
不适合保护资源:若用二值信号量替代互斥量保护资源,会因无优先级继承导致优先级反转,需避免。
-
超时时间单位:CMSIS-RTOS v2 中所有超时参数单位为 毫秒(ms)(原生 FreeRTOS 为节拍数),无需转换(CubeMX 已配置
osKernelGetTickFreq()自动适配)。
2.互斥量注意事项
-
严格所有权:仅加锁任务可解锁,跨任务解锁会导致错误(CubeMX 生成的工程中,错误状态可通过
osStatus_t返回值捕获)。 -
禁止在 ISR 中使用:
osMutexAcquire()可能阻塞(超时时间非 0),而 ISR 必须快速执行,不允许阻塞;且互斥量的优先级继承在 ISR 中无效。 -
递归锁慎用:通过
osMutexRecursive属性创建的递归互斥量,允许同一任务多次加锁,但需确保 “加锁次数 = 解锁次数”,否则会导致死锁。
// 递归锁正确用法
osMutexAcquire(xMutex, osWaitForever); // 第1次加锁
osMutexAcquire(xMutex, osWaitForever); // 第2次加锁(仅递归锁支持)
// 业务逻辑...
osMutexRelease(xMutex); // 第1次解锁
osMutexRelease(xMutex); // 第2次解锁(必须配对)
-
持有时间要短:加锁后仅执行 “访问共享资源” 的必要操作(如读写变量、调用外设函数),尽快解锁,避免其他任务长期等待。
3.通用注意事项(CubeMX 工程特有)
-
句柄有效性检查:CubeMX 生成的工程中,信号量 / 互斥量创建失败(返回 NULL)通常是因为 FreeRTOS 堆内存不足,需在
CubeMX → Middleware → FreeRTOS → Heap Size中增大堆大小(建议至少 2KB 以上)。 -
任务栈大小配置:调用信号量 / 互斥量函数会占用任务栈空间,CubeMX 中需为任务配置足够栈大小(
CubeMX → Tasks and Queues → 选择任务 → Stack Size,建议至少 512 字节)。 -
中断优先级配置:若在 ISR 中使用信号量,需确保中断优先级低于
configMAX_SYSCALL_INTERRUPT_PRIORITY(CubeMX 中在FreeRTOS → Configuration → Kernel Settings配置),否则无法调用FromISR版本函数。
五、场景选择指南(CubeMX 工程实战)

总结
CMSIS-RTOS v2 接口下,二值信号量和互斥量的核心逻辑与原生 FreeRTOS 一致,区别仅在于函数名和参数格式的标准化。记住以下核心原则,即可在 CubeMX 工程中正确使用:
-
同步用二值信号量:像 “门铃”,谁都能按(释放)、谁都能听(获取),用于 “通知事件”;
-
互斥用互斥量:像 “厕所钥匙”,谁拿谁用、用完自还,用于 “保护资源”,并能避免优先级反转。
更多推荐



所有评论(0)