HRTOS应用示例:信号量机制详解

在实时操作系统中,多个任务可能需要访问同一个共享资源。例如:

  • 公共硬件设备

  • 串口

  • 显示屏

  • 存储设备

  • 共享数据

  • 临界资源

如果多个任务同时访问这些资源,就可能产生资源竞争。

信号量(Semaphore)是 RTOS 中用于任务同步和资源访问控制的重要机制。

本文以 HRTOS_Semaphore.c 示例为基础,通过两个任务竞争同一个信号量,演示 HRTOS 中信号量的初始化、获取、使用和释放过程,并结合裸机开发说明信号量解决的问题,同时分析 AI 编写 HRTOS 应用程序时需要注意的设计原则。


一、实验目的

本示例创建两个任务:

复制代码
TASK_A
优先级 3

TASK_B
优先级 4

两个任务共同使用:

复制代码
#define SEM_KEY 1

也就是说,两个任务竞争同一个信号量。

核心流程为:

复制代码
任务A ──┐
        ├──→ SEM_KEY
任务B ──┘

任意时刻只有成功获取信号量的任务可以进入资源使用区域。

因此,本实验主要验证:

多个任务竞争同一个信号量时,HRTOS 如何保证资源访问的互斥性。


二、为什么需要信号量

假设系统中有一个共享资源:

复制代码
共享资源
    ↑
 ┌──┴──┐
任务A  任务B

如果任务 A 和任务 B 同时访问:

复制代码
任务A → 使用资源
任务B → 同时使用资源

就可能产生冲突。

例如两个任务同时操作同一个硬件设备:

复制代码
任务A:发送数据AAAA
任务B:发送数据BBBB

如果没有任何保护机制,最终可能出现:

复制代码
A B A B A B ...

导致数据混乱。

信号量可以把资源访问变成:

复制代码
任务A
  ↓
获取信号量
  ↓
使用资源
  ↓
释放信号量

任务B
  ↓
等待信号量
  ↓
获取信号量
  ↓
使用资源
  ↓
释放信号量

从而避免多个任务同时进入资源访问区域。


三、本示例的整体设计

本示例包含两个任务。

任务 A

复制代码
优先级:3

周期性尝试获取信号量。

成功后:

复制代码
获取信号量
    ↓
LED_A翻转
    ↓
延时50
    ↓
释放信号量

任务 B

复制代码
优先级:4

同样竞争:

复制代码
SEM_KEY

成功后:

复制代码
获取信号量
    ↓
LED_B翻转
    ↓
延时50
    ↓
释放信号量

因此两个任务形成:

复制代码
TASK_A ──┐
         │
         ▼
      SEM_KEY
         ▲
         │
TASK_B ──┘

四、信号量的初始化

系统入口首先执行:

复制代码
os_sem_init(SEM_KEY, 1)

其中:

复制代码
SEM_KEY = 1

表示信号量的编号。

第二个参数:

复制代码
1

表示信号量初始计数值。

可以理解为:

复制代码
SEM_KEY
初始值 = 1

这意味着系统最开始允许一个任务成功获取该信号量。


五、为什么初始化之后还要调用 os_sem_post()

本示例最后还有:

复制代码
os_sem_post(SEM_KEY);

这一句非常值得注意。

因为示例采用的实际信号量使用流程,需要保证信号量在任务开始竞争时处于可用状态。

完整初始化流程为:

复制代码
os_sem_init()
      ↓
创建任务
      ↓
os_sem_post()
      ↓
任务开始竞争信号量

因此,在实际使用 HRTOS 信号量时,应当严格依据 HRTOS 当前版本的信号量实现和 API 语义来设计初始化流程,而不能仅根据其他 RTOS 的经验猜测。


六、任务如何获取信号量

任务 A:

复制代码
if (os_sem_wait(SEM_KEY) == 2)

任务 B:

