本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:STM32是基于ARM Cortex-M内核的高性能微控制器,广泛用于嵌入式系统开发。UCOSII是一个轻量级实时操作系统(RTOS),为嵌入式应用提供多任务调度、内存管理和时间控制等功能。本资源“STM32的UCOSII信号量程序,亲测可用”提供了一个经过验证的实践项目,重点讲解如何在STM32平台上使用UCOSII的信号量机制。信号量是RTOS中实现任务同步与资源共享控制的关键工具,通过创建、获取和释放信号量,可以有效避免死锁和资源竞争问题。本项目包含完整示例代码和实验文档,适合嵌入式开发者学习和应用RTOS中的信号量技术,提升多任务系统的开发能力。
信号量程序

1. STM32微控制器基础

STM32系列微控制器基于ARM Cortex-M内核,具备高性能、低功耗和丰富的外设接口,广泛应用于各类嵌入式系统中。其架构采用哈佛结构,支持指令与数据并行访问,提升运行效率。

其核心模块包括:
- 内核(Cortex-M系列) :负责指令执行与任务调度
- 系统时钟树(Clock Tree) :提供各模块时钟源,支持动态频率调节
- 中断控制器(NVIC) :实现多级中断响应,提升系统实时性

此外,STM32具备多种低功耗模式(如Sleep、Stop、Standby),适合电池供电设备。理解其基本架构是后续在STM32平台上移植UCOSII操作系统和使用信号量机制的基础。

2. UCOSII实时操作系统(RTOS)概述

2.1 UCOSII的核心特性

2.1.1 实时性与多任务调度机制

UCOSII 是一款轻量级、可移植的实时操作系统(RTOS),广泛应用于嵌入式系统中,特别是在资源受限的微控制器平台上,如 STM32。其核心特性之一是 实时性 ,即系统能够对任务的执行做出可预测的响应时间。UCOSII 提供了硬实时的支持,能够确保关键任务在规定时间内完成,这对于工业控制、传感器采集、通信模块管理等应用场景至关重要。

在任务调度方面,UCOSII 采用的是 抢占式优先级调度机制 。每个任务都有一个唯一的优先级(0~63,0为最高),当更高优先级的任务变为就绪状态时,系统会立即暂停当前任务,切换到高优先级任务执行。这种机制确保了关键任务的优先执行。

此外,UCOSII 还支持 时间片轮转调度 (Round Robin Scheduling),即多个同优先级任务可以轮流执行,防止某个任务长期占用CPU资源。这种机制适用于需要多任务并行处理但优先级相同的情况。

特性 描述
实时性 提供确定性的任务响应时间,适用于硬实时场景
抢占式调度 高优先级任务可以中断低优先级任务
时间片轮转 同优先级任务轮流执行,防止资源垄断
任务数量限制 最多支持64个任务(包括系统任务)

2.1.2 内核结构与任务管理

UCOSII 的内核采用模块化设计,主要包括任务管理、时间管理、中断处理、信号量、消息队列、内存管理等核心组件。其内核代码体积小,运行效率高,非常适合嵌入式平台使用。

在任务管理方面,UCOSII 中的任务由 任务控制块 (TCB)进行管理,TCB 是一个结构体变量,用于保存任务的上下文信息、优先级、堆栈指针、状态等信息。任务的生命周期包括:

  • 创建 :使用 OSTaskCreate 函数创建任务
  • 就绪 :任务等待调度执行
  • 运行 :当前执行的任务
  • 阻塞 :任务等待某个事件(如信号量、延时)
  • 挂起 :任务被显式挂起,直到被恢复

任务的上下文切换由 调度器 负责,在任务切换时,系统会保存当前任务的寄存器状态到任务堆栈中,并加载下一个任务的寄存器内容,从而实现任务切换。

以下是一个任务的基本结构:

void MyTask(void *pdata)
{
    while (1)
    {
        // 执行任务逻辑
        OSTimeDlyHMSM(0, 0, 1, 0);  // 延时1秒
    }
}

代码逻辑分析:

  • void MyTask(void *pdata) :任务函数,是任务的入口点, pdata 为传递给任务的参数。
  • while(1) :任务主体循环,表示任务是一个无限循环结构。
  • OSTimeDlyHMSM(...) :调用UCOSII的延时函数,使任务周期性运行。

参数说明:
- OSTimeDlyHMSM(Hours, Minutes, Seconds, Milliseconds) :以小时、分钟、秒、毫秒为单位进行延时。例如, OSTimeDlyHMSM(0, 0, 1, 0) 表示延时1秒。

2.2 UCOSII在STM32平台上的移植

2.2.1 移植前的环境准备

在将 UCOSII 移植到 STM32 平台上之前,需要准备好开发环境和相关工具链。通常使用 STM32CubeIDE 或 Keil MDK-ARM 作为开发平台,配合 STM32 HAL 库或标准外设库进行硬件驱动开发。

移植前的准备工作包括:

  • 下载 UCOSII 源码包(官方或开源版本)
  • 准备 STM32 开发板(如 STM32F103、STM32F407 等)
  • 安装开发工具链(如 Keil、IAR、GCC + Eclipse)
  • 创建工程并导入 UCOSII 的核心文件(如 os_core.c、os_task.c、os_sem.c 等)

此外,还需要根据目标平台配置 UCOSII 的配置头文件 os_cfg.h ,设置任务数量、最大优先级数、是否启用时间片轮转等参数。

2.2.2 启动文件与堆栈配置

在 STM32 上运行 UCOSII,需要正确配置启动文件和任务堆栈空间。STM32 的启动文件一般以 .s .S 结尾,负责初始化堆栈指针、中断向量表,并跳转到主函数。

UCOSII 使用独立的任务堆栈来保存每个任务的上下文。堆栈的大小需要根据任务的复杂度合理分配。通常每个任务的堆栈大小为 128~512 字节。例如:

#define TASK_STK_SIZE     512       // 每个任务堆栈大小

OS_STK Task1Stk[TASK_STK_SIZE];    // 任务1的堆栈
OS_STK Task2Stk[TASK_STK_SIZE];    // 任务2的堆栈

在创建任务时,需要将堆栈地址传递给 OSTaskCreate 函数:

OSTaskCreate(MyTask, (void *)0, &Task1Stk[TASK_STK_SIZE - 1], 5);

参数说明:

  • MyTask :任务函数入口
  • (void *)0 :传递给任务的参数
  • &Task1Stk[TASK_STK_SIZE - 1] :堆栈的顶部地址(STM32使用满递减堆栈)
  • 5 :任务的优先级(数值越小优先级越高)

2.2.3 系统时钟与中断配置

