跳转至

多任务并发

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_kthread_lock();
g_led_running = false;   // 安全修改——不会有其他任务插进来
osal_kthread_unlock();

三种保护的适用场景:

机制 禁止什么 持锁上限 典型场景
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) 翻转一次

程序运行流程:

  1. app_run 入口创建 KEY 任务和 LED 任务
  2. KEY 任务优先级更高,创建后先执行 → 进入 osal_msleep() 等待按键
  3. LED 任务获得 CPU → 开始 500ms 间隔闪烁
  4. 按键按下 → KEY 任务被唤醒 → 抢占 LED 任务 → 挂起 LED 任务
  5. 再次按键 → 恢复 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 恢复闪烁

案例操作指导

第一步:编译

fbb build ws63-liteos-app

需确保代码中已开启 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 任务继续执行时挂起的是错误的任务。

优先级抢占的观察点

上电后的执行时序:

  1. app_entry 解锁后,KEY 任务(prio=3)比 LED 任务(prio=10)优先级高,立即抢占 CPU
  2. KEY 任务执行到 osal_msleep(100) → 主动让出 CPU
  3. LED 任务获得 CPU → 开始循环翻转 LED
  4. 100ms 后 KEY 任务就绪 → 抢占 LED 任务 → 读 GPIO → 又 osal_msleep(100) → 继续循环
  5. 虽然两个任务同时存在,但 KEY 任务每次只占用极短的 CPU 时间(微秒级),LED 闪烁几乎不受影响

这就是抢占式调度:高优先级任务就绪时立即抢占,但通过 osal_msleep() 主动让出 CPU,低优先级任务仍然能获得充分的执行时间。