复制代码
if (os_sem_wait(SEM_KEY) == 2)

两个任务使用的是同一个:

复制代码
SEM_KEY = 1

因此,它们实际上是在竞争同一个信号量。

当任务成功获取后,才执行:

复制代码
sem_count++;

LED_A = ~LED_A;

或者:

复制代码
sem_count++;

LED_B = ~LED_B;

也就是说:

只有获取信号量成功,任务才能进入资源使用区域。


七、为什么要判断返回值

这里不是简单调用:

复制代码
os_sem_wait(SEM_KEY);

而是:

复制代码
if (os_sem_wait(SEM_KEY) == 2)

说明应用程序需要根据 HRTOS API 的返回结果判断信号量获取是否成功。

只有:

复制代码
返回值 == 2

时,本示例才继续执行资源访问逻辑。

因此 AI 编写 HRTOS 程序时,不能只根据函数名称猜测返回值含义。

例如不能直接假设:

复制代码
1 = 成功

或者:

复制代码
0 = 成功

应该以 HRTOS 当前版本的 API 定义和官方示例为准。


八、获取成功以后发生什么

以任务 A 为例:

复制代码
if (os_sem_wait(SEM_KEY) == 2)
{
    sem_count++;

    LED_A = ~LED_A;

    os_delay(TICK_A);

    os_sem_post(SEM_KEY);
}

可以分成四个阶段:

复制代码
① 获取信号量
       ↓
② 进入资源使用区域
       ↓
③ 使用资源
       ↓
④ 释放信号量

其中:

复制代码
os_delay(TICK_A);

用于模拟任务占用共享资源的过程。

这里并不是说实际项目中获取信号量以后必须延时,而是为了让测试过程更加明显。


九、为什么获取信号量以后不能忘记释放

这是使用信号量最重要的规则之一。

获取:

复制代码
os_sem_wait(SEM_KEY)

成功以后,最终必须执行:

复制代码
os_sem_post(SEM_KEY);

否则可能出现:

复制代码
任务A获取信号量
      ↓
进入资源使用区域
      ↓
没有释放
      ↓
任务B一直无法获取

最终可能导致其他任务长期等待。

因此,可以把信号量的基本使用模式记成:

复制代码
Wait
 ↓
使用资源
 ↓
Post

即:

获取 → 使用 → 释放


十、两个任务为什么使用同一个信号量

代码中:

复制代码
#define SEM_KEY 1

任务 A:

复制代码
os_sem_wait(SEM_KEY)

任务 B:

复制代码
os_sem_wait(SEM_KEY)

它们使用相同的信号量 ID。

因此:

复制代码
        SEM_KEY
        /     \
       /       \
   TASK_A    TASK_B

如果使用不同的信号量:

复制代码
TASK_A → SEM_A

TASK_B → SEM_B

那么两个任务实际上不会通过同一个信号量互斥。

所以:

需要保护同一个共享资源的任务,必须使用同一个同步对象。


十一、为什么两个任务设置不同优先级

本示例:

复制代码
TASK_A = 优先级3
TASK_B = 优先级4

任务 B 优先级更高。

这样设计可以让实验同时体现:

复制代码
任务优先级
+
信号量竞争

但需要注意:

信号量解决的是资源访问问题,优先级解决的是任务调度问题。

二者不是同一个概念。

可以简单理解为:

复制代码
优先级
    ↓
决定谁更应该获得CPU

信号量
    ↓
决定谁可以进入共享资源访问区域

十二、信号量和任务延时不是一回事

代码中同时出现:

复制代码
os_sem_wait()

和:

复制代码
os_delay()

但它们承担完全不同的作用。

os_sem_wait()

用于:

复制代码
等待资源/同步条件

os_delay()

用于:

复制代码
让任务进入延时状态

因此不能把:

复制代码
os_delay(50);

简单理解成信号量的替代品。

二者解决的问题不同。


十三、与裸机开发的区别