UCOSII 需要一个系统时钟来驱动任务调度和延时功能。系统时钟通常由定时器中断实现,例如使用 STM32 的 SysTick 定时器。

配置系统时钟的步骤如下:

  1. 初始化 SysTick 定时器,设置中断周期为 1ms。
  2. 在中断服务函数中调用 OSTimeTick() 函数更新系统时间。
  3. 在 UCOSII 初始化完成后启动定时器。

以下是 SysTick 初始化的代码示例:

void SysTick_Handler(void)
{
    OSIntEnter();        // 进入中断
    OSTimeTick();        // 更新系统时间
    OSIntExit();         // 退出中断
}

void InitSysTick(uint32_t ticks)
{
    SysTick->CTRL = 0;                  // 清除控制寄存器
    SysTick->LOAD = ticks - 1;          // 设置重载值
    SysTick->VAL = 0;                   // 清除当前计数值
    SysTick->CTRL = SysTick_CTRL_CLKSOURCE_Msk | 
                    SysTick_CTRL_TICKINT_Msk | 
                    SysTick_CTRL_ENABLE_Msk; // 启动SysTick
}

逻辑分析:

  • SysTick_Handler :SysTick 中断服务函数,每次中断调用 OSTimeTick() 更新系统时间。
  • InitSysTick :初始化 SysTick 定时器,每 ticks 个时钟周期产生一次中断。
  • OSIntEnter() OSIntExit() :用于通知内核进入中断处理,确保中断嵌套调度正常。

2.3 UCOSII任务的基本结构与运行机制

2.3.1 任务创建函数 OSTaskCreate

在 UCOSII 中,任务的创建通过 OSTaskCreate() 函数完成。该函数的原型如下:

INT8U OSTaskCreate(
    void (*task)(void *pd),
    void *pdata,
    OS_STK *ptos,
    INT8U prio
);

参数说明:

  • task :任务函数指针,指向任务的执行函数
  • pdata :传递给任务的参数
  • ptos :任务堆栈的栈顶指针
  • prio :任务优先级(0~63)

例如,创建一个优先级为 5 的任务:

OSTaskCreate(Task1, NULL, &Task1Stk[TASK_STK_SIZE - 1], 5);

调用该函数后,UCOSII 会初始化任务控制块(TCB),将任务加入就绪队列,并等待调度器调度执行。

2.3.2 任务状态转换与调度策略

UCOSII 中的任务可以处于以下几种状态:

  • 就绪态(Ready) :任务已创建,等待被调度执行
  • 运行态(Running) :当前正在执行的任务
  • 阻塞态(Waiting) :任务等待某个事件(如信号量、消息队列、延时)
  • 挂起态(Suspended) :任务被显式挂起,需要调用 OSTaskResume() 恢复

任务状态之间的转换由调度器控制,状态转换图如下:

graph TD
    A[就绪态] --> B{调度器选择}
    B --> C[运行态]
    C --> D{是否调用延时或等待事件}
    D -->|是| E[阻塞态]
    D -->|否| C
    E --> F{事件是否发生}
    F -->|是| A
    F -->|否| E
    C --> G{是否被挂起}
    G -->|是| H[挂起态]
    H --> I{是否恢复}
    I -->|是| A

调度策略说明:

  • 抢占式调度 :当更高优先级任务变为就绪状态时,立即抢占当前任务
  • 时间片轮转 :多个同优先级任务按时间片轮流执行
  • 空闲任务 :UCOSII 内置优先级最低的空闲任务,用于处理系统空闲时的资源释放

2.3.3 任务通信与同步机制概述

在多任务系统中,任务之间需要进行通信与同步。UCOSII 提供了多种任务间通信机制,包括:

  • 信号量(Semaphore)
  • 互斥量(Mutex)
  • 消息队列(Message Queue)
  • 事件标志组(Event Flag)
  • 邮箱(Mailbox)

其中, 信号量 是最常用的同步机制,用于协调任务之间的资源访问和事件通知。例如:

OS_EVENT *sem;

sem = OSSemCreate(1);  // 创建一个初始值为1的信号量

void TaskA(void *pdata)
{
    while (1)
    {
        OSSemPend(sem, 0, &err);   // 获取信号量
        // 访问共享资源
        OSSemPost(sem);            // 释放信号量
    }
}

void TaskB(void *pdata)
{
    while (1)
    {
        // 触发某个事件
        OSSemPost(sem);            // 通知其他任务
    }
}

代码逻辑分析:

  • OSSemCreate(1) :创建一个二进制信号量,初始值为1,表示资源可用
  • OSSemPend() :尝试获取信号量,若不可用则阻塞等待
  • OSSemPost() :释放信号量,唤醒等待的任务

参数说明:

  • OSSemPend(OS_EVENT *pevent, INT16U timeout, INT8U *err)
  • pevent :信号量指针
  • timeout :超时时间(0表示无限等待)
  • err :返回错误码
  • OSSemPost(OS_EVENT *pevent)
  • pevent :信号量指针

通过合理使用信号量等机制,可以在 STM32 平台上实现高效、可靠的任务通信与同步,为构建复杂嵌入式系统打下坚实基础。

3. 信号量基本概念与作用

在嵌入式实时操作系统(RTOS)中,信号量(Semaphore)是实现任务间通信与同步的核心机制之一。它不仅可以有效管理共享资源的访问,还能在多个任务之间协调执行顺序。本章将从信号量的定义出发,深入剖析其分类、作用场景以及与互斥量的区别,帮助开发者理解其在多任务系统中的关键地位。

3.1 信号量的定义与分类

3.1.1 信号量的本质与作用场景

信号量是一种用于控制多个任务访问共享资源的同步机制。它本质上是一个带有计数器的变量,用于表示可用资源的数量。信号量的操作主要包括两个核心函数:

  • P操作(也称作OSSemPend) :尝试获取信号量。如果信号量计数大于0,则计数减1并继续执行;如果计数为0,则当前任务可能被阻塞,直到信号量被释放。
  • V操作(也称作OSSemPost) :释放信号量,计数加1,如果有任务在等待该信号量,则唤醒其中一个。

信号量广泛应用于以下典型场景:

应用场景 说明
资源访问控制 如多个任务访问共享外设(如串口、SPI)
任务同步 控制任务之间的执行顺序
事件通知机制 中断服务中释放信号量,通知任务执行

在UCOSII中,信号量通过 OSSemCreate() 创建,使用 OSSemPend() OSSemPost() 进行操作。其结构体 OS_SEM 中包含了当前计数、等待任务列表等信息。

3.1.2 信号量与互斥量的区别

虽然信号量和互斥量(Mutex)在功能上有一定的相似性,都用于保护共享资源,但它们在设计目的和使用方式上有显著区别。

