打破瓶颈:如何在STM32F103上使用μCOS-II信号量解决多任务同步问题

文章总结(帮你们节约时间)

  • μ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. 首先检查信号量计数值
  2. 如果计数值为1(资源可用),则将其减为0并立即返回
  3. 如果计数值为0(资源不可用),则:
    • 根据调用选项决定是否等待
    • 如选择等待,当前任务被挂起并加入等待列表
    • μC/OS-II调度器会选择另一个就绪任务执行

这个过程必须是原子操作,在μC/OS-II中通过禁用中断来实现。

释放信号量过程

当任务完成资源使用并释放信号量时:

  1. 检查是否有任务在等待该信号量
  2. 如有等待任务,将最高优先级的等待任务移至就绪状态
  3. 如无等待任务,将信号量计数值增加到1
  4. 根据优先级抢占规则决定是否进行任务切换

计数信号量运作机制

计数信号量相比二值信号量更为灵活,适用于管理多个同类资源或进行事件计数。其内部机制值得我们详细了解。

计数信号量的特性

计数信号量的核心特性在于其计数值可以大于1。其计数上限为65535(对于16位整数),这意味着它可以管理多达65535个资源实例。

创建计数信号量

创建计数信号量的过程与二值信号量类似,但初始计数值可以设置为任意值(在有效范围内):

OS_EVENT *OSSemCreate(INT16U cnt);  // cnt可以大于1
获取与释放机制

计数信号量的获取与释放遵循以下规则:

  1. 获取操作

    • 检查计数值是否大于0
    • 如果是,将计数值减1并返回成功
    • 如果否,根据调用选项决定是否等待
  2. 释放操作

    • 如有任务等待,唤醒优先级最高的任务
    • 如无任务等待,将计数值加1(注意防止溢出)
计数信号量应用场景

计数信号量特别适合以下场景:

  1. 资源池管理:管理有限数量的相同资源,如内存块、通信缓冲区等
  2. 事件计数:记录特定事件的发生次数
  3. 生产者-消费者问题:协调生产者和消费者之间的速度差异

例如,在管理多个串口设备时,可以这样使用:

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 - 创建信号量

这是创建新信号量的基础函数。

参数类型说明
cntINT16U信号量初始计数值,0≤cnt≤65535
返回值OS_EVENT*成功时返回信号量指针,失败时返回NULL
OS_EVENT *sem;
sem = OSSemCreate(1);  // 创建初始值为1的信号量
if (sem == NULL) {
    // 创建失败处理...
}
OSSemPend - 获取信号量

此函数用于获取(等待)信号量。

参数类型说明
peventOS_EVENT*要获取的信号量指针
timeoutINT16U等待超时时间(ticks),0表示永久等待
perrINT8U*错误代码指针
返回值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 - 释放信号量

此函数用于释放(发送)信号量。

参数类型说明
peventOS_EVENT*要释放的信号量指针
返回值INT8U错误代码

返回值包括:

  • OS_NO_ERR: 成功释放
  • OS_SEM_OVF: 信号量计数溢出
  • OS_ERR_EVENT_TYPE: 事件不是信号量
INT8U err;
err = OSSemPost(sem);
if (err != OS_NO_ERR) {
    // 处理错误
}
OSSemAccept - 无等待获取信号量

此函数尝试获取信号量但不等待。

参数类型说明
peventOS_EVENT*要获取的信号量指针
返回值INT16U操作后信号量的计数值
INT16U cnt;
cnt = OSSemAccept(sem);
if (cnt > 0) {
    // 成功获取信号量
} else {
    // 信号量不可用
}
OSSemSet - 设置信号量计数值

此函数直接设置信号量的计数值。

参数类型说明
peventOS_EVENT*信号量指针
cntINT16U新的计数值
perrINT8U*错误代码指针
返回值void无返回值
INT8U err;
OSSemSet(sem, 5, &err);  // 将信号量计数值设为5
OSSemQuery - 查询信号量状态

此函数获取信号量的当前状态。

参数类型说明
peventOS_EVENT*要查询的信号量指针
p_sem_dataOS_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提供了多种获取信号量的方法,每种都有其特定用途:

  1. 阻塞式获取:使用OSSemPend并设置timeout=0,任务将无限期等待直到获取信号量。

    OSSemPend(sem, 0, &err);  // 永久等待
    
  2. 超时式获取:使用OSSemPend并设置非零timeout,任务最多等待指定时间。

    OSSemPend(sem, 100, &err);  // 最多等待100个时钟节拍
    
  3. 非阻塞式获取:使用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平台上的实际例子,展示信号量在不同场景下的应用。

