多任务并发
LED 闪烁 + 按键控制 — OSAL 任务管理 API、GPIO 驱动
学习目标
- 理解 WS63 的软件架构——OSAL(Operating System Abstraction Layer)抽象层如何屏蔽不同 RTOS 内核的差异,以及为什么应用代码只调 OSAL API
- 掌握
osal_kthread_create()创建任务的完整写法:栈大小如何估算、优先级如何选择 - 通过两个任务的实际运行,理解抢占式调度的行为——高优先级就绪时 CPU 如何被立即抢占
- 掌握调度锁
osal_kthread_lock()/unlock()保护任务间共享数据的使用场景 - 能够在 WS63 开发板上运行多任务,通过按键控制 LED 任务的启停来验证理解
基本概念
OS 抽象层(OSAL)
WS63 SDK 在应用代码与 RTOS 内核之间设计了一层 OSAL(OS Abstraction Layer),提供统一的 osal_* 接口。OSAL 的作用是屏蔽不同 RTOS 内核的 API 差异——无论底层是 LiteOS、FreeRTOS、RT-Thread 还是裸机(Non-OS),应用代码都调用同一套 osal_* 函数。切换内核时不需要修改任何应用逻辑。
flowchart TD
A[你的应用代码] --> B[OSAL API — osal_*]
B --> C1[LiteOS]
B --> C2[FreeRTOS]
B --> C3[RT-Thread]
B --> C4[Non-OS 裸机]
C1 --> D[WS63 RISC-V 硬件]
C2 --> D
C3 --> D
C4 --> D
开发时只调
osal_*函数,不需要关心底层用的是哪个内核。osal_*的声明在kernel/osal/include/下各子目录中。
任务优先级和抢占
底层 RTOS 内核均为抢占式调度:任何时候高优先级任务就绪,会立即抢占正在运行的低优先级任务。优先级数字越小越高——OSAL_TASK_PRIORITY_HIGH(3)高于 OSAL_TASK_PRIORITY_LOW(10)。
sequenceDiagram
participant KEY as KEY 任务 prio=3
participant LED as LED 任务 prio=10
LED->>LED: 运行中,翻转 GPIO
Note over KEY: 按键中断唤醒 KEY 任务
KEY-->>LED: 抢占! LED 被暂停
KEY->>KEY: 处理按键,挂起 LED 任务
KEY->>KEY: osal_msleep() 等待按键
Note over LED: KEY 睡了,LED 恢复(如果没被挂起)
调度锁:保护共享数据
如果两个任务都读写同一个全局变量,就需要保护。osal_kthread_lock() 禁止任务切换但不禁止中断——适合保护只有任务会访问的共享数据。
三种保护的适用场景:
| 机制 | 禁止什么 | 持锁上限 | 典型场景 |
|---|---|---|---|
osal_irq_lock() |
中断 + 调度 | < 10μs | ISR 与任务共享的数据 |
osal_kthread_lock() |
任务调度 | < 1ms | 任务间共享的变量 |
osal_mutex_lock() |
同一资源的其他访问 | 不限 | 保护复杂数据结构 |
osal_msleep() 不是空转
裸机编程中 delay(1000) 是 CPU 空转 1000ms。RTOS 中 osal_msleep(1000) 是让出 CPU 给其他任务,1000ms 后再回来。这对低功耗至关重要——CPU 可以在 sleep 期间进入休眠。
涉及 API
| API | 谁调用 | 用途 | 头文件 |
|---|---|---|---|
osal_kthread_create(handler, data, name, stack_size) |
入口任务 | 创建任务,返回 osal_task* |
osal_task.h |
osal_kthread_set_priority(task, priority) |
入口任务 | 设置任务优先级 | osal_task.h |
osal_kthread_lock() / osal_kthread_unlock() |
KEY 任务 | 调度锁——禁止/恢复任务调度 | osal_task.h |
osal_kthread_suspend(task) / osal_kthread_resume(task) |
KEY 任务 | 挂起/恢复 LED 任务 | osal_task.h |
osal_msleep(ms) |
LED/KEY 任务 | 毫秒延迟(让出 CPU) | osal_task.h |
uapi_gpio_init() |
入口任务 | 初始化 GPIO 子系统 | gpio.h |
uapi_gpio_set_dir(pin, dir) |
入口任务 | 设置引脚输入/输出方向 | gpio.h |
uapi_gpio_toggle(pin) |
LED 任务 | 翻转引脚电平 | gpio.h |
案例说明
做什么
一块 WS63 同时跑两个任务——LED 闪烁和按键检测。按键按下时 LED 停止闪烁(保持当前亮灭状态),再次按下恢复。验证 RTOS 多任务并发的核心概念:优先级抢占、调度锁、任务挂起/恢复。
这个案例是后续所有 OS 案例(中断、IPC、定时器)的基础。后续案例会逐步优化按键检测方式——本篇用轮询,下一篇「中断处理」会用 ISR 替代。
规格与功能
| 规格项 | KEY 任务 | LED 任务 |
|---|---|---|
| 优先级 | OSAL_TASK_PRIORITY_HIGH(3) |
OSAL_TASK_PRIORITY_LOW(10) |
| 栈大小 | 4096 字节 | 2048 字节 |
| 职责 | 检测按键,控制 LED 任务启停 | 循环翻转 LED 引脚 |
| 行为 | 按键按下 → 挂起/恢复 LED 任务 | osal_msleep(500) 翻转一次 |
程序运行流程:
app_run入口创建 KEY 任务和 LED 任务- KEY 任务优先级更高,创建后先执行 → 进入
osal_msleep()等待按键 - LED 任务获得 CPU → 开始 500ms 间隔闪烁
- 按键按下 → KEY 任务被唤醒 → 抢占 LED 任务 → 挂起 LED 任务
- 再次按键 → 恢复 LED 任务 → LED 恢复闪烁
任务架构
flowchart TD
ENTRY[app_run 入口] --> CREATE_KEY[创建 KEY 任务<br/>prio=3, stack=4096]
ENTRY --> CREATE_LED[创建 LED 任务<br/>prio=10, stack=2048]
CREATE_KEY --> KEY_LOOP[KEY 任务循环<br/>等按键 → 挂起/恢复 LED]
CREATE_LED --> LED_LOOP[LED 任务循环<br/>翻转 GPIO → sleep 500ms]
KEY_LOOP -.->|suspend/resume| LED_LOOP
案例流程
sequenceDiagram
participant E as 入口
participant K as KEY 任务
participant L as LED 任务
participant G as GPIO
E->>G: uapi_gpio_init()
E->>G: 设置 LED_PIN 为输出
E->>G: 设置 KEY_PIN 为输入+上拉
E->>K: osal_kthread_create(KEY, prio=3)
E->>L: osal_kthread_create(LED, prio=10)
activate K
Note over K: 高优先级,先执行
K->>K: osal_msleep(100) — 等待按键
activate L
Note over L: KEY 睡了,LED 开始运行
loop LED 闪烁
L->>G: uapi_gpio_toggle(LED_PIN)
L->>L: osal_msleep(500)
end
Note over K: 按键按下,KEY 被唤醒
K-->>L: 抢占! LED 暂停
K->>K: osal_kthread_lock()
K->>K: osal_kthread_suspend(LED)
K->>K: osal_kthread_unlock()
deactivate L
Note over L: LED 任务被挂起
K->>K: osal_msleep(100) — 再次等待
Note over K: 再次按键
K->>K: osal_kthread_resume(LED)
activate L
Note over L: LED 恢复闪烁
案例操作指导
第一步:编译
需确保代码中已开启 OSAL 任务管理和 GPIO 相关配置。
第二步:烧录并运行
烧录固件后上电。
第三步:验证
上电后 LED 以 1Hz 频率闪烁(500ms 亮 + 500ms 灭)。按下 KEY 按键:
- 第一次按压 → LED 停止闪烁(保持当前亮或灭状态)
- 第二次按压 → LED 恢复闪烁
- 每次按压都能可靠切换
关键配置
| 参数 | 值 | 说明 | 调大影响 | 调小影响 |
|---|---|---|---|---|
| KEY 任务栈 | 4096 | 有 GPIO 操作,留余量 | 浪费 RAM | 可能栈溢出死机 |
| LED 任务栈 | 2048 | 简单循环 + GPIO 翻转,够用 | 浪费 RAM | 可能栈溢出死机 |
| KEY 优先级 | HIGH(3) |
确保按键响应不被 LED 阻塞 | 可能抢占更重要任务 | 按键响应变慢 |
| LED 优先级 | LOW(10) |
后台任务,随时可被抢占 | 抢占其他低优任务 | 闪烁可能被长时间阻塞 |
| LED 闪烁周期 | 500ms | osal_msleep(500) 控制 |
闪得慢 | 闪得快,功耗略增 |
| 调度锁持锁 | < 1ms | 只保护 g_led_running 标志位 |
其他任务饿死 | 保护范围不够 |
栈大小选小了 → 栈溢出,死机。选太大了 → 浪费 RAM。2048 是简单死循环任务的常用值,4096 是回调较多或有局部数组任务的常用值。如果不确定,先给 4096,跑起来后用栈水位检测 API 观察实际使用量。
代码详解
应用入口:创建两个任务
app_run() 是 SDK 的约定——系统初始化完成后自动调用。在其中创建 KEY 和 LED 两个任务。用 osal_kthread_lock() 包裹创建过程,防止在创建第二个任务之前就被调度抢占。
#include "osal_task.h"
#include "driver/gpio.h"
#define LED_PIN S_MGPIO10 // LED 连接引脚,根据硬件调整
#define KEY_PIN S_MGPIO11 // 按键连接引脚,根据硬件调整
static osal_task *g_led_task = NULL; // 供 KEY 任务挂起/恢复用
static void app_entry(void)
{
osal_task *key_task = NULL;
uapi_gpio_init();
/* 配置 LED 引脚为输出 */
uapi_gpio_set_dir(LED_PIN, GPIO_DIRECTION_OUTPUT);
/* 配置 KEY 引脚为输入 + 上拉——未按下时为高电平 */
uapi_gpio_set_dir(KEY_PIN, GPIO_DIRECTION_INPUT);
// 上拉配置:通过 pinctrl 设置,此处省略
osal_kthread_lock();
/* 先创建高优先级 KEY 任务 */
key_task = osal_kthread_create(
(osal_kthread_handler)key_task_handler,
NULL, "KeyTask", 4096);
osal_kthread_set_priority(key_task, OSAL_TASK_PRIORITY_HIGH); // 3
/* 再创建低优先级 LED 任务 */
g_led_task = osal_kthread_create(
(osal_kthread_handler)led_task_handler,
NULL, "LedTask", 2048);
osal_kthread_set_priority(g_led_task, OSAL_TASK_PRIORITY_LOW); // 10
osal_kthread_unlock();
/* 解锁后,高优先级 KEY 任务立即获得 CPU */
}
app_run(app_entry);
osal_kthread_set_priority()在osal_kthread_create()之后调用。创建时任务继承当前优先级,创建后再设为目标值。
LED 闪烁任务
死循环中翻转 GPIO 然后让出 CPU。osal_msleep(500) 是关键——如果换成 delay(500),CPU 会空转 500ms,其他任务全部饿死。
static int led_task_handler(void *data)
{
(void)data;
while (1) {
uapi_gpio_toggle(LED_PIN); // 翻转电平:高→低,低→高
osal_msleep(500); // 让出 CPU 给其他任务 500ms
}
return 0;
}
按键检测任务
用轮询方式检测按键——循环读 GPIO 电平,检测到下降沿(高→低)表示按下。按一次切换 LED 任务状态,用调度锁保护标志位。
#include "osal_task.h"
static int key_task_handler(void *data)
{
(void)data;
bool led_running = true;
int last_level = 1; // 上拉:默认高电平
while (1) {
/* 简化轮询读 GPIO——实际用 gpio.h 中对应 API 读取电平 */
int level = 1; // TODO: 替换为 uapi_gpio_get_level(KEY_PIN)
if (last_level == 1 && level == 0) {
/* 下降沿:按键按下 */
osal_kthread_lock();
/* 加锁——修改 g_led_running 期间不许其他任务插进来 */
if (led_running) {
osal_kthread_suspend(g_led_task);
led_running = false;
} else {
osal_kthread_resume(g_led_task);
led_running = true;
}
osal_kthread_unlock();
}
last_level = level;
osal_msleep(100); // 100ms 轮询一次,让出 CPU
}
return 0;
}
为什么要加锁?因为
led_running可能被多个任务访问。不加锁时:KEY 任务刚判断完led_running == true准备挂起 LED 任务,此时发生调度、LED 任务修改了led_running——KEY 任务继续执行时挂起的是错误的任务。
优先级抢占的观察点
上电后的执行时序:
app_entry解锁后,KEY 任务(prio=3)比 LED 任务(prio=10)优先级高,立即抢占 CPU- KEY 任务执行到
osal_msleep(100)→ 主动让出 CPU - LED 任务获得 CPU → 开始循环翻转 LED
- 100ms 后 KEY 任务就绪 → 抢占 LED 任务 → 读 GPIO → 又
osal_msleep(100)→ 继续循环 - 虽然两个任务同时存在,但 KEY 任务每次只占用极短的 CPU 时间(微秒级),LED 闪烁几乎不受影响
这就是抢占式调度:高优先级任务就绪时立即抢占,但通过
osal_msleep()主动让出 CPU,低优先级任务仍然能获得充分的执行时间。