特性 信号量(Semaphore) 互斥量(Mutex)
类型 二进制信号量、计数信号量 通常为二进制
所有权机制 无任务所有权 有任务所有权
优先级继承支持 是(防止优先级反转)
使用场景 资源管理、任务同步 临界资源保护
多次释放是否允许 否(只能释放一次)

例如,在UCOSII中,使用信号量时无需考虑释放者是否为获取者,而在使用互斥量时,必须由获取它的任务释放,否则系统会报错。

// 示例:使用UCOSII创建信号量
OS_SEM mySemaphore;
OSSemCreate(&mySemaphore, 1);  // 创建初始值为1的二进制信号量

// 任务中获取信号量
void TaskFunction(void *pdata) {
    while (1) {
        OSSemPend(&mySemaphore, 0, &err);  // 阻塞等待信号量
        // 临界区操作
        OSSemPost(&mySemaphore);          // 释放信号量
    }
}

逐行分析:

  • OSSemCreate(&mySemaphore, 1) :创建一个初始值为1的信号量,相当于一个二进制信号量。
  • OSSemPend :尝试获取信号量,若计数为0则阻塞。
  • OSSemPost :释放信号量,计数加1。
  • 该代码可用于保护共享资源,例如串口操作。

注意 :虽然信号量可以用于资源保护,但在需要避免优先级反转的场景中,应优先使用互斥量。

3.2 信号量在多任务系统中的核心作用

3.2.1 资源共享与访问控制

在多任务系统中,多个任务可能同时访问同一硬件资源(如ADC、串口、GPIO),若没有同步机制,可能导致数据不一致或设备状态混乱。信号量可以很好地解决这个问题。

例如,两个任务TaskA和TaskB都需要使用串口发送数据:

graph TD
    A[TaskA] --> B{获取信号量?}
    B -->|是| C[发送数据]
    B -->|否| D[阻塞等待]
    C --> E[释放信号量]
    F[TaskB] --> G{获取信号量?}
    G -->|是| H[发送数据]
    G -->|否| I[阻塞等待]
    H --> J[释放信号量]

逻辑说明:

  • TaskA尝试获取信号量,成功后发送数据,完成后释放。
  • TaskB此时尝试获取失败,进入阻塞。
  • TaskA释放后,TaskB恢复运行并发送数据。

该机制确保了串口资源不会被两个任务同时占用,从而避免数据冲突。

3.2.2 任务间同步与异步通信

信号量不仅可以用于资源管理,还可以作为任务间同步的工具。例如,任务A等待任务B完成某项操作后才继续执行。

// 示例:任务间同步
OS_SEM syncSemaphore;

void TaskA(void *pdata) {
    OSSemPend(&syncSemaphore, 0, &err);  // 等待任务B通知
    printf("TaskA继续执行...\n");
}

void TaskB(void *pdata) {
    // 执行某些操作
    DelayMs(1000);
    OSSemPost(&syncSemaphore);  // 通知TaskA
}

逐行分析:

  • OSSemPend :TaskA等待信号量,进入阻塞状态。
  • TaskB执行完成后调用 OSSemPost ,使信号量计数+1,TaskA被唤醒继续执行。
  • 这种机制非常适合用于异步通知,如中断服务中释放信号量唤醒任务处理事件。

3.3 UCOSII中信号量的工作机制

3.3.1 内核对信号量的支持

UCOSII内核提供了完整的信号量支持,包括创建、等待、释放等接口。内核通过任务等待队列来管理等待信号量的任务。当一个任务调用 OSSemPend() 时,若信号量不可用,该任务将被挂起并插入等待队列中。当另一个任务或中断服务调用 OSSemPost() 时,内核将唤醒等待队列中的任务。

UCOSII信号量操作函数如下:

函数名 功能描述
OSSemCreate() 创建信号量并初始化计数
OSSemPend() 获取信号量,可设定超时
OSSemPost() 释放信号量
OSSemAccept() 非阻塞方式获取信号量
OSSemDel() 删除信号量
OSSemQuery() 查询信号量状态

3.3.2 信号量控制块(OS_SEM)结构分析

在UCOSII中,每个信号量都由一个 OS_SEM 结构体管理。该结构体定义如下(简化):

typedef struct os_sem {
    INT16U     SemCnt;     // 当前信号量计数值
    OS_TCB    *First;      // 等待任务链表头
    OS_TCB    *Last;       // 等待任务链表尾
    OS_EVENT  *OSEvent;    // 指向事件控制块
} OS_SEM;

字段说明:

  • SemCnt :信号量当前可用数量。例如,二进制信号量初始值为1,计数范围为0~1;计数信号量初始值为N,最大值也为N。
  • First/Last :指向等待该信号量的任务链表,采用FIFO方式管理。
  • OSEvent :指向事件控制块,用于系统内部管理。

当任务调用 OSSemPend() 时,若 SemCnt > 0 ,则将其减1并继续执行;否则,任务将被挂起,并插入等待队列中。调用 OSSemPost() 时,若等待队列非空,则唤醒队列中的第一个任务,并将其插入就绪队列。

信号量工作机制流程图如下:

graph LR
    A[任务调用OSSemPend] --> B{SemCnt > 0?}
    B -->|是| C[SemCnt减1, 继续执行]
    B -->|否| D[任务阻塞, 插入等待队列]
    D --> E[等待OSSemPost或超时]
    E --> F{是否收到OSSemPost?}
    F -->|是| G[唤醒任务, SemCnt加1]
    F -->|否| H[超时, 返回错误]
    G --> I[任务进入就绪状态]
    H --> J[任务继续运行, 返回错误码]

总结说明:

  • UCOSII通过内核调度机制,确保信号量操作的原子性和任务调度的公平性。
  • 使用信号量时应避免在中断服务中调用可能导致阻塞的函数(如 OSSemPend ),而 OSSemPost 则可以在中断中安全调用。

本章系统地介绍了信号量的定义、分类、应用场景,并结合UCOSII内核机制详细解析了信号量的内部结构和工作机制。通过代码示例和流程图,帮助开发者深入理解信号量在多任务系统中的关键作用,为后续章节中信号量的具体应用打下坚实基础。

4. 二进制信号量与计数信号量

在实时多任务系统中,信号量是实现任务同步与资源共享控制的重要机制。根据信号量值的取值范围与使用方式,UCOSII支持两种主要类型的信号量: 二进制信号量(Binary Semaphore) 计数信号量(Counting Semaphore) 。它们分别适用于不同的应用场景,理解它们的机制与使用方式,是构建高效、安全的嵌入式系统任务通信模型的关键。

4.1 二进制信号量的应用场景

二进制信号量的值只能是0或1,它适用于需要对单个资源进行互斥访问或实现任务间简单同步的场景。