硬件环境与基础设置

首先,确保我们的开发环境已正确配置:

  1. 硬件:STM32F103系列微控制器(如STM32F103ZET6)
  2. 开发工具:Keil MDK 5.x
  3. RTOS:μC/OS-II V2.9x
  4. 外设:按需配置(本例中使用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。

解决方法

  1. 资源排序:按照固定顺序获取多个资源
  2. 超时机制:使用非永久等待方式获取资源
  3. 死锁检测:定期检查系统是否存在死锁状态
// 错误示例(可能导致死锁)
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
    }
}
优先级反转

前面我们提到过优先级反转问题,这里再详细探讨一下:

问题描述:低优先级任务持有高优先级任务需要的资源,但中优先级任务抢占了低优先级任务,导致高优先级任务长时间等待。

解决方法

  1. 使用互斥信号量(最佳方案)
  2. 禁止任务抢占(在关键代码段)
  3. 优先级天花板协议
// 使用互斥信号量解决优先级反转
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)并溢出。

解决方法

  1. 在释放信号量前检查当前计数值
  2. 设计良好的2. 设计良好的应用架构,确保每次获取都有对应的释放
  3. 实现自定义包装函数,防止错误使用
// 安全的信号量释放函数
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函数会导致系统崩溃。

解决方法

  1. 在ISR中只使用OSSemPost
  2. 使用特定的ISR版本函数
// 中断服务程序
void EXTI0_IRQHandler(void)
{
    OSIntEnter();  // 进入中断
    
    if (EXTI_GetITStatus(EXTI_Line0) != RESET) {
        EXTI_ClearITPendingBit(EXTI_Line0);
        // 不能在这里使用OSSemPend!
        OSSemPost(ButtonSem);  // 这是安全的
    }
    
    OSIntExit();   // 退出中断
}
共享资源保护不完整

不正确的信号量使用可能导致共享资源保护不完整。

问题描述:代码中只保护了部分访问共享资源的路径,导致仍有未保护的访问可能。

解决方法

  1. 集中封装所有对共享资源的访问
  2. 代码审查确保所有访问路径都受保护
// 全局缓冲区
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。

系统需求
  1. 温湿度传感器每2秒读取一次
  2. 光照传感器每1秒读取一次
  3. 红外传感器检测到物体移动时产生中断
  4. 所有数据通过同一个UART发送到PC
  5. 系统需要响应用户按键来切换工作模式
系统架构设计

我们将使用以下信号量:

  • 温湿度数据就绪信号量(二值)
  • 光照数据就绪信号量(二值)
  • 红外事件信号量(二值)
  • UART访问互斥信号量(互斥)
  • 模式切换信号量(二值)
// 信号量定义
OS_EVENT *TempHumReadySem;  // 温湿度数据就绪信号量
OS_EVENT *LightReadySem;    // 光照数据就绪信号量
OS_EVENT *IREventSem;       // 红外事件信号量
OS_EVENT *UARTMutex;        // UART访问互斥信号量
OS_EVENT *ModeChangeSem;    // 模式切换信号量
任务设计

我们设计以下任务:

  1. 温湿度采集任务
  2. 光照采集任务
  3. 红外处理任务
  4. 数据处理和发送任务
  5. 按键检测任务
代码实现

首先是系统初始化:

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;
}
信号量超时处理策略

合理的超时处理可以提高系统鲁棒性:

  1. 明确区分错误类型:区分超时、参数错误等不同错误类型
  2. 提供恢复机制:设计重试或替代路径
  3. 记录错误信息:便于调试和故障分析
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;
}
防止信号量滥用

信号量是强大的工具,但过度使用可能导致系统复杂性增加。应当:

  1. 只在必要时使用信号量
  2. 选择合适的同步原语(不是所有同步都需要信号量)
  3. 保持信号量逻辑简单清晰
// 不良实践:过度使用信号量
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();  // 不需要同步保护
}
信号量调试技术

调试与信号量相关的问题可能具有挑战性,以下是一些有效技术:

  1. 记录信号量操作:在获取/释放信号量时记录日志
  2. 监控等待时间:跟踪任务在信号量上等待的时间
  3. 统计信号量使用情况:记录获取/释放次数、最大等待队列长度等
// 带调试功能的信号量包装函数
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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值