裸机程序可能通过变量实现资源保护:

复制代码
if (busy == 0)
{
    busy = 1;

    // 使用资源

    busy = 0;
}

这种方式在简单程序中可以使用,但随着任务数量增加,任务状态和调度关系会越来越复杂。

RTOS 则可以使用信号量:

复制代码
if (os_sem_wait(SEM_KEY) == 2)
{
    // 使用资源

    os_sem_post(SEM_KEY);
}

资源访问关系更加明确。

因此,从裸机开发进入 RTOS 开发后,可以逐渐从:

自己维护各种状态变量

转变为:

使用 RTOS 提供的同步机制管理任务之间的资源竞争。


十四、完整代码

复制代码
#include "hrtos.h"

/*==================== 配置 ====================*/
#define TASK_A       1
#define TASK_B       2

#define PRIO_A       3
#define PRIO_B       4

#define STACK_A      5
#define STACK_B      5

#define SEM_KEY      1

#define TICK_A       50
#define TICK_B       50

/*==================== LED 定义 ====================*/
sbit LED_A = P1^0;   /* 任务A指示灯 */
sbit LED_B = P1^1;   /* 任务B指示灯 */

/*==================== 全局 ====================*/
static u8 sem_count = 0;

/*==================== 任务A ====================*/
static void task_a(void)
{
    LED_A = 1;

    while (1)
    {
        /*
         * 获取信号量
         */
        if (os_sem_wait(SEM_KEY) == 2)
        {
            sem_count++;

            /* 获取成功 → 翻转LED */
            LED_A = ~LED_A;

            /*
             * 模拟占用资源
             */
            os_delay(TICK_A);

            /*
             * 释放信号量
             */
            os_sem_post(SEM_KEY);
        }

        os_delay(10);
    }
}

/*==================== 任务B ====================*/
static void task_b(void)
{
    LED_B = 1;

    while (1)
    {
        /*
         * 获取信号量
         */
        if (os_sem_wait(SEM_KEY) == 2)
        {
            sem_count++;

            /* 获取成功 → 翻转LED */
            LED_B = ~LED_B;

            /*
             * 模拟占用资源
             */
            os_delay(TICK_B);

            /*
             * 释放信号量
             */
            os_sem_post(SEM_KEY);
        }

        os_delay(10);
    }
}

/*==================== 系统入口 ====================*/
void hrtos_main(void)
{
    if (os_sem_init(SEM_KEY, 1) != 1)
    {
        while (1);
    }

    if (os_task_create(task_a,
                       TASK_A,
                       PRIO_A,
                       STACK_A) != 1)
    {
        while (1);
    }

    if (os_task_create(task_b,
                       TASK_B,
                       PRIO_B,
                       STACK_B) != 1)
    {
        while (1);
    }

    os_sem_post(SEM_KEY);
}

十五、实验现象

程序运行以后,可以通过两个 LED 观察任务获取信号量的情况。

复制代码
LED_A
 ↓
TASK_A成功获取信号量

LED_B
 ↓
TASK_B成功获取信号量

两个任务不断竞争同一个:

复制代码
SEM_KEY

成功获取后才执行 LED 翻转和模拟资源占用。

因此可以直观观察两个任务对同一个同步对象的竞争过程。


十六、信号量的核心使用模型

把整个示例简化以后,实际上就是:

复制代码
if (os_sem_wait(SEM_KEY) == 2)
{
    /* 进入受保护区域 */

    // 使用共享资源

    /* 释放信号量 */
    os_sem_post(SEM_KEY);
}

这就是 HRTOS 应用中非常典型的信号量使用模式。

可以记住:

复制代码
获取
 ↓
临界资源
 ↓
释放

而不是:

复制代码
获取
 ↓
一直占用

十七、这个示例对 AI 编写 HRTOS 程序的意义

信号量属于非常容易被 AI "写得像,但实际上不一定对"的功能。