4.1.1 单次资源的访问控制

在多任务系统中,多个任务可能同时访问某一共享资源,例如外设寄存器、共享缓冲区或I/O设备。若没有访问控制机制,将可能导致数据竞争甚至系统崩溃。二进制信号量在这种场景中非常有效。

例如,考虑一个串口通信模块,多个任务都希望使用该串口发送数据。我们可以通过创建一个初始值为1的二进制信号量来控制访问:

OS_SEM serial_sem;

void serial_init(void) {
    OSSemCreate(&serial_sem, "Serial Access", 1); // 初始化为1,表示可用
}

在任务中使用串口前,需调用 OSSemPend 尝试获取信号量:

void taskA(void *pdata) {
    while (1) {
        OSSemPend(&serial_sem, 0, &err);  // 等待信号量
        send_data("Hello from Task A");
        OSSemPost(&serial_sem);           // 释放信号量
        OSTimeDlyHMSM(0, 0, 1, 0);        // 延时1秒
    }
}

逻辑分析:

  • OSSemCreate 函数用于创建信号量,第三个参数表示初始值。此处设为1,表示资源初始可用。
  • OSSemPend 用于尝试获取信号量。若信号量值为1,则减为0,任务继续执行;否则任务进入等待队列。
  • OSSemPost 释放信号量,将其值恢复为1,并唤醒等待队列中的任务。

这种机制确保了在同一时间只有一个任务可以访问串口,从而避免了冲突。

4.1.2 任务唤醒机制示例

二进制信号量还常用于任务间的同步唤醒机制。例如,一个任务负责等待外部中断,另一个任务则负责处理中断事件。当外部中断发生时,中断服务程序(ISR)释放信号量,通知处理任务执行操作。

以下是一个典型的中断与任务同步示例:

OS_SEM button_sem;

void EXTI0_IRQHandler(void) {
    if (EXTI_GetITStatus(EXTI_Line0) != RESET) {
        OSSemPostFromISR(&button_sem, &err); // 中断中释放信号量
        EXTI_ClearITPendingBit(EXTI_Line0);  // 清除中断标志
    }
}

void task_button_handler(void *pdata) {
    while (1) {
        OSSemPend(&button_sem, 0, &err); // 等待信号量
        process_button_press();         // 执行按键处理逻辑
    }
}

参数说明:

  • OSSemPostFromISR 是专门用于中断上下文中的信号量释放函数,确保不会破坏内核调度状态。
  • 按键中断触发后,信号量被释放,任务被唤醒,执行按键处理函数。

流程图:

graph TD
    A[按键按下] --> B[触发EXTI中断]
    B --> C[中断服务函数执行]
    C --> D[调用OSSemPostFromISR释放信号量]
    D --> E{任务是否在等待?}
    E -->|是| F[任务被唤醒,执行处理逻辑]
    E -->|否| G[信号量值置为1,等待任务获取]

4.2 计数信号量的应用场景

与二进制信号量不同,计数信号量的值可以大于1,适用于需要管理多个相同资源的场景,如缓冲区池、事件计数、资源队列等。

4.2.1 多资源并发管理

在某些系统中,资源是多个但有限的,例如系统中存在多个DMA通道、多个串口缓冲区等。使用计数信号量可以实现对这些资源的合理分配与回收。

例如,系统中有3个DMA通道可用:

OS_SEM dma_sem;

void dma_init(void) {
    OSSemCreate(&dma_sem, "DMA Channel", 3); // 初始值为3,表示有3个DMA通道可用
}

void task_dma_user(void *pdata) {
    while (1) {
        OSSemPend(&dma_sem, 0, &err);   // 获取DMA通道
        use_dma_channel();              // 使用DMA传输
        OSSemPost(&dma_sem);            // 释放DMA通道
        OSTimeDlyHMSM(0, 0, 0, 500);    // 延时500ms
    }
}

逻辑分析:

  • 初始信号量值为3,表示有3个DMA通道可用。
  • 每个任务调用 OSSemPend 获取一个通道,成功后信号量减1。
  • 当信号量值为0时,后续任务将进入等待状态,直到有任务释放通道。
  • 任务完成DMA操作后调用 OSSemPost 释放通道,信号量值加1。

这种机制有效防止了资源耗尽导致的任务死锁或系统崩溃。

资源状态表格:

时间 信号量值 当前占用DMA的任务数 等待队列中的任务数
T0 3 0 0
T1 2 1 0
T2 1 2 0
T3 0 3 2

4.2.2 生产者-消费者模型实现

计数信号量在实现生产者-消费者模型中也扮演关键角色。该模型常用于缓冲区管理、数据队列处理等场景。

以下是一个基于信号量的生产者-消费者模型示例:

#define BUF_SIZE 5

OS_SEM full_sem;   // 指示缓冲区中已填满的项数
OS_SEM empty_sem;  // 指示缓冲区中空闲项数

int buffer[BUF_SIZE];
int in = 0, out = 0;

void producer_task(void *pdata) {
    int data = 0;
    while (1) {
        OSSemPend(&empty_sem, 0, &err);  // 等待空闲空间
        buffer[in] = data++;
        in = (in + 1) % BUF_SIZE;
        OSSemPost(&full_sem);            // 通知消费者数据已就绪
        OSTimeDlyHMSM(0, 0, 0, 200);
    }
}

void consumer_task(void *pdata) {
    while (1) {
        OSSemPend(&full_sem, 0, &err);   // 等待数据就绪
        int data = buffer[out];
        out = (out + 1) % BUF_SIZE;
        OSSemPost(&empty_sem);           // 释放空闲位置
        process_data(data);
    }
}

参数说明:

  • full_sem 初始化为0,表示缓冲区初始为空。
  • empty_sem 初始化为BUF_SIZE(如5),表示初始时所有缓冲区位置都为空。
  • 生产者每次放入数据前等待 empty_sem ,放入后增加 full_sem
  • 消费者每次取出数据前等待 full_sem ,取出后增加 empty_sem

流程图:

graph LR
    subgraph 生产者
    A[等待empty_sem] --> B[放入数据到缓冲区]
    B --> C[释放full_sem]
    end

    subgraph 消费者
    D[等待full_sem] --> E[从缓冲区取出数据]
    E --> F[释放empty_sem]
    end

    A <--> G[缓冲区]
    G <--> D

4.3 信号量的创建与初始化

在UCOSII中,信号量的创建和初始化是使用 OSSemCreate 函数完成的,它决定了信号量的类型和初始状态。

4.3.1 OSSemCreate函数的参数说明

函数原型如下:

OS_SEM *OSSemCreate (OS_SEM_CTR cnt, CPU_CHAR *name, OS_ERR *p_err);

参数说明:

