在实时操作系统中,多个任务可能需要访问同一个共享资源。例如:
-
公共硬件设备
-
串口
-
显示屏
-
存储设备
-
共享数据
-
临界资源
如果多个任务同时访问这些资源,就可能产生资源竞争。
信号量(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 语义、任务状态和调度机制,再根据这些规则设计应用程序。