【FreeRTOS】 二值信号量与互斥量(CMSIS-RTOS v2 版本)
目录
一、基本概念(CMSIS-RTOS v2 视角)
1.二值信号量(Binary Semaphore)
2.互斥量(Mutex)
二、核心区别(CMSIS-RTOS v2 特性)
关键区别详解
1.所有权(核心差异)
2.优先级反转处理(互斥量核心特性)
(1)用二值信号量保护资源(会出现优先级反转)
(2)用互斥量保护资源(避免优先级反转)
三、使用方法(适配 CubeMX + Keil 工程)
1.二值信号量(同步场景)
(1)创建二值信号量
(2)获取信号量(任务中等待事件)
(3)释放信号量(任务 / ISR 中发通知)
2.互斥量(互斥场景)
(1)创建互斥量
(2)加锁(任务中访问资源前)
(3)解锁(任务中访问资源后)
四、核心注意事项(CubeMX 工程适配)
1.二值信号量注意事项
2.互斥量注意事项
3.通用注意事项(CubeMX 工程特有)
五、场景选择指南(CubeMX 工程实战)
总结
对于使用 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_AttrxBinarySem = 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 工程中正确使用:
-
同步用二值信号量:像 “门铃”,谁都能按(释放)、谁都能听(获取),用于 “通知事件”;
-
互斥用互斥量:像 “厕所钥匙”,谁拿谁用、用完自还,用于 “保护资源”,并能避免优先级反转。