参数名 类型 说明
cnt OS_SEM_CTR 初始信号量值,用于决定信号量类型。0~1为二进制信号量,>1为计数信号量
name CPU_CHAR * 信号量的名称,用于调试
p_err OS_ERR * 错误码指针,用于返回错误信息

使用示例:

OS_SEM sem1;
OS_SEM sem2;

// 创建二进制信号量,初始为1
OSSemCreate(&sem1, "Binary Semaphore", 1, &err);

// 创建计数信号量,初始为3
OSSemCreate(&sem2, "Counting Semaphore", 3, &err);

4.3.2 初始化值的设置原则与影响

  • 二进制信号量 :通常初始化为1(资源可用)或0(资源不可用),用于控制互斥或同步。
  • 计数信号量 :初始化值应等于资源总数,例如缓冲区大小、DMA通道数等。

影响分析:

  • 初始值为0的信号量表示资源不可用,任务调用 OSSemPend 会直接进入等待。
  • 初始值为n(n>0)的信号量表示有n个资源可用,可支持n个任务并行执行。
  • 若初始化值设置过大,可能导致资源被错误占用;设置过小则可能造成资源浪费或任务阻塞。

初始化值对照表:

信号量类型 推荐初始值 应用场景
二进制信号量 0 或 1 资源互斥、任务同步
计数信号量 资源总数 多资源分配、生产者-消费者模型

通过合理设置信号量的类型与初始值,开发者可以灵活控制任务的执行顺序与资源的使用方式,从而构建出高效、安全的嵌入式多任务系统。

5. 信号量获取操作(OSSemPend)

在嵌入式实时操作系统 UCOSII 中, OSSemPend 是信号量获取操作的核心函数之一。该函数用于任务尝试获取一个信号量,若信号量当前不可用,则任务可以选择阻塞等待、非阻塞立即返回,或设置一个等待超时时间。本章将深入解析 OSSemPend 的执行流程、调用方式及其在多任务并发环境中的使用技巧,并结合异常处理机制探讨任务状态管理与日志记录策略。

5.1 获取信号量的基本流程

OSSemPend 函数是 UCOSII 中用于任务等待信号量的核心接口。其基本流程包括以下几个关键步骤:

  1. 检查信号量计数值 :如果信号量计数值大于 0,说明资源可用,任务可以立即获取。
  2. 资源不可用时的处理方式
    - 若任务选择阻塞等待,则将任务加入信号量的等待队列。
    - 若设置超时时间,则启动定时器并进入阻塞状态,等待信号量释放或超时发生。
  3. 唤醒与调度 :当其他任务或中断服务程序释放信号量后,UCOSII 会根据优先级调度机制唤醒等待队列中的最高优先级任务。

5.1.1 阻塞与非阻塞方式的选择

OSSemPend 支持两种基本的等待方式:

等待方式 参数值(timeout) 行为说明
非阻塞等待 0 立即返回,若信号量不可用则返回错误码 OS_ERR_PEND_ISR OS_ERR_NONE_AVAIL
阻塞等待 大于0 任务进入等待状态,直到信号量可用或超时发生

示例代码:

OS_ERR err;
OS_SEM mySem;

// 非阻塞获取信号量
if (OSSemPend(&mySem, 0, OS_OPT_PEND_BLOCKING, NULL, &err) == OS_ERR_NONE_AVAIL) {
    // 信号量不可用,处理逻辑
}

// 阻塞获取信号量,最多等待100个系统时钟节拍
OSSemPend(&mySem, 100, OS_OPT_PEND_BLOCKING, NULL, &err);

逐行解析:

  • &mySem :指向已创建的信号量控制块。
  • 100 :表示最多等待 100 个系统时钟节拍(tick),若设为 0 则为非阻塞。
  • OS_OPT_PEND_BLOCKING :表示任务将阻塞等待。
  • NULL :用于返回消息或事件指针,此处为 NULL。
  • &err :用于返回错误码。

逻辑分析:

  • 若当前信号量有资源(计数值 > 0),则计数值减 1,函数返回 OS_ERR_NONE
  • 若无资源且设为非阻塞,则返回 OS_ERR_NONE_AVAIL
  • 若设为阻塞等待,则任务进入等待队列,并触发调度器切换任务。

5.1.2 超时机制与优先级继承问题

UCOSII 的 OSSemPend 支持超时机制,任务在等待信号量时可以指定一个最大等待时间,防止任务永久阻塞。

超时机制示意图:

graph TD
    A[任务调用OSSemPend] --> B{信号量可用?}
    B -- 是 --> C[获取成功,返回]
    B -- 否 --> D{是否设为阻塞?}
    D -- 是 --> E[任务进入等待队列]
    E --> F{是否超时?}
    F -- 否 --> G[等待信号量释放]
    F -- 是 --> H[任务被唤醒,返回超时错误]
    D -- 否 --> I[立即返回错误]

优先级继承问题:

在某些 RTOS 中,为避免优先级反转(priority inversion)问题,会采用优先级继承(priority inheritance)机制。但在 UCOSII 中,默认不支持优先级继承,因此需要开发者在设计任务优先级时合理安排,避免高优先级任务因等待低优先级任务释放信号量而陷入长时间等待。

5.2 OSSemPend函数的使用技巧

5.2.1 在任务中的调用方式

OSSemPend 应在任务上下文中调用,不能在中断服务程序中使用。这是因为在中断上下文中,调度器可能被锁定,任务状态无法正确切换。

推荐调用结构:

void Task1(void *p_arg) {
    OS_ERR err;
    while (1) {
        OSSemPend(&mySem, 0, OS_OPT_PEND_BLOCKING, NULL, &err);
        if (err == OS_ERR_NONE) {
            // 成功获取信号量,执行资源访问
        } else if (err == OS_ERR_NONE_AVAIL) {
            // 信号量不可用,非阻塞情况下处理
        }
        OSTimeDlyHMSM(0, 0, 1, 0); // 模拟任务执行时间
    }
}

逐行解析:

  • while (1) :任务主循环。
  • OSSemPend :尝试获取信号量。
  • OSTimeDlyHMSM :模拟任务执行延迟,防止 CPU 资源占用过高。

逻辑分析:

  • 任务在每次循环中尝试获取信号量,若成功则执行资源操作。
  • 若失败且设为非阻塞,则跳过本次操作或执行其他逻辑。

5.2.2 多信号量并发获取的处理策略

UCOSII 不支持直接在一个函数调用中等待多个信号量,但可以通过轮询或使用队列等方式实现类似功能。

轮询方式示例:

OS_ERR err1, err2;
BOOLEAN sem1Acquired = DEF_FALSE;
BOOLEAN sem2Acquired = DEF_FALSE;