例如 AI 可能熟悉 FreeRTOS、RT-Thread 等系统,然后直接按照其他系统的 API 习惯生成代码。

但不同 RTOS 的:

  • API 名称

  • 返回值

  • 初始化方式

  • 计数规则

  • 等待机制

  • 调度行为

都可能不同。

因此:

不能因为函数名字相似,就认为底层行为完全相同。

例如本示例明确使用:

复制代码
os_sem_wait(SEM_KEY) == 2

判断获取结果。

这就是 HRTOS 自身 API 语义的一部分。


十八、AI 编写 HRTOS 应用程序时的正确方法

如果让 AI 编写 HRTOS 信号量程序,应该明确告诉 AI:

复制代码
1. 使用 HRTOS 官方信号量 API。
2. 使用 os_sem_init() 初始化信号量。
3. 使用 os_sem_wait() 获取信号量。
4. 获取成功后才能访问共享资源。
5. 使用 os_sem_post() 释放信号量。
6. 获取和释放必须形成完整配对。
7. 严格按照 HRTOS 的返回值判断规则编写。
8. 不要直接套用其他 RTOS 的信号量实现。

这样才能减少 AI 根据其他 RTOS 经验"自行脑补"的问题。


十九、总结

HRTOS_Semaphore.c 通过两个任务竞争同一个信号量,完整演示了 HRTOS 中资源互斥的基本思想。

核心流程为:

复制代码
TASK_A ──┐
         │
         ▼
      SEM_KEY
         ▲
         │
TASK_B ──┘
         │
         ▼
     获取成功
         │
         ▼
     使用资源
         │
         ▼
     释放信号量

信号量解决的核心问题是:

多个任务访问共享资源时,如何避免资源竞争。

而优先级解决的是:

多个任务同时就绪时,谁应该优先获得 CPU。

理解这两个概念,是从裸机开发进入 HRTOS 应用开发的重要一步。

对于 AI 辅助开发同样如此。生成 HRTOS 代码时,不能只看 API 名称,更应该理解 HRTOS 自身的 API 语义、任务状态和调度机制,再根据这些规则设计应用程序。

相关推荐
白猫不黑7 小时前
AI时代如何利用AI学好网络安全
人工智能·安全·web安全·计算机·网络安全·信息安全
码云之上7 小时前
让聊天机器人学会工作方法,星悟接 Agent Skills 的实践
人工智能·架构·全栈
打破砂锅问到底0077 小时前
端侧 Agent:手机本地多 Agent 协作
人工智能·ai·ai工程化·agent skills
容器魔方7 小时前
议程一览 | 华为云亮相 KubeCon + CloudNativeCon China 2026
人工智能·云原生·容器·开源·华为云·云计算
LuminousCPP7 小时前
数据结构 - 排序(二):快速排序从错误初版到优化版|双指针划分 + 三数取中 + 小区间插入优化
c语言·数据结构·笔记·算法·排序算法
竞赛考级题库8 小时前
202606 青少年等级考试C/C++真题一级 建议答题时长:60min
java·c语言·c++
国科安芯8 小时前
抗辐射MCU在低轨卫星电源管理单元中的电压监测与保护策略研究
大数据·单片机·嵌入式硬件·电源管理·低轨卫星·抗辐射·星间链路
Csvn8 小时前
第 20 章 质量保障 Harness 与评测体系
人工智能·aigc·agent
4SAPI8 小时前
2026年大模型API接入选型指南:企业与个人用户的架构、稳定性与成本考量
大数据·开发语言·数据库·人工智能·架构·php
paopaokaka_luck8 小时前
非遗文物数字化系统(AI非遗问答、ONNX图像识别、协同过滤推荐、ECharts数据分析、非遗知识浏览与互动、文创商城订单闭环、文化活动报名签到、社区交流)
前端·javascript·vue.js·人工智能·数据分析·echarts