文章总结(帮你们节约时间)
- μC/OS-II信号量是实现任务同步和互斥访问的强大机制,包括二值信号量、计数信号量、互斥信号量和递归信号量四种类型。
- 信号量的核心功能是通过OSSemCreate创建、OSSemPend获取和OSSemPost释放,同时提供了查询和无等待获取等辅助功能。
- 在STM32F103平台上,可以结合硬件特性使用信号量实现任务同步、资源保护、生产者-消费者模型等多种应用模式。
- 合理使用信号量可以避免死锁、优先级反转等常见问题,提高实时系统的可靠性和性能。
- 在实际系统设计中,应当遵循良好的命名规范、完整封装共享资源访问、设置合理的超时处理,避免信号量滥用。
操作系统中的交通管理员
想象一下,如果多个任务同时访问共享资源,就像繁忙十字路口的车辆一样,没有交通信号灯会怎样?没错,一片混乱!μC/OS-II中的信号量就像这样的交通信号灯,它们协调任务的执行顺序,确保资源访问的有序性。有没有想过为什么即使在资源有限的嵌入式系统中,我们仍能实现复杂的多任务操作?信号量功不可没!
信号量基本概念
信号量是操作系统中用于任务同步和互斥访问共享资源的重要机制。在μC/OS-II中,信号量通过一个计数值和一个等待任务列表来实现。当一个任务需要访问受信号量保护的资源时,它必须先获取该信号量;当完成操作后,则释放信号量。
μC/OS-II中提供了四种类型的信号量,每种都有其特定用途:
1. 二值信号量
二值信号量的计数值只能为0或1,就像一个开关,要么开要么关。当计数值为1时,表示资源可用;当为0时,表示资源已被占用。二值信号量主要用于同步任务执行或保护单一资源。
想象一下,二值信号量就像一个只有一把钥匙的房间。任何时候只有一个人能拿到钥匙并进入房间,其他人必须等待钥匙被归还。
2. 计数信号量
计数信号量的计数值可以大于1,通常用于管理多个相同资源的访问。例如,如果系统有5个串口设备,则可以创建初始值为5的计数信号量。每当一个任务使用一个串口时,计数值减1;当任务完成使用并释放串口时,计数值加1。
这就像一个有多把钥匙的储物柜系统。每个人可以借一把钥匙使用一个储物柜,当所有钥匙都被借出时,新来的人必须等待。
3. 互斥信号量
互斥信号量是特殊的二值信号量,具有优先级继承机制,用于解决优先级反转问题。当高优先级任务等待低优先级任务释放互斥信号量时,低优先级任务会暂时继承高优先级任务的优先级,加快资源释放过程。
这就像VIP通道服务:当一位VIP顾客在等待普通顾客完成服务时,系统会将该普通顾客临时提升为"加急处理",确保VIP顾客不会等待太久。
4. 递归信号量
递归信号量允许同一个任务多次获取同一个信号量而不会死锁。系统会记录获取次数,只有当释放次数与获取次数相等时,其他任务才能获取该信号量。
这就像一个人可以多次进入自己已经锁定的房间,但必须确保每次进入都有相应的离开,最后一次离开时才真正上锁。
二值信号量运作机制
二值信号量是μC/OS-II中最基本的同步工具,其工作原理看似简单却十分精妙。让我们深入了解它的内部运作机制。
信号量结构体
在μC/OS-II中,二值信号量由以下结构体表示:
typedef struct {
INT8U OSEventType; /* 事件类型,表示这是一个信号量 */
void *OSEventPtr; /* 指向事件控制块的指针 */
INT16U OSEventCnt; /* 信号量计数器,二值信号量为0或1 */
OS_EVENT *OSEventList; /* 等待任务列表 */
/* 其他成员... */
} OS_EVENT;
创建二值信号量
创建二值信号量时,系统会分配一个OS_EVENT结构体,并将OSEventCnt初始化为指定值(0或1)。
OS_EVENT *OSSemCreate(INT16U cnt);
当执行此函数时,μC/OS-II会从系统的事件控制块池中获取一个空闲事件控制块,初始化其成员,并返回指向该控制块的指针。
获取信号量过程
当任务尝试获取二值信号量时:
- 首先检查信号量计数值
- 如果计数值为1(资源可用),则将其减为0并立即返回
- 如果计数值为0(资源不可用),则:
- 根据调用选项决定是否等待
- 如选择等待,当前任务被挂起并加入等待列表
- μC/OS-II调度器会选择另一个就绪任务执行
这个过程必须是原子操作,在μC/OS-II中通过禁用中断来实现。
释放信号量过程
当任务完成资源使用并释放信号量时:
- 检查是否有任务在等待该信号量
- 如有等待任务,将最高优先级的等待任务移至就绪状态
- 如无等待任务,将信号量计数值增加到1
- 根据优先级抢占规则决定是否进行任务切换
计数信号量运作机制
计数信号量相比二值信号量更为灵活,适用于管理多个同类资源或进行事件计数。其内部机制值得我们详细了解。
计数信号量的特性
计数信号量的核心特性在于其计数值可以大于1。其计数上限为65535(对于16位整数),这意味着它可以管理多达65535个资源实例。
创建计数信号量
创建计数信号量的过程与二值信号量类似,但初始计数值可以设置为任意值(在有效范围内):
OS_EVENT *OSSemCreate(INT16U cnt); // cnt可以大于1
获取与释放机制
计数信号量的获取与释放遵循以下规则:
-
获取操作:
- 检查计数值是否大于0
- 如果是,将计数值减1并返回成功
- 如果否,根据调用选项决定是否等待
-
释放操作:
- 如有任务等待,唤醒优先级最高的任务
- 如无任务等待,将计数值加1(注意防止溢出)
计数信号量应用场景
计数信号量特别适合以下场景:
- 资源池管理:管理有限数量的相同资源,如内存块、通信缓冲区等
- 事件计数:记录特定事件的发生次数
- 生产者-消费者问题:协调生产者和消费者之间的速度差异
例如,在管理多个串口设备时,可以这样使用:
OS_EVENT *UartSem; // 串口信号量
void InitUart(void) {
UartSem = OSSemCreate(5); // 系统有5个串口可用
}
void TaskUseUart(void *p_arg) {
INT8U err;
while (1) {
OSSemPend(UartSem, 0, &err); // 获取一个串口
// 使用串口...
OSSemPost(UartSem); // 释放串口
}
}
常用信号量函数接口讲解
μC/OS-II提供了一组完整的API函数来操作信号量。这些函数使得任务同步和互斥操作变得简单而强大。让我们详细了解这些关键函数。
OSSemCreate - 创建信号量
这是创建新信号量的基础函数。
参数 | 类型 | 说明 |
---|---|---|
cnt | INT16U | 信号量初始计数值,0≤cnt≤65535 |
返回值 | OS_EVENT* | 成功时返回信号量指针,失败时返回NULL |
OS_EVENT *sem;
sem = OSSemCreate(1); // 创建初始值为1的信号量
if (sem == NULL) {
// 创建失败处理...
}
OSSemPend - 获取信号量
此函数用于获取(等待)信号量。
参数 | 类型 | 说明 |
---|---|---|
pevent | OS_EVENT* | 要获取的信号量指针 |
timeout | INT16U | 等待超时时间(ticks),0表示永久等待 |
perr | INT8U* | 错误代码指针 |
返回值 | void | 无返回值 |
错误代码包括:
- OS_NO_ERR: 成功获取信号量
- OS_TIMEOUT: 等待超时
- OS_ERR_PEND_ISR: 在中断中调用此函数
- OS_ERR_EVENT_TYPE: 事件不是信号量
void Task(void *p_arg) {
INT8U err;
OSSemPend(sem, 100, &err); // 等待最多100个时钟节拍
if (err == OS_NO_ERR) {
// 成功获取信号量
} else if (err == OS_TIMEOUT) {
// 等待超时处理
}
}
OSSemPost - 释放信号量
此函数用于释放(发送)信号量。
参数 | 类型 | 说明 |
---|---|---|
pevent | OS_EVENT* | 要释放的信号量指针 |
返回值 | INT8U | 错误代码 |
返回值包括:
- OS_NO_ERR: 成功释放
- OS_SEM_OVF: 信号量计数溢出
- OS_ERR_EVENT_TYPE: 事件不是信号量
INT8U err;
err = OSSemPost(sem);
if (err != OS_NO_ERR) {
// 处理错误
}
OSSemAccept - 无等待获取信号量
此函数尝试获取信号量但不等待。
参数 | 类型 | 说明 |
---|---|---|
pevent | OS_EVENT* | 要获取的信号量指针 |
返回值 | INT16U | 操作后信号量的计数值 |
INT16U cnt;
cnt = OSSemAccept(sem);
if (cnt > 0) {
// 成功获取信号量
} else {
// 信号量不可用
}
OSSemSet - 设置信号量计数值
此函数直接设置信号量的计数值。
参数 | 类型 | 说明 |
---|---|---|
pevent | OS_EVENT* | 信号量指针 |
cnt | INT16U | 新的计数值 |
perr | INT8U* | 错误代码指针 |
返回值 | void | 无返回值 |
INT8U err;
OSSemSet(sem, 5, &err); // 将信号量计数值设为5
OSSemQuery - 查询信号量状态
此函数获取信号量的当前状态。
参数 | 类型 | 说明 |
---|---|---|
pevent | OS_EVENT* | 要查询的信号量指针 |
p_sem_data | OS_SEM_DATA* | 存储查询结果的指针 |
返回值 | INT8U | 错误代码 |
OS_SEM_DATA sem_data;
INT8U err;
err = OSSemQuery(sem, &sem_data);
if (err == OS_NO_ERR) {
printf("当前信号量计数值: %d\n", sem_data.OSCnt);
printf("等待任务数: %d\n", sem_data.OSEventGrp ? 1 : 0);
}
信号量获取函数详解
信号量获取是同步过程中最关键的环节,μC/OS-II提供了多种获取信号量的方法,适应不同的应用场景。让我们深入分析这些函数的内部工作原理和使用技巧。
OSSemPend内部实现分析
OSSemPend
是获取信号量的主要函数,它的内部实现揭示了μC/OS-II任务同步的核心机制。
void OSSemPend(OS_EVENT *pevent, INT16U timeout, INT8U *perr)
{
/* 保存处理器寄存器 */
/* 关中断 */
if (pevent->OSEventType != OS_EVENT_TYPE_SEM) {
*perr = OS_ERR_EVENT_TYPE;
return;
}
if (OSIntNesting > 0) {
*perr = OS_ERR_PEND_ISR;
return;
}
if (pevent->OSEventCnt > 0) {
pevent->OSEventCnt--; /* 信号量可用,计数值减1 */
*perr = OS_NO_ERR;
} else {
/* 信号量不可用,将任务加入等待列表 */
OSTCBCur->OSTCBStat |= OS_STAT_SEM;
OSTCBCur->OSTCBEventPtr = pevent;
OS_EventTaskWait(pevent);
if (timeout > 0) {
OSTimeDly(timeout); /* 设置超时 */
}
OS_Sched(); /* 调度其他任务 */
*perr = OSTCBCur->OSTCBStatPend;
}
/* 开中断 */
/* 恢复寄存器 */
}
获取信号量的不同方式
μC/OS-II提供了多种获取信号量的方法,每种都有其特定用途:
-
阻塞式获取:使用
OSSemPend
并设置timeout=0,任务将无限期等待直到获取信号量。OSSemPend(sem, 0, &err); // 永久等待
-
超时式获取:使用
OSSemPend
并设置非零timeout,任务最多等待指定时间。OSSemPend(sem, 100, &err); // 最多等待100个时钟节拍
-
非阻塞式获取:使用
OSSemAccept
,任务尝试获取信号量但不等待。cnt = OSSemAccept(sem); // 立即返回
等待超时处理策略
当使用超时式获取信号量时,需要妥善处理可能的超时情况:
INT8U err;
OSSemPend(sem, 200, &err);
if (err == OS_NO_ERR) {
// 成功获取信号量,执行正常操作
// ...
OSSemPost(sem); // 完成后释放信号量
} else if (err == OS_TIMEOUT) {
// 等待超时,执行恢复策略
// 可能需要重试、报告错误或选择替代方案
} else {
// 处理其他错误
}
优先级反转问题与解决方案
当使用二值信号量进行任务间互斥时,可能出现优先级反转问题。这是一种现象,其中高优先级任务因等待被低优先级任务持有的资源而被阻塞,同时中优先级任务却可以执行,导致系统优先级机制失效。
μC/OS-II通过互斥信号量解决这个问题:
OS_EVENT *mutex;
mutex = OSMutexCreate(priority_ceiling, &err); // 创建互斥信号量
// 在高优先级任务中
OSMutexPend(mutex, timeout, &err); // 获取互斥信号量
// 访问共享资源
OSMutexPost(mutex); // 释放互斥信号量
互斥信号量使用优先级继承或优先级天花板协议,当高优先级任务等待时,持有互斥量的低优先级任务会暂时继承高优先级任务的优先级,从而防止中优先级任务的干扰。
信号量实践应用
理论终归是理论,只有将信号量应用到实际开发中,才能真正领会其精髓。下面我们通过一系列在STM32F103平台上的实际例子,展示信号量在不同场景下的应用。
硬件环境与基础设置
首先,确保我们的开发环境已正确配置:
- 硬件:STM32F103系列微控制器(如STM32F103ZET6)
- 开发工具:Keil MDK 5.x
- RTOS:μC/OS-II V2.9x
- 外设:按需配置(本例中使用LED、按键和UART)
在开始前,我们需要完成μC/OS-II的移植和初始化:
#include "includes.h"
static OS_STK StartTaskStk[TASK_STK_SIZE];
int main(void)
{
BSP_Init(); // 板级支持包初始化
OSInit(); // μC/OS-II初始化
// 创建起始任务
OSTaskCreate(StartTask, (void*)0, &StartTaskStk[TASK_STK_SIZE-1], START_TASK_PRIO);
OSStart(); // 启动μC/OS-II调度器
return 0;
}
// 起始任务
static void StartTask(void *p_arg)
{
OS_CPU_SR cpu_sr = 0;
p_arg = p_arg; // 防止编译警告
BSP_InitTick(); // 初始化系统时钟节拍
OS_ENTER_CRITICAL(); // 进入临界区
// 在这里创建其他任务和信号量
OS_EXIT_CRITICAL(); // 退出临界区
OSTaskDel(OS_PRIO_SELF); // 删除自身任务
}
示例1:使用二值信号量实现任务同步
我们来实现一个经典场景:一个任务检测按键状态,当按键按下时通知另一个任务执行相应操作。
OS_EVENT *KeySem; // 按键信号量
// 按键检测任务
static void KeyTask(void *p_arg)
{
while (1) {
if (KEY_Scan(0) == 1) { // 检测到按键按下
OSSemPost(KeySem); // 释放信号量,通知处理任务
}
OSTimeDly(10); // 延时10个时钟节拍
}
}
// 按键处理任务
static void KeyHandlerTask(void *p_arg)
{
INT8U err;
while (1) {
OSSemPend(KeySem, 0, &err); // 等待按键信号量
if (err == OS_NO_ERR) {
LED_Toggle(0); // 切换LED状态
printf("按键被按下,LED状态切换\r\n");
}
}
}
// 在StartTask中创建任务和信号量
KeySem = OSSemCreate(0); // 创建初值为0的信号量
OSTaskCreate(KeyTask, (void*)0, &KeyTaskStk[TASK_STK_SIZE-1], KEY_TASK_PRIO);
OSTaskCreate(KeyHandlerTask, (void*)0, &KeyHandlerTaskStk[TASK_STK_SIZE-1], KEY_HANDLER_TASK_PRIO);
示例2:使用互斥信号量保护UART通信
当多个任务需要使用UART发送数据时,我们需要确保在一个任务发送期间,其他任务不会干扰。
OS_EVENT *UartMutex; // UART互斥信号量
// 发送字符串函数(受互斥信号量保护)
void UART_SendString_Safe(const char *str)
{
INT8U err;
OSMutexPend(UartMutex, 0, &err); // 获取UART互斥信号量
if (err == OS_NO_ERR) {
UART_SendString(str); // 发送字符串
OSMutexPost(UartMutex); // 释放互斥信号量
}
}
// 任务1定期发送数据
static void Task1(void *p_arg)
{
while (1) {
UART_SendString_Safe("Task1 is running...\r\n");
OSTimeDly(200);
}
}
// 任务2定期发送数据
static void Task2(void *p_arg)
{
while (1) {
UART_SendString_Safe("Task2 is running...\r\n");
OSTimeDly(300);
}
}
// 在StartTask中初始化
UartMutex = OSMutexCreate(UART_MUTEX_PRIO, &err); // 创建UART互斥信号量
示例3:使用计数信号量实现资源池
假设我们有多个DMA通道,需要在多任务环境下管理这些有限资源。
OS_EVENT *DMASem; // DMA资源信号量
INT8U DMAChannelMap[5]; // 记录DMA通道状态
// 初始化DMA资源管理
void DMA_ResourceInit(void)
{
INT8U i;
for (i = 0; i < 5; i++) {
DMAChannelMap[i] = 0; // 0表示空闲
}
DMASem = OSSemCreate(5); // 创建计数为5的信号量
}
// 申请DMA通道
INT8U DMA_RequestChannel(void)
{
INT8U i;
INT8U err;
OSSemPend(DMASem, 0, &err); // 等待可用DMA资源
if (err != OS_NO_ERR) {
return 0xFF; // 错误
}
OS_ENTER_CRITICAL();
for (i = 0; i < 5; i++) {
if (DMAChannelMap[i] == 0) {
DMAChannelMap[i] = 1; // 标记为已使用
OS_EXIT_CRITICAL();
return i + 1; // 返回DMA通道号(1-5)
}
}
OS_EXIT_CRITICAL();
return 0xFF; // 不应该执行到这里
}
// 释放DMA通道
void DMA_ReleaseChannel(INT8U channel)
{
if (channel > 0 && channel <= 5) {
OS_ENTER_CRITICAL();
DMAChannelMap[channel-1] = 0; // 标记为空闲
OS_EXIT_CRITICAL();
OSSemPost(DMASem); // 释放信号量
}
}
示例4:使用信号量实现生产者-消费者模型
在数据采集系统中,一个经典场景是一个任务采集数据(生产者),另一个任务处理数据(消费者)。
#define BUFFER_SIZE 10
typedef struct {
INT16U data[BUFFER_SIZE];
INT8U in; // 写入索引
INT8U out; // 读取索引
INT8U count; // 当前数据数量
} CircularBuffer;
CircularBuffer DataBuffer;
OS_EVENT *DataSem; // 数据可用信号量
OS_EVENT *SpaceSem; // 缓冲区空间信号量
OS_EVENT *BufferMutex; // 缓冲区互斥信号量
// 初始化缓冲区和信号量
void Buffer_Init(void)
{
DataBuffer.in = 0;
DataBuffer.out = 0;
DataBuffer.count = 0;
DataSem = OSSemCreate(0); // 初始无数据可用
SpaceSem = OSSemCreate(BUFFER_SIZE); // 初始缓冲区全空
BufferMutex = OSMutexCreate(BUFFER_MUTEX_PRIO, &err);
}
// 生产者任务:采集ADC数据并存入缓冲区
static void ProducerTask(void *p_arg)
{
INT8U err;
INT16U adc_value;
while (1) {
adc_value = ADC_GetValue(); // 获取ADC值
OSSemPend(SpaceSem, 0, &err); // 等待缓冲区空间
if (err == OS_NO_ERR) {
OSMutexPend(BufferMutex, 0, &err); // 获取缓冲区访问权
DataBuffer.data[DataBuffer.in] = adc_value;
DataBuffer.in = (DataBuffer.in + 1) % BUFFER_SIZE;
DataBuffer.count++;
OSMutexPost(BufferMutex); // 释放缓冲区访问权
OSSemPost(DataSem); // 通知有新数据可用
}
OSTimeDly(50); // 采样周期
}
}
// 消费者任务:从缓冲区读取数据并处理
static void ConsumerTask(void *p_arg)
{
INT8U err;
INT16U data;
while (1) {
OSSemPend(DataSem, 0, &err); // 等待数据可用
if (err == OS_NO_ERR) {
OSMutexPend(BufferMutex, 0, &err); // 获取缓冲区访问权
data = DataBuffer.data[DataBuffer.out];
DataBuffer.out = (DataBuffer.out + 1) % BUFFER_SIZE;
DataBuffer.count--;
OSMutexPost(BufferMutex); // 释放缓冲区访问权
OSSemPost(SpaceSem); // 通知有新空间可用
// 处理数据
ProcessData(data);
}
}
}
示例5:使用信号量实现周期性任务触发
通过定时器中断和信号量,我们可以实现精确的周期性任务。
OS_EVENT *TimerSem; // 定时器信号量
// 定时器中断服务程序
void TIM3_IRQHandler(void)
{
OSIntEnter(); // 进入中断
if (TIM_GetITStatus(TIM3, TIM_IT_Update) == SET) {
TIM_ClearITPendingBit(TIM3, TIM_IT_Update);
OSSemPost(TimerSem); // 在中断中释放信号量
}
OSIntExit(); // 退出中断
}
// 周期性任务
static void PeriodicTask(void *p_arg)
{
INT8U err;
// 初始化定时器TIM3,配置为1ms中断
TIM3_Config();
while (1) {
OSSemPend(TimerSem, 0, &err); // 等待定时器中断释放信号量
if (err == OS_NO_ERR) {
// 执行周期性操作,精确定时
LED_Toggle(1);
// 其他操作...
}
}
}
// 在StartTask中初始化
TimerSem = OSSemCreate(0); // 创建定时器信号量
信号量常见问题与解决方法
在使用μC/OS-II信号量的过程中,可能会遇到各种问题。让我们看看一些常见的陷阱及其解决方法。
死锁问题
死锁是指两个或多个任务都在等待对方持有的资源,导致所有相关任务永久阻塞。
典型场景:任务A持有资源X并等待资源Y,同时任务B持有资源Y并等待资源X。
解决方法:
- 资源排序:按照固定顺序获取多个资源
- 超时机制:使用非永久等待方式获取资源
- 死锁检测:定期检查系统是否存在死锁状态
// 错误示例(可能导致死锁)
void TaskA(void *p_arg)
{
while (1) {
OSSemPend(SemX, 0, &err); // 获取资源X
// ... 一些操作 ...
OSSemPend(SemY, 0, &err); // 获取资源Y
// 使用资源X和Y
OSSemPost(SemY); // 释放资源Y
OSSemPost(SemX); // 释放资源X
}
}
void TaskB(void *p_arg)
{
while (1) {
OSSemPend(SemY, 0, &err); // 获取资源Y
// ... 一些操作 ...
OSSemPend(SemX, 0, &err); // 获取资源X
// 使用资源Y和X
OSSemPost(SemX); // 释放资源X
OSSemPost(SemY); // 释放资源Y
}
}
// 正确示例(避免死锁)
void TaskA(void *p_arg)
{
while (1) {
OSSemPend(SemX, 0, &err); // 始终先获取资源X
OSSemPend(SemY, 0, &err); // 再获取资源Y
// 使用资源X和Y
OSSemPost(SemY); // 先释放资源Y
OSSemPost(SemX); // 再释放资源X
}
}
void TaskB(void *p_arg)
{
while (1) {
OSSemPend(SemX, 0, &err); // 始终先获取资源X
OSSemPend(SemY, 0, &err); // 再获取资源Y
// 使用资源X和Y
OSSemPost(SemY); // 先释放资源Y
OSSemPost(SemX); // 再释放资源X
}
}
优先级反转
前面我们提到过优先级反转问题,这里再详细探讨一下:
问题描述:低优先级任务持有高优先级任务需要的资源,但中优先级任务抢占了低优先级任务,导致高优先级任务长时间等待。
解决方法:
- 使用互斥信号量(最佳方案)
- 禁止任务抢占(在关键代码段)
- 优先级天花板协议
// 使用互斥信号量解决优先级反转
OS_EVENT *uart_mutex;
void Init(void)
{
uart_mutex = OSMutexCreate(UART_MUTEX_PRIO, &err);
}
void HighPriorityTask(void *p_arg)
{
INT8U err;
while (1) {
// ... 一些操作 ...
OSMutexPend(uart_mutex, 0, &err); // 获取UART互斥量
// 使用UART
OSMutexPost(uart_mutex); // 释放UART互斥量
}
}
void LowPriorityTask(void *p_arg)
{
INT8U err;
while (1) {
// ... 一些操作 ...
OSMutexPend(uart_mutex, 0, &err); // 获取UART互斥量
// 使用UART(如果高优先级任务等待,此任务会暂时继承高优先级)
OSMutexPost(uart_mutex); // 释放UART互斥量
}
}
信号量计数溢出
当使用计数信号量时,如果不小心,可能会导致计数值溢出。
问题描述:多次调用OSSemPost而没有对应的OSSemPend,导致计数值达到上限(65535)并溢出。
解决方法:
- 在释放信号量前检查当前计数值
- 设计良好的2. 设计良好的应用架构,确保每次获取都有对应的释放
- 实现自定义包装函数,防止错误使用
// 安全的信号量释放函数
INT8U SafeSemPost(OS_EVENT *pevent)
{
INT16U count;
INT8U err;
OS_ENTER_CRITICAL();
count = pevent->OSEventCnt;
if (count >= 65535) { // 接近最大值,防止溢出
OS_EXIT_CRITICAL();
return OS_SEM_OVF;
}
OS_EXIT_CRITICAL();
return OSSemPost(pevent);
}
在中断中使用信号量
在中断服务程序中使用信号量需要特别小心,因为ISR中不能调用可能导致任务阻塞的函数。
问题描述:在ISR中调用OSSemPend函数会导致系统崩溃。
解决方法:
- 在ISR中只使用OSSemPost
- 使用特定的ISR版本函数
// 中断服务程序
void EXTI0_IRQHandler(void)
{
OSIntEnter(); // 进入中断
if (EXTI_GetITStatus(EXTI_Line0) != RESET) {
EXTI_ClearITPendingBit(EXTI_Line0);
// 不能在这里使用OSSemPend!
OSSemPost(ButtonSem); // 这是安全的
}
OSIntExit(); // 退出中断
}
共享资源保护不完整
不正确的信号量使用可能导致共享资源保护不完整。
问题描述:代码中只保护了部分访问共享资源的路径,导致仍有未保护的访问可能。
解决方法:
- 集中封装所有对共享资源的访问
- 代码审查确保所有访问路径都受保护
// 全局缓冲区
uint8_t SharedBuffer[BUFFER_SIZE];
// 错误示例:不同函数使用不同方式访问共享资源
void Function1(void)
{
OSSemPend(BufferSem, 0, &err);
// 操作SharedBuffer
OSSemPost(BufferSem);
}
void Function2(void)
{
// 直接访问SharedBuffer,没有保护!
// 这可能导致与Function1竞争条件
}
// 正确示例:集中所有访问
void WriteBuffer(uint8_t *data, uint16_t len)
{
OSSemPend(BufferSem, 0, &err);
// 操作SharedBuffer
OSSemPost(BufferSem);
}
void ReadBuffer(uint8_t *data, uint16_t *len)
{
OSSemPend(BufferSem, 0, &err);
// 从SharedBuffer读取数据
OSSemPost(BufferSem);
}
// 所有地方都通过这些函数访问SharedBuffer
void Function1(void)
{
uint8_t data[10] = {0};
WriteBuffer(data, 10);
}
void Function2(void)
{
uint8_t data[10];
uint16_t len;
ReadBuffer(data, &len);
}
信号量性能优化
在资源有限的嵌入式系统中,信号量操作的性能至关重要。以下是一些优化信号量使用的技巧。
减少信号量操作频率
信号量操作涉及任务切换和系统调度,较为耗时。我们应尽量减少不必要的信号量操作。
// 低效方式:频繁获取释放信号量
void ProcessData(void)
{
for (int i = 0; i < 1000; i++) {
OSSemPend(DataSem, 0, &err);
// 处理少量数据
OSSemPost(DataSem);
}
}
// 优化方式:一次性获取更多数据
void ProcessData(void)
{
OSSemPend(DataSem, 0, &err);
// 一次性处理大量数据
OSSemPost(DataSem);
}
使用适当类型的信号量
根据应用场景选择合适的信号量类型,可以显著提升性能:
- 单纯同步使用二值信号量
- 资源计数使用计数信号量
- 需要优先级继承时使用互斥信号量
合理设置等待超时
避免无限期等待,合理设置超时时间可以防止任务长时间阻塞,提高系统响应性。
// 设置合理的超时时间
OSSemPend(ResourceSem, 100, &err); // 最多等待100个时钟节拍
if (err == OS_TIMEOUT) {
// 执行替代方案或错误处理
}
批量处理和信号量粒度
根据资源特性和系统要求,调整信号量保护的粒度:
- 粒度太细:信号量操作开销大
- 粒度太粗:可能导致任务阻塞时间过长
// 适当粒度的资源保护示例
void ProcessMultipleChannels(void)
{
// 每个通道使用独立的信号量,而不是一个全局信号量
for (int i = 0; i < CHANNEL_COUNT; i++) {
OSSemPend(ChannelSem[i], 0, &err);
ProcessChannel(i);
OSSemPost(ChannelSem[i]);
}
}
实际案例分析:多传感器数据采集系统
让我们通过一个完整的实际案例,综合运用μC/OS-II信号量。假设我们需要设计一个基于STM32F103的多传感器数据采集系统,包含温湿度传感器、光照传感器和红外传感器,并通过串口将数据发送到PC。
系统需求
- 温湿度传感器每2秒读取一次
- 光照传感器每1秒读取一次
- 红外传感器检测到物体移动时产生中断
- 所有数据通过同一个UART发送到PC
- 系统需要响应用户按键来切换工作模式
系统架构设计
我们将使用以下信号量:
- 温湿度数据就绪信号量(二值)
- 光照数据就绪信号量(二值)
- 红外事件信号量(二值)
- UART访问互斥信号量(互斥)
- 模式切换信号量(二值)
// 信号量定义
OS_EVENT *TempHumReadySem; // 温湿度数据就绪信号量
OS_EVENT *LightReadySem; // 光照数据就绪信号量
OS_EVENT *IREventSem; // 红外事件信号量
OS_EVENT *UARTMutex; // UART访问互斥信号量
OS_EVENT *ModeChangeSem; // 模式切换信号量
任务设计
我们设计以下任务:
- 温湿度采集任务
- 光照采集任务
- 红外处理任务
- 数据处理和发送任务
- 按键检测任务
代码实现
首先是系统初始化:
void SystemInit(void)
{
// 硬件初始化
BSP_Init();
UART_Config();
DHT11_Init();
LightSensor_Init();
IR_Init();
KEY_Init();
// 创建信号量
TempHumReadySem = OSSemCreate(0);
LightReadySem = OSSemCreate(0);
IREventSem = OSSemCreate(0);
UARTMutex = OSMutexCreate(UART_MUTEX_PRIO, &err);
ModeChangeSem = OSSemCreate(0);
// 创建任务
OSTaskCreate(TempHumTask, (void*)0, &TempHumTaskStk[TASK_STK_SIZE-1], TEMP_HUM_TASK_PRIO);
OSTaskCreate(LightTask, (void*)0, &LightTaskStk[TASK_STK_SIZE-1], LIGHT_TASK_PRIO);
OSTaskCreate(IRTask, (void*)0, &IRTaskStk[TASK_STK_SIZE-1], IR_TASK_PRIO);
OSTaskCreate(DataTask, (void*)0, &DataTaskStk[TASK_STK_SIZE-1], DATA_TASK_PRIO);
OSTaskCreate(KeyTask, (void*)0, &KeyTaskStk[TASK_STK_SIZE-1], KEY_TASK_PRIO);
}
各个传感器任务实现:
// 温湿度采集任务
static void TempHumTask(void *p_arg)
{
float temperature, humidity;
while (1) {
if (DHT11_Read(&temperature, &humidity) == 0) {
// 读取成功,更新全局数据
OS_ENTER_CRITICAL();
g_temperature = temperature;
g_humidity = humidity;
OS_EXIT_CRITICAL();
// 发送信号量通知数据就绪
OSSemPost(TempHumReadySem);
}
// 每2秒读取一次
OSTimeDlyHMSM(0, 0, 2, 0);
}
}
// 光照采集任务
static void LightTask(void *p_arg)
{
uint16_t light_value;
while (1) {
light_value = LightSensor_Read();
// 更新全局数据
OS_ENTER_CRITICAL();
g_light = light_value;
OS_EXIT_CRITICAL();
// 发送信号量通知数据就绪
OSSemPost(LightReadySem);
// 每1秒读取一次
OSTimeDlyHMSM(0, 0, 1, 0);
}
}
// 红外处理任务
static void IRTask(void *p_arg)
{
INT8U err;
while (1) {
// 等待红外信号量(由中断触发)
OSSemPend(IREventSem, 0, &err);
if (err == OS_NO_ERR) {
// 更新红外状态
OS_ENTER_CRITICAL();
g_ir_detected = 1;
g_ir_time = OSTimeGet();
OS_EXIT_CRITICAL();
// LED指示
LED_On(IR_LED);
OSTimeDlyHMSM(0, 0, 0, 500);
LED_Off(IR_LED);
}
}
}
数据处理和发送任务:
// 数据处理和发送任务
static void DataTask(void *p_arg)
{
INT8U err;
char buffer[100];
uint8_t mode = 0; // 0:所有数据 1:仅温湿度 2:仅光照 3:仅红外
while (1) {
// 检查模式是否变更
if (OSSemAccept(ModeChangeSem) > 0) {
mode = (mode + 1) % 4;
OSMutexPend(UARTMutex, 0, &err);
sprintf(buffer, "Mode changed to: %d\r\n", mode);
UART_SendString(buffer);
OSMutexPost(UARTMutex);
}
// 处理温湿度数据
if (mode == 0 || mode == 1) {
if (OSSemAccept(TempHumReadySem) > 0) {
OSMutexPend(UARTMutex, 0, &err);
sprintf(buffer, "Temp: %.1f°C, Humidity: %.1f%%\r\n",
g_temperature, g_humidity);
UART_SendString(buffer);
OSMutexPost(UARTMutex);
}
}
// 处理光照数据
if (mode == 0 || mode == 2) {
if (OSSemAccept(LightReadySem) > 0) {
OSMutexPend(UARTMutex, 0, &err);
sprintf(buffer, "Light: %u lux\r\n", g_light);
UART_SendString(buffer);
OSMutexPost(UARTMutex);
}
}
// 处理红外数据
if ((mode == 0 || mode == 3) && g_ir_detected) {
OSMutexPend(UARTMutex, 0, &err);
sprintf(buffer, "IR detected at: %u ms\r\n", g_ir_time);
UART_SendString(buffer);
OSMutexPost(UARTMutex);
OS_ENTER_CRITICAL();
g_ir_detected = 0;
OS_EXIT_CRITICAL();
}
// 短暂延时,避免过度占用CPU
OSTimeDlyHMSM(0, 0, 0, 10);
}
}
按键处理和中断处理:
// 按键检测任务
static void KeyTask(void *p_arg)
{
while (1) {
if (KEY_Scan(0) == 1) { // 检测到按键按下
OSSemPost(ModeChangeSem); // 发送模式切换信号量
}
OSTimeDlyHMSM(0, 0, 0, 10);
}
}
// 红外中断服务程序
void EXTI1_IRQHandler(void)
{
OSIntEnter();
if (EXTI_GetITStatus(EXTI_Line1) != RESET) {
EXTI_ClearITPendingBit(EXTI_Line1);
OSSemPost(IREventSem); // 发送红外事件信号量
}
OSIntExit();
}
进阶话题:信号量与其他同步机制的比较
μC/OS-II除了信号量外,还提供了其他同步机制。让我们比较它们的特点,以便在实际开发中作出明智选择。
信号量与消息邮箱
消息邮箱:允许任务间传递一个指针大小的消息,而且只能存储一个消息。
特性 | 信号量 | 消息邮箱 |
---|---|---|
数据传递 | 不直接传递数据 | 可传递一个指针大小的数据 |
存储容量 | 计数值(多个事件) | 单个消息 |
主要用途 | 同步、互斥 | 数据交换 |
多任务使用 | 多个任务可获取 | 只有一个任务可获取一个邮件 |
适用场景:当需要在任务间传递实际数据而非仅同步时,消息邮箱更合适。
// 使用消息邮箱传递数据
OS_EVENT *Mbox;
Mbox = OSMboxCreate((void*)0); // 创建消息邮箱
// 发送数据
int *data = malloc(sizeof(int));
*data = 100;
OSMboxPost(Mbox, (void*)data);
// 接收数据
void *msg;
INT8U err;
msg = OSMboxPend(Mbox, 0, &err);
int received = *(int*)msg;
free(msg);
信号量与消息队列
消息队列:允许任务间传递多个消息,形成一个先进先出的队列。
特性 | 信号量 | 消息队列 |
---|---|---|
数据传递 | 不直接传递数据 | 可传递多个指针大小的数据 |
存储容量 | 计数值 | 多个消息 |
消息顺序 | 无序 | 先进先出 |
内存需求 | 低 | 高(需要队列空间) |
适用场景:当需要有序地传递多个数据项时,消息队列是理想选择。
// 使用消息队列
OS_EVENT *Queue;
void *QueueStorage[10];
Queue = OSQCreate(QueueStorage, 10); // 创建队列,深度为10
// 发送多个消息
OSQPost(Queue, (void*)1);
OSQPost(Queue, (void*)2);
OSQPost(Queue, (void*)3);
// 接收消息
void *msg;
INT8U err;
msg = OSQPend(Queue, 0, &err); // 先得到1
msg = OSQPend(Queue, 0, &err); // 再得到2
msg = OSQPend(Queue, 0, &err); // 最后得到3
信号量与事件标志组
事件标志组:允许任务等待多个条件的组合,可以是"与"关系或"或"关系。
特性 | 信号量 | 事件标志组 |
---|---|---|
等待条件 | 单一条件 | 多条件组合 |
条件关系 | 无 | 支持"与"或"或"关系 |
资源管理 | 可以计数 | 二进制状态位 |
复杂度 | 低 | 高 |
适用场景:当任务需要等待多个条件满足时,事件标志组更为合适。
// 使用事件标志组
OS_FLAG_GRP *EventFlags;
EventFlags = OSFlagCreate(0, &err); // 创建事件标志组,初始全为0
// 设置事件标志
OSFlagPost(EventFlags, 0x01, OS_FLAG_SET, &err); // 设置位0
OSFlagPost(EventFlags, 0x02, OS_FLAG_SET, &err); // 设置位1
// 等待多个条件(AND关系)
OS_FLAGS flags;
flags = OSFlagPend(EventFlags, 0x03, OS_FLAG_WAIT_SET_ALL, 0, &err);
// 只有当位0和位1都设置时才会返回
// 等待多个条件(OR关系)
flags = OSFlagPend(EventFlags, 0x03, OS_FLAG_WAIT_SET_ANY, 0, &err);
// 当位0或位1任一设置时即返回
信号量在实时系统设计中的最佳实践
基于我们对μC/OS-II信号量的深入理解,总结一些在实时系统设计中的最佳实践。
信号量命名规范
采用一致的命名规范可以提高代码可读性:
- 使用描述性名称,表明信号量保护的资源或同步的事件
- 添加类型后缀(例如Sem、Mutex)
- 对于全局信号量,使用g_前缀
OS_EVENT *g_UartMutex; // UART互斥信号量
OS_EVENT *g_CanRxEventSem; // CAN接收事件信号量
OS_EVENT *g_FlashResourceSem; // Flash资源信号量
资源保护的完整封装
为避免资源保护不完整,应将所有对共享资源的访问封装在特定函数中:
// Flash操作封装
INT8U Flash_Write(uint32_t address, uint8_t *data, uint16_t len)
{
INT8U err;
OSSemPend(g_FlashResourceSem, 100, &err);
if (err != OS_NO_ERR) return FLASH_ERR_TIMEOUT;
// 执行Flash写入操作
INT8U result = FLASH_WriteData(address, data, len);
OSSemPost(g_FlashResourceSem);
return result;
}
INT8U Flash_Read(uint32_t address, uint8_t *buffer, uint16_t len)
{
INT8U err;
OSSemPend(g_FlashResourceSem, 100, &err);
if (err != OS_NO_ERR) return FLASH_ERR_TIMEOUT;
// 执行Flash读取操作
INT8U result = FLASH_ReadData(address, buffer, len);
OSSemPost(g_FlashResourceSem);
return result;
}
信号量超时处理策略
合理的超时处理可以提高系统鲁棒性:
- 明确区分错误类型:区分超时、参数错误等不同错误类型
- 提供恢复机制:设计重试或替代路径
- 记录错误信息:便于调试和故障分析
INT8U Safe_ResourceOperation(void)
{
INT8U retry_count = 0;
INT8U err;
while (retry_count < 3) {
OSSemPend(g_ResourceSem, 100, &err);
if (err == OS_NO_ERR) {
// 执行资源操作
INT8U result = DoOperation();
OSSemPost(g_ResourceSem);
return result;
} else if (err == OS_TIMEOUT) {
retry_count++;
LOG_WARNING("Resource timeout, retry: %d", retry_count);
OSTimeDlyHMSM(0, 0, 0, 50); // 短暂延时后重试
} else {
LOG_ERROR("Resource error: %d", err);
return RES_ERR_UNKNOWN;
}
}
LOG_ERROR("Resource max retry reached");
return RES_ERR_TIMEOUT;
}
防止信号量滥用
信号量是强大的工具,但过度使用可能导致系统复杂性增加。应当:
- 只在必要时使用信号量
- 选择合适的同步原语(不是所有同步都需要信号量)
- 保持信号量逻辑简单清晰
// 不良实践:过度使用信号量
void BadDesign(void)
{
// 每个小操作都使用信号量,增加系统复杂性
OSSemPend(Sem1, 0, &err);
// 简单操作1
OSSemPost(Sem1);
OSSemPend(Sem2, 0, &err);
// 简单操作2
OSSemPost(Sem2);
// ...
}
// 良好实践:适度使用信号量
void GoodDesign(void)
{
// 只保护真正需要同步的关键资源
DoSimpleOperation1(); // 不需要同步保护
OSSemPend(CriticalResourceSem, 0, &err);
// 关键资源操作
OSSemPost(CriticalResourceSem);
DoSimpleOperation2(); // 不需要同步保护
}
信号量调试技术
调试与信号量相关的问题可能具有挑战性,以下是一些有效技术:
- 记录信号量操作:在获取/释放信号量时记录日志
- 监控等待时间:跟踪任务在信号量上等待的时间
- 统计信号量使用情况:记录获取/释放次数、最大等待队列长度等
// 带调试功能的信号量包装函数
INT8U DebugSemPend(OS_EVENT *sem, INT16U timeout, INT8U *err)
{
uint32_t start_time = OSTimeGet();
OSSemPend(sem, timeout, err);
uint32_t wait_time = OSTimeGet() - start_time;
if (*err == OS_NO_ERR) {
LOG_DEBUG("Sem %p acquired after %u ticks", sem, wait_time);
// 更新统计信息
UpdateSemStats(sem, wait_time);
} else {
LOG_WARNING("Sem %p failed with error %d after %u ticks",
sem, *err, wait_time);
}
return *err;
}