while (1) {
    if (!sem1Acquired) {
        OSSemPend(&sem1, 0, OS_OPT_PEND_NON_BLOCKING, NULL, &err1);
        if (err1 == OS_ERR_NONE) sem1Acquired = DEF_TRUE;
    }

    if (!sem2Acquired) {
        OSSemPend(&sem2, 0, OS_OPT_PEND_NON_BLOCKING, NULL, &err2);
        if (err2 == OS_ERR_NONE) sem2Acquired = DEF_TRUE;
    }

    if (sem1Acquired && sem2Acquired) {
        // 同时获取两个信号量,执行逻辑
        sem1Acquired = DEF_FALSE;
        sem2Acquired = DEF_FALSE;
    }

    OSTimeDlyHMSM(0, 0, 0, 100); // 避免CPU占用过高
}

逻辑分析:

  • 每次循环尝试获取两个信号量。
  • 若两个都获取成功,则执行资源操作。
  • 释放后重置标志位,准备下一轮获取。

优化建议:

  • 若需保证两个信号量同时获取成功才执行操作,建议使用互斥量或队列机制替代。
  • 可以使用信号量组(UCOSIII 支持)来替代多个信号量管理。

5.3 信号量获取的异常处理

在实际应用中,信号量可能因为资源竞争、中断嵌套、死锁等问题导致获取失败。因此,必须对异常情况进行处理和日志记录。

5.3.1 资源不可用时的任务状态管理

任务在调用 OSSemPend 后若无法获取信号量,会进入等待状态( OS_TASK_STATE_PEND )。此时任务不会参与调度,直到信号量被释放或超时发生。

任务状态转换图:

graph LR
    A[就绪态] --> B[运行态]
    B --> C{是否调用OSSemPend?}
    C -- 是 --> D[等待态]
    D --> E{是否收到信号量或超时?}
    E -- 是 --> F[重新进入就绪态]
    E -- 否 --> D

处理建议:

  • 在等待态中加入看门狗机制,防止任务长时间阻塞导致系统异常。
  • 使用超时机制,避免任务陷入永久等待。

5.3.2 错误码返回与日志记录方法

OSSemPend 返回的错误码可用于诊断信号量获取失败的原因,常见错误码如下:

错误码 含义说明
OS_ERR_NONE 成功获取信号量
OS_ERR_NONE_AVAIL 信号量不可用,非阻塞模式返回
OS_ERR_TIMEOUT 等待超时
OS_ERR_PEND_ISR 在中断服务程序中调用,禁止阻塞操作
OS_ERR_OBJ_TYPE 传入的参数不是信号量类型

日志记录示例:

void LogSemaphoreError(OS_ERR err) {
    switch (err) {
        case OS_ERR_NONE_AVAIL:
            printf("Error: Semaphore not available.\n");
            break;
        case OS_ERR_TIMEOUT:
            printf("Error: Semaphore wait timeout.\n");
            break;
        case OS_ERR_PEND_ISR:
            printf("Error: Cannot pend in ISR.\n");
            break;
        case OS_ERR_OBJ_TYPE:
            printf("Error: Invalid object type.\n");
            break;
        default:
            printf("Error: Unknown error code %d.\n", err);
    }
}

逻辑分析:

  • 根据错误码输出日志信息,便于调试。
  • 可以将日志信息写入 Flash 或通过串口输出。

增强建议:

  • 集成到系统日志模块中,支持时间戳、任务名等信息。
  • 对于频繁发生的错误(如超时),可触发系统警报或任务重启机制。

综上所述, OSSemPend 作为 UCOSII 中信号量获取操作的核心函数,其调用方式、等待机制及异常处理都对系统的稳定性和实时性至关重要。通过合理使用阻塞/非阻塞模式、设置超时机制、处理错误码与日志记录,可以有效提升多任务系统的健壮性与可维护性。

6. 信号量释放操作(OSSemPost)

在多任务实时系统中,任务之间的同步和资源管理是核心问题。UCOSII通过信号量机制实现任务间的协调,而 信号量的释放(OSSemPost) 是这一机制中极为关键的环节。它不仅决定了资源的可用性,还影响任务调度、系统响应速度以及整体性能。本章将深入解析OSSemPost的执行逻辑、调用限制与高级应用,帮助开发者掌握其在多任务环境下的最佳实践。

6.1 信号量释放的执行逻辑

在UCOSII中, OSSemPost 函数用于释放一个信号量,其本质是将信号量计数器增加1。当任务完成对共享资源的使用后,应调用该函数通知其他等待任务资源已释放。

6.1.1 唤醒等待队列中的任务

当调用 OSSemPost 时,如果当前信号量值大于等于0,表示没有任务在等待,直接增加计数器;如果存在等待任务,UCOSII会从等待队列中唤醒一个任务,并将信号量计数器加1。这一过程由内核自动完成。

信号量释放流程图(Mermaid格式)
graph TD
    A[调用OSSemPost函数] --> B{信号量计数器是否>0?}
    B -->|是| C[直接计数器+1]
    B -->|否| D[检查是否有等待任务]
    D -->|有| E[唤醒最高优先级等待任务]
    D -->|无| F[计数器+1]
    E --> G[任务进入就绪态]
    F --> H[信号量可用]
代码示例
INT8U OSSemPost (OS_SEM *psem)
{
    if (psem->OSEventType != OS_EVENT_TYPE_SEM) {    // 检查事件类型是否为信号量
        return (OS_ERR_EVENT_TYPE);
    }

    OS_ENTER_CRITICAL();                            // 进入临界区,防止中断干扰
    if (psem->OSEventGrp != 0) {                    // 如果有任务在等待
        (void)OS_EventTaskRdy(psem, (void *)0, OS_STAT_SEM);  // 唤醒最高优先级任务
        OS_EXIT_CRITICAL();
        OSSched();                                    // 任务调度器执行
        return (OS_NO_ERR);
    } else {
        if (psem->OSCnt < OS_SEM_CTR) {             // 判断是否超过最大计数
            psem->OSCnt++;                          // 计数器+1
            OS_EXIT_CRITICAL();
            return (OS_NO_ERR);
        } else {
            OS_EXIT_CRITICAL();
            return (OS_ERR_SEM_OVF);                // 信号量溢出错误
        }
    }
}
代码逻辑分析
  1. 事件类型检查 :确保调用的事件是信号量类型,避免误操作。
  2. 进入临界区 :防止中断打断信号量操作,保证操作的原子性。
  3. 判断是否有等待任务 :若存在等待任务,调用 OS_EventTaskRdy 唤醒队列中的最高优先级任务。
  4. 调度器执行 :通过 OSSched() 触发任务调度,让被唤醒任务有机会执行。
  5. 无等待任务时处理 :直接增加计数器,若计数超过最大值则返回溢出错误。

6.1.2 优先级调度与任务切换机制

在多任务系统中,任务的优先级决定了资源的获取顺序。 OSSemPost 唤醒的是 等待队列中优先级最高的任务 。若该任务的优先级高于当前运行任务,则会立即触发任务切换,提高系统响应速度。

示例:优先级调度对比
当前任务优先级 唤醒任务优先级 是否切换
3 2
2 3
1 1

参数说明
- OSEventGrp :事件等待组,用于记录哪些优先级的任务正在等待该信号量。
- OSCnt :信号量的当前计数器值。
- OS_STAT_SEM :任务等待信号量的状态标识。

6.2 OSSemPost函数的正确使用

尽管 OSSemPost 功能强大,但其使用也受到一定限制,特别是在中断服务程序(ISR)中调用时必须格外小心。

6.2.1 在中断服务程序中的调用限制

在UCOSII中, OSSemPost 可以在中断服务程序中调用,但需要使用 OSSemPostISR 版本。这是因为 OSSemPost 可能会触发任务调度,而在中断上下文中直接调用 OSSched() 可能导致系统崩溃。

中断中调用示例
void EXTI0_IRQHandler(void)
{
    if (EXTI_GetITStatus(EXTI_Line0) != RESET) {
        OSSemPostISR(&SemKey);      // 在中断中释放信号量
        EXTI_ClearITPendingBit(EXTI_Line0);  // 清除中断标志
    }
}
与普通OSSemPost的对比
特性 OSSemPost OSSemPostISR
是否允许在ISR中调用
是否可触发调度 ✅(但需手动调用 OSSched()
是否检查临界区 ✅(但跳过部分检查)

注意事项
- OSSemPostISR 不会立即调度任务,而是设置一个标志,待退出中断后由主循环调用 OSSched()
- 若在中断中释放信号量并唤醒高优先级任务,建议在中断退出后调用 OSSched() 以确保任务及时执行。

6.2.2 释放信号量的时机选择

信号量释放的时机直接影响系统行为。以下是一些典型场景:

  1. 任务完成资源使用后
    - 例如:任务A访问串口后释放信号量,允许任务B继续访问。
  2. 中断服务中事件触发
    - 例如:按键中断中释放信号量,通知任务响应事件。
  3. 资源生产完成时
    - 例如:数据采集任务完成数据写入缓冲区后释放信号量,通知处理任务读取。
信号量释放时机对比表
场景 调用函数 调用位置 是否在ISR中
任务访问串口结束 OSSemPost 任务代码末尾
按键中断触发 OSSemPostISR 中断服务函数
数据采集完成 OSSemPost 采集任务末尾

6.3 信号量释放的高级应用

除了基本的资源释放功能, OSSemPost 还可用于实现更高级的同步机制,如广播信号量、公平调度等。

6.3.1 广播信号量与单播信号量的实现

  • 单播信号量 :仅唤醒一个等待任务,适用于一对一同步。
  • 广播信号量 :唤醒所有等待任务,适用于一对多同步(如事件通知)。
实现广播信号量

UCOSII本身不直接支持广播,但可以通过多次调用 OSSemPost 或自定义事件机制实现。

void BroadcastSemaphore(OS_SEM *psem)
{
    INT8U i;
    for (i = 0; i < MAX_WAITING_TASKS; i++) {
        OSSemPost(psem);   // 多次调用释放信号量
    }
}
优缺点对比
特性 单播信号量 广播信号量
唤醒任务数 1个 多个
适用场景 资源互斥访问 事件通知、状态更新
系统开销 较大
实现复杂度 简单 复杂(需队列管理)

6.3.2 多任务竞争资源的公平调度策略

在多个任务竞争同一资源时,若只采用默认的优先级调度策略,可能导致低优先级任务“饥饿”。为实现公平调度,可采用以下策略:

  • 轮询唤醒机制 :维护一个等待队列,按顺序唤醒任务。
  • 优先级反转避免 :使用互斥量+优先级继承机制(虽然不是信号量范畴,但常结合使用)。
  • 时间片轮转策略 :限制每个任务持有信号量的时间。
示例:轮询唤醒策略实现
OS_SEM semResource;
OS_TCB waitingTasks[MAX_TASKS];
INT8U waitIndex = 0;

void OSSemFairPost(OS_SEM *psem)
{
    if (psem->OSEventGrp != 0) {
        OS_TCB *prioTask = OS_EventFindHighestWaiter(psem);  // 找到优先级最高任务
        OS_EventTaskRdy(psem, (void *)0, OS_STAT_SEM);

        // 插入等待队列尾部
        waitingTasks[waitIndex++] = *prioTask;
        if (waitIndex >= MAX_TASKS) waitIndex = 0;
    }
}

逻辑说明
- OS_EventFindHighestWaiter 找到优先级最高的等待任务。
- 唤醒后将其插入等待队列尾部,下次释放信号量时优先唤醒其他任务,实现“公平轮询”。

总结

OSSemPost 作为UCOSII信号量机制的重要组成部分,不仅负责资源的释放,还影响任务调度、中断响应与系统性能。掌握其执行流程、调用限制与高级用法,是构建稳定、高效的多任务嵌入式系统的关键。在下一章中,我们将通过实际项目案例,进一步展示信号量在STM32平台上的综合应用。

7. STM32平台UCOSII信号量实战项目

7.1 多任务LED同步控制项目

在嵌入式系统中,LED控制是最常见的外设操作之一。但在多任务环境下,多个任务可能同时访问LED资源,导致控制冲突。为了解决这个问题,我们可以通过信号量来实现任务间的同步控制。

7.1.1 项目需求与任务划分

项目需求:

  • 使用两个任务控制LED的闪烁,任务1控制LED1以1秒频率闪烁,任务2控制LED2以2秒频率闪烁。
  • LED的控制必须通过信号量实现同步,避免同时操作导致资源竞争。

任务划分:

  • LED1_Task :控制LED1每隔1秒亮灭。
  • LED2_Task :控制LED2每隔2秒亮灭。
  • LED_Semaphore :用于控制LED访问的二进制信号量。

7.1.2 使用信号量协调任务执行顺序

步骤说明:

  1. 初始化信号量为1(可用)。
  2. 每个任务在操作LED前调用 OSSemPend() 获取信号量。
  3. 使用完成后调用 OSSemPost() 释放信号量。

代码示例:

#include "includes.h"

OS_SEM LED_Semaphore;

void LED1_Task(void *pdata) {
    while (1) {
        OSSemPend(&LED_Semaphore, 0, &err); // 获取信号量
        LED1_ON();
        OSTimeDlyHMSM(0, 0, 1, 0); // 延时1秒
        LED1_OFF();
        OSSemPost(&LED_Semaphore); // 释放信号量
    }
}

void LED2_Task(void *pdata) {
    while (1) {
        OSSemPend(&LED_Semaphore, 0, &err);
        LED2_ON();
        OSTimeDlyHMSM(0, 0, 2, 0); // 延时2秒
        LED2_OFF();
        OSSemPost(&LED_Semaphore);
    }
}

int main(void) {
    OSInit();
    OSSemCreate(&LED_Semaphore, 1); // 创建信号量,初始值为1
    OSTaskCreate(LED1_Task, NULL, &LED1_TaskStk[TASK_STK_SIZE], 5);
    OSTaskCreate(LED2_Task, NULL, &LED2_TaskStk[TASK_STK_SIZE], 6);
    OSStart();
}

代码说明:

  • OSSemCreate(&LED_Semaphore, 1); 创建一个二进制信号量,初始值为1。
  • OSSemPend() :尝试获取信号量,若不可用则阻塞。
  • OSSemPost() :释放信号量,允许其他任务访问。

7.2 按键中断与任务同步通信

在STM32平台上,按键通常连接到外部中断引脚。为了在中断服务程序中通知任务执行某些操作,我们可以使用信号量实现中断与任务之间的同步通信。

7.2.1 中断服务中释放信号量

实现原理:

  • 按键按下触发中断。
  • 中断服务函数中调用 OSSemPost() 释放信号量。
  • 任务通过 OSSemPend() 等待信号量,接收到后执行按键处理逻辑。

代码示例:

#include "includes.h"

OS_SEM Button_Sem;

void EXTI0_IRQHandler(void) {
    if (EXTI_GetITStatus(EXTI_Line0) != RESET) {
        OSSemPost(&Button_Sem); // 在中断中释放信号量
        EXTI_ClearITPendingBit(EXTI_Line0);
    }
}

void Button_Task(void *pdata) {
    while (1) {
        OSSemPend(&Button_Sem, 0, &err); // 等待信号量
        printf("Button pressed!\r\n");
    }
}

int main(void) {
    OSInit();
    OSSemCreate(&Button_Sem, 0); // 初始值为0,表示无信号
    OSTaskCreate(Button_Task, NULL, &Button_TaskStk[TASK_STK_SIZE], 4);
    OSStart();
}

注意事项:

  • 在中断服务程序中不能调用可能导致阻塞的函数,只能使用 OSSemPost()
  • 需要配置GPIO中断和NVIC优先级。

7.2.2 任务中获取信号量响应按键事件

任务在等待信号量期间可以进入阻塞状态,释放CPU资源,提高系统效率。当信号量被释放时,任务被唤醒执行响应逻辑。

7.3 多任务共享串口资源管理

串口是嵌入式系统中常用的通信接口,但在多任务系统中,多个任务同时使用串口会导致数据混乱。使用信号量可有效管理串口资源的访问权限。

7.3.1 串口访问冲突问题分析

问题现象:

  • 多个任务同时调用 USART_SendData()
  • 数据帧被分割或合并,通信失败。

解决方案:

  • 使用互斥信号量(二进制信号量)保护串口资源。
  • 每个任务在发送数据前必须先获取信号量,发送完成后释放。

7.3.2 利用信号量实现安全访问

代码示例:

OS_SEM UART_Sem;

void UART_Task1(void *pdata) {
    while (1) {
        OSSemPend(&UART_Sem, 0, &err);
        USART_SendString("Task1: Hello World\r\n");
        OSSemPost(&UART_Sem);
        OSTimeDlyHMSM(0, 0, 1, 0);
    }
}

void UART_Task2(void *pdata) {
    while (1) {
        OSSemPend(&UART_Sem, 0, &err);
        USART_SendString("Task2: Hello STM32\r\n");
        OSSemPost(&UART_Sem);
        OSTimeDlyHMSM(0, 0, 2, 0);
    }
}

int main(void) {
    OSInit();
    OSSemCreate(&UART_Sem, 1); // 串口资源初始可用
    OSTaskCreate(UART_Task1, NULL, &UART1_TaskStk[TASK_STK_SIZE], 3);
    OSTaskCreate(UART_Task2, NULL, &UART2_TaskStk[TASK_STK_SIZE], 4);
    OSStart();
}

参数说明:

  • OSSemCreate(&UART_Sem, 1) :初始化信号量为1,表示串口资源可用。
  • USART_SendString() :封装的串口发送函数,需自行实现。

7.4 项目调试与性能优化

在实际开发中,信号量使用不当可能导致死锁、资源竞争等问题。因此,项目调试和性能优化是关键步骤。

7.4.1 使用调试工具监控信号量状态

常用工具:

  • uC/Probe :实时监控信号量计数值、任务等待队列。
  • Tracealyzer :分析任务调度、信号量获取/释放事件时间线。

推荐操作:

  • 设置断点观察信号量结构体 OS_SEM Ctr 字段值。
  • 使用调试器查看任务状态和等待队列。

7.4.2 死锁检测与资源回收策略

死锁常见原因:

  • 任务A持有信号量S1,请求S2;任务B持有S2,请求S1。
  • 信号量未正确释放,任务永久阻塞。

预防策略:

  • 按照固定顺序申请资源。
  • 使用超时机制,避免无限等待:
OSSemPend(&sem, 100, &err); // 最多等待100个时钟节拍

7.4.3 信号量使用中的常见问题排查

常见问题与排查方法:

问题现象 可能原因 排查方法
任务无法运行 信号量未初始化 检查 OSSemCreate() 调用
信号量始终为0 未释放 检查 OSSemPost() 是否调用
任务死锁 多信号量循环等待 使用调试工具查看任务状态
中断中调用阻塞函数 函数使用错误 检查是否在ISR中调用了 OSSemPend()

建议实践:

  • 使用调试器单步执行,观察信号量变化。
  • 添加日志打印,记录信号量获取/释放动作。
  • 使用断言机制检测信号量状态。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:STM32是基于ARM Cortex-M内核的高性能微控制器,广泛用于嵌入式系统开发。UCOSII是一个轻量级实时操作系统(RTOS),为嵌入式应用提供多任务调度、内存管理和时间控制等功能。本资源“STM32的UCOSII信号量程序,亲测可用”提供了一个经过验证的实践项目,重点讲解如何在STM32平台上使用UCOSII的信号量机制。信号量是RTOS中实现任务同步与资源共享控制的关键工具,通过创建、获取和释放信号量,可以有效避免死锁和资源竞争问题。本项目包含完整示例代码和实验文档,适合嵌入式开发者学习和应用RTOS中的信号量技术,提升多任务系统的开发能力。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