基于 OpenHarmony 5.0 的星闪轻量型设备应用开发——Ch2 OpenHarmony LiteOS-M 内核应用开发

发布于:2025-04-14 ⋅ 阅读:(22) ⋅ 点赞:(0)

写在前面: 此篇是系列文章《基于 OpenHarmony5.0 的星闪轻量型设备应用开发》的第 2 章。本篇介绍了如何在 OpenHarmony 5.0 框架下,针对 WS63 进行 LiteOS-M 内核应用工程的开发。

为了方便读者学习,需要OpenHarmony 5.0 WS63 SDK 的小伙伴可以访问润和软件官方网站咨询获取。

2.1 OpenHarmony LiteOS-M 内核简介

2.2.1 OpenHarmony 的多内核结构设计

OpenHarmony 操作系统整体遵从分层设计, 自顶向下分别为应用层框架层系统服务层以及内核层

作为一个分布式操作系统框架,OpenHarmony 同样有内核层的概念。内核层是 OpenHarmony 的最底层,提供了包括任务调度、线程互斥等在内的各种基本操作系统内核功能。

OpenHarmony 的内核层由内核子系统驱动子系统组成。其中内核子系统采用多核(Linux 内核或者 LiteOS)设计,支持针对不同资源受限设备选用合适的 OS 内核。例如,OpenHarmony 在轻量系统和小型系统上使用 LiteOS 内核,它是面向 IoT 领域的实时操作系统内核,同时具备 RTOS 的轻快还有 Linux 的用的特点。按其系统体量细分,LiteOS 内核又可分为 LiteOS-A 以及 LiteOS-M 这两个内核,其中后者主要针对轻量系统(mini system),具有小体积、低功耗、高性能的特点。

图 2.1-1 OpenHarmony LiteOS-M 内核架构图

2.1.2 OSAL

OSAL(Operating System Abstraction Layer,操作系统抽象层),其核心目标是屏蔽不同操作系统之间的差异,为上层应用程序提供统一的编程接口。在实际开发中,存在着众多类型的操作系统,如常见的实时操作系统(RTOS)FreeRTOS、uC/OS,以及通用操作系统 Linux、Windows 等。这些操作系统在任务管理、内存管理、文件系统等方面的实现方式和接口差异较大。OSAL 的作用就是将这些差异屏蔽起来,为上层应用提供统一的操作接口。例如,不同的 RTOS 对于任务创建的函数名、参数列表可能都不一样,通过 OSAL 可以提供一个统一的函数,让开发者无需关心底层具体使用的是哪种 RTOS。

2.1.3 WS63 的应用开发介绍

润和软件出品的 WS63 模组基于海思 Hi3863 解决方案,该模组内置了高性能 RISC-V 架构的 32 位 MCU,在 OpenHarmony 的设备分级中属于“轻量型”。

由于 OpenHarmony 官方发布的源码仅仅提供了不同内核的框架支持,如果针对某款芯片做应用开发,需要将与芯片有关的 SDK 移植到 OpenHarmony 源码框架下才行。润和软件已经完成了 WS63 SDK 的 OpenHarmony 移植工作,并将其源码开源供开发者使用。

WS63 SDK 存放在 OpenHarmony 源码目录的 device/soc/hisilicon/ws63v100/sdk 目录中,以下将该目录简称为“SDK 目录”。

2.2 LiteOS-M 内核线程功能

2.2.1 内核线程及其调度管理

(1)任务与线程

从系统的角度来看,**任务(Task)**是竞争系统资源的最小单元,OpenHarmony LiteOS-M 的任务模块可以给用户提供多个任务,实现任务间的切换。

在操作系统领域中,与任务类似的概念又被称为线程(Thread),且在 OpenHarmony 源代码中相关 API 名称及结构体均使用 Thread 一词,故而本文中不对任务、线程两个名字做严格区分,且更多以“线程”代之。

(2)线程调度

LiteOS-M 的任务模块采用抢占式线程调度机制:

  • 共分为 32 个任务优先级(0…31),其中数字越小优先级越高,0 代表最高优先级,31 代表最低优先级
  • 高优先级线程可以打断低优先级线程,低优先级线程必须在高优先级线程阻塞或者结束后才能得到调度运行
  • 相同优先级线程支持使用时间片轮转调度和 FIFO 调度方式
(3)线程状态

在系统初始化完成之后,创建的线程就可以在系统中竞争一定的资源,此时将由内核进行统一调度。线程通常可分以下几种状态:

  • 初始化(Init):该线程正在被创建。
  • 就绪(Ready):该线程在就绪队列中,等待被 CPU 执行。
  • 运行(Running):该线程正在被执行。
  • 阻塞(Blocked):该线程被阻塞挂起。这一状态包括:pend(因为锁、事件、信号量等阻塞)、suspend(主动 pend)、delay(延时阻塞)、pendtime(因为锁、事件、信号量时间等超时等待)。
  • 退出(Exit):该线程运行结束,等待父线程回收其控制块资源。
(4)线程状态迁移

图 2.2-1 线程状态迁移

  • 创建(Init) → 就绪(Ready):

线程创建拿到控制块后为创建状态,处于线程初始化阶段,当线程初始化完成将线程插入调度队列,此时线程进入就绪状态。

  • 就绪(Ready) → 运行(Running):

线程创建后进入就绪态,发生线程切换时,就绪列表中最高优先级的线程被执行,从而进入运行态,但此刻该线程会从就绪列表中删除。

  • 运行(Running) → 阻塞(Blocked):

正在运行的线程发生阻塞(挂起、延时、读信号量等)时,该线程会从就绪列表中删除,线程状态由运行态变成阻塞态,然后发生线程切换,运行就绪列表中剩余最高优先级线程

  • 阻塞(Blocked) → 就绪(Ready) / 阻塞(Blocked) → 运行(Running):

阻塞的线程被恢复后(线程恢复、延时时间超时、读信号量超时或读到信号量等),此时被恢复的线程会被加入就绪列表,从而由阻塞态变成就绪态;此时如果被恢复线程的优先级高于正在运行线程的优先级,则会发生线程切换,将该线程由就绪态变成运行态。

  • 就绪(Ready) → 阻塞(Blocked):

线程也有可能在就绪态时被阻塞(挂起),此时线程状态会由就绪态转变为阻塞态,该线程从就绪列表中删除,不会参与线程调度,直到该线程被恢复。

  • 运行(Running) → 就绪(Ready):

有更高优先级线程创建或者恢复后,会发生线程调度,此刻就绪列表中最高优先级线程变为运行态,那么原先运行的线程由运行态变为就绪态,并加入就绪列表中。

  • 运行(Running) → 退出(Exit):

运行中的线程运行结束,线程状态由运行态变为退出态。

  • 阻塞(Blocked) → 退出(Exit):

阻塞的线程调用删除接口,线程状态由阻塞态变为退出态。

线程创建后,用户态可以执行线程调度、挂起、/恢复、延时等操作,同时也可以设置线程优先级和调度策略,获取线程优先级和调度策略。这些操作均可以通过对应的内核线程接口函数来完成(见 2.2.3 内核线程接口介绍)。

2.2.2 内核线程开发的基本步骤

OpenHarmony 为内核线程的相关操作提供了诸多接口函数,通过调用对应的接口函数,即可实现不同的功能。一般的开发步骤可以总结如下:

  • 编写线程入口函数:根据实际需求,编写独立 / 协同的任务(线程)入口函数,并定义函数参数
  • 预设置线程的参数属性:为不同线程设置各自的名称、控制块信息(可选)、栈区信息(可选)、不同的优先级别
  • 新建内核线程:调用内核功能接口,创建线程,并接收返回的线程 id
  • 视需求进行内核线程调度、管理:
    • 查询线程运行状态
    • 挂起、恢复线程
    • 等待线程执行完毕或者提前结束线程

2.2.3 内核线程接口介绍

针对 WS63 的 LiteOS-M 内核线程的接口均在 SDK 目录/kernel/osal/include/schedule/osal_task.h 文件中声明,在使用的时候需要包含这个头文件。

(1)创建内核线程
  • 原型:osal_task *osal_kthread_create(osal_kthread_handler handler, void *data, const char *name, unsigned int stack_size);
  • 参数:
    • handler 是一个 osal_kthread_handler 类型的函数指针,该指针指向的是线程功能函数的入口,即该线程如果被创建成功,将会执行由 handler 指向的函数功能。因此,在创建内核线程之前,首先要定义该线程的入口函数。

【说明】线程的入口函数有统一的函数原型。在
SDK目录/kernel/osal/include/schedule/osal_task.h
文件中可以找到 osal_kthread_handler 的定义(如代码 2.2-1
所示),它是一个函数指针类型定义,描述了线程入口函数的参数、返回值信息。线程入口函数内容与常规代码并没有显著区别。通常需要注意几点:

  • 使用强制类型转换处理 param 参数,如果不使用它,用 (void) 进行修饰
  • 使用 osal_msleep 代替 sleep 等方法进行延时
  • 灵活使用事件机制与信号量机制

代码 2.2-2 是一个线程入口函数定义的示例。

// 代码 2.2-1
/// Entry point of a thread.
typedef struct {
    void *task;
} osal_task;
typedef int (*osal_kthread_handler)(void *data);
// 代码 2.2-2

int Task1 (void *param)
{
    // 告知编译器不去检查 param 是否被使用
    (void)param;
    // 写个死循环
    while(1)
    {
        // 每隔 1s 发送一行内容
        osal_printk("Hello!\r\n");
        osal_msleep(1000);	// osal_msleep 使用 1ms 基准延时,因此参数指定为 1000 时将延时 1s
    }
}
- data 为 void * 空指针类型,实际上代表一个万能数据类型,可以通过强制类型转换将其他类型的值作为参数传递给线程。
- name 为一个 const char 类型数据,用于指定线程的名称。
- stack_size 是一个 unsigned int 型的数据,用于指定线程运行所需要的栈空间大小。(这里强调一下,不同的线程需要的栈空间大小不同,如果指定的栈空间过小,会导致线程运行出错)
  • 返回值:
    • 如果成功创建了线程,则返回指向该线程的 osal_task 类型的指针,其定义见代码 2.2-1 。
    • 如果未成功创建,则返回值为 NULL。
  • 调用:以下是一个调用的示例代码,注意该函数在调用前,应确保已经对线程的入口函数进行了定义。
// 代码 2.2-3

// 调用前,已经定义了 Task1 

// 创建线程
osal_task * task1 = osal_kthread_create((osal_kthread_handler)Task1, NULL, "Task1", 0x1000);

// 对 task1 的值进行判断以确定线程是否创建成功,后续也可使用该值对线程进行调度、管理
if(task1 != NULL)
{
    osal_printk("Task1 created with handler: %p\r\n", task1);
}
else
{
    osal_printk("Task1 failed\r\n");
}
(2)设置优先级
  • 原型:int osal_kthread_set_priority(osal_task *task, unsigned int priority);
  • 参数:
    • task 是一个 osal_task * 类型的数据,即通过 osal_kthread_create 函数创建线程时得到的返回值。
    • 参数 priority 为 unsigned int 类型,用于设置线程的优先级。在 osal_task.h 文件中对 LiteOS 的线程优先级有如下的宏定义:
// 代码 2.2-4
/// Priority values.
#define OSAL_TASK_PRIORITY_ABOVE_HIGH   2
#define OSAL_TASK_PRIORITY_HIGH         3
#define OSAL_TASK_PRIORITY_BELOW_HIGH   4
#define OSAL_TASK_PRIORITY_ABOVE_MIDDLE 5
#define OSAL_TASK_PRIORITY_MIDDLE       6
#define OSAL_TASK_PRIORITY_BELOW_MIDDLE 7
#define OSAL_TASK_PRIORITY_ABOVE_LOW    8
#define OSAL_TASK_PRIORITY_LOW          10
#define OSAL_TASK_PRIORITY_BELOW_LOW    11

【说明】虽然在 SDK 注释中表明只能使用以下预定义的宏
OSAL_TASK_PRIORITY_HIGH
OSAL_TASK_PRIORITY_MIDDLE
OSAL_TASK_PRIORITY_LOW

但在实际的使用中,依然可以通过序号的方式对线程指定优先级的大小。

  • 返回值:
    • 线程创建成功返回 OSAL_SUCCESS,对应整型数据 0
    • 当线程创建失败时返回 OSAL_FAILURE,对应整型数据 -1
  • 调用:线程需要先被创建才能指定优先级,以下代码是一个调用的示例。
// 代码 2.2-5
// 在之前已经创建了 task1

// 设置线程 task1 优先级为 HIGH
osal_kthread_set_priority(task1, OSAL_TASK_PRIORITY_HIGH);
(3)挂起指定的线程
  • 原型:void osal_kthread_suspend(osal_task *task);
  • 参数:task 是一个 osal_task * 类型的数据,即通过 osal_kthread_create 函数创建线程时得到的返回值。
  • 返回值:无返回值
  • 调用:该函数用于对指定的线程执行“挂起”操作,一旦该函数被执行,则指定线程将会从线程调度队列中被删除。具体调用示例参见代码 2.2-6。
(4)将挂起的线程恢复到调度队列中
  • 原型:void osal_kthread_resume(osal_task *task);
  • 参数:task 是一个 osal_task * 类型的数据,即通过 osal_kthread_create 函数创建线程时得到的返回值。
  • 返回值:无返回值
  • 调用:该函数用于对被“挂起”的内核线程执行恢复操作。注意:这个函数应当在 osal_kthead_suspend 函数执行之后再调用。具体调用示例参见代码 2.2-6。
(5)锁定线程调度
  • 原型:void osal_kthread_lock(void);
  • 参数:无
  • 返回值:无返回值
  • 调用:该函数用于对系统线程的调度进行锁定,具体调用示例参见代码 2.2-6。
(6)解锁线程调度
  • 原型:void osal_kthread_unlock(void);
  • 参数:无
  • 返回值:无返回值
  • 调用:该函数用于回复系统线程的调度,具体调用示例参见代码 2.2-6。
(7)销毁指定的线程
  • 原型:void osal_kthread_destroy(osal_task *task, unsigned int stop_flag);
  • 参数:
    • task 是一个 osal_task * 类型的数据,即通过 osal_kthread_create 函数创建线程时得到的返回值。
    • stop_flag 是一个无符号的整型数据,用于指示当前需要销毁的线程是否存在,如果该值为 0,表示线程不存在,通常情况该值不能为 0.
  • 返回值:无返回值
  • 调用:该函数用于对将已经创建的线程所占用的空间进行回收,特别强调的是,这个函数执行时并不能对线程进行终止,它仅仅是将线程占用的空间回收了,换句话说,如果在销毁线程前没有对其进行任务的终止,那将会带来严重的后果,所以该函数慎用。具体调用示例参见代码 2.2-6。

2.2.4 内核线程功能开发实现

基于 OpenHarmony 的应用开发需要依次通过“创建工程”、“编写功能代码”、“编写和修改配置文件”、“编译”和“烧写”等步骤,本书所有示例工程均遵循上述步骤。

本示例工程演示如何创建两个任务线程,其中任务 1 正常运行时,每隔 1s 在控制台打印一行内容;任务 2 则负责处理任务 1 的调度,每隔 5s 对任务 1 进行挂起、恢复的操作。

(1)创建工程

OpenHarmony 的工程目录一般存放在 application/sample/wifi-iot/app 目录下,以下将该目录统称为“工程目录”。本章所有示例工程都建在工程目录下新建的 kernel 文件夹下, 本章称该目录为 “内核开发工程目录”。

在内核开发工程目录下新建 “kernel_01_task_demo” 文件夹,并在该文件夹中新建“kernel_01_task_demo.c”文件和“BUILD.gn”文件。

(2)编写功能代码

kernel_01_task_demo.c 文件内容如下:

// 代码 2.2-6

// 必要头文件
#include "osal_debug.h"  // 用于支持 osal_printk
#include "common_def.h"  // 常用的类型定义
#include "soc_osal.h"    // osal 内核所有头文件引用合集
#include "app_init.h"    // 用于支持 app_run 方法

// 任务 1 的线程入口函数
int Task1 (void*param)
{
    // 并不需要使用参数,告知编译器不要检查此参数是否使用
    (void)param;
    // 每 1s 输出一行内容
	while(1)
	{
		osal_printk("[1] Hello, this is Task 1.\r\n");
		osal_msleep(1000);  // 延迟 1s
	}
}

// 任务 2 的线程入口函数
int Task2 (void*param)
{
    // 从线程参数中取出所需要的值,任务 1 的线程 handler
	osal_task * task1 =(osal_task *) param;
	while(1)
    {
        osal_printk("[2] Hello, this is Task 2.\r\n");
        // 每过 5s 切换任务 1 的运行状态
        osal_msleep(5000);
        osal_kthread_suspend(task1);
        osal_printk("[2] Task 1 suspended.\r\n");
        osal_msleep(5000);
        osal_kthread_resume(task1);
        osal_printk("[2] Task 1 resumed.\r\n");
    }
}

// 系统入口函数
void TaskDemoEntry (void)
{
	osal_printk("\r\n========= Hello, Task Demo! =========\r\n");
    osal_kthread_lock();

    // 创建任务 1 线程
	osal_task * task1 = osal_kthread_create((osal_kthread_handler)Task1, NULL, "Task1", 0x1000);
	if(task1 != NULL)
    {
        osal_printk("Task1 created with handler: %p\r\n", task1);
    }
    else
    {
        osal_printk("Task1 failed\r\n");
    }

    // 创建任务 2 线程
	osal_task * task2 = osal_kthread_create((osal_kthread_handler)Task2, (void *)Task1, "Task2", 0x1000);
	if(task2!= NULL)
    {
        osal_printk("Task2 created with handler: %p\r\n", task2);
    }
    else
    {
        osal_printk("Task2 failed\r\n");
    }
    osal_kthread_unlock();
}

app_run(TaskDemoEntry);

其中 osal_msleep 用于在系统层次实现 1ms 级别的延时,由内核进行实现,在延时期间不会造成其他任务的阻塞。例如 osal_msleep(1000) 将实现一个 1s 的延时。

(3)编写配置文件

接着是编译配置 BUILD.gn 文件,内容如下:

# 代码 2.2-7

static_library("kernel_01_TaskDemo") {
    sources = [
        # 添加参与编译的 .c
        "kernel_01_task_demo.c",
    ]
    
# 在参与编译的源码文件中,引用了一批头文件,
# 这些头文件所在的路径需要填写到 include_dirs 数组中,
# 以下字符串中的 “//” 代表 SDK 的根目录。
    include_dirs = [
        # 添加头文件搜索路径
        "//commonlibrary/utils_lite/include",
        "//device/soc/hisilicon/ws63v100/sdk/kernel/osal/include/debug",
        "//device/soc/hisilicon/ws63v100/sdk/include",
        "//device/soc/hisilicon/ws63v100/sdk/kernel/osal/include",
        "//device/soc/hisilicon/ws63v100/sdk/middleware/utils/app_init"
    ]
}

【说明】基于 OpenHarmony 的 WS63 工程编译采用的是 GN+Ninja
工具链进行配置,编译。每个独立的应用工程都需要包含一个 BUILD.gn 文件。

BUILD.gn文件由三部分内容(目标、源文件、头文件路径)构成,其中:

  • static_library 中指定业务模块的编译结果,为静态库文件 libXXXX.a(其中 XXXX 表示static_library 中指定的名称,本例中 XXXX 为 kernel_01_TaskDemo),开发者根据实际情况完成填写。
  • sources 中指定静态库 .a 所依赖的 .c 文件及其路径,若路径中包含"//“则表示绝对路径(此处为代码根路径),若不包含”//"则表示相对路径。
  • include_dirs 中指定 source 所需要依赖的 .h 文件路径。
(4)修改工程配置文件

一个基于 OpenHarmony 的 WS63 工程的配置文件有三个,以下分别对其修改方法进行介绍。

1) 修改“工程目录”下的 BUILD.gn 文件,修改后的代码如下所示:

# 代码 2.2-8 

import("//build/lite/config/component/lite_component.gni")

lite_component("app") {
  features = [ 
    # "startup" 
    "kernel/kernel_01_task_demo:kernel_01_TaskDemo",
  ]
}

【说明】

  • lite_component(“app”) 编译生成的组件的名字叫 “app”。
  • features 是一个数组,其中包含的是待编译组件“app”所拥有的功能特性,换句话说就是“app”组件中都有哪些功能。如果需要多个功能,那么各个功能以数组元素的方式进行组织;如果只需要一个功能,那么将数组中无需编译的功能注释即可,注释符号是“#”。
  • kernel/kernel_01_task_demo 表示待编译的工程在“工程目录”的存放路径。
  • kernel_01_TaskDemo 是在“工程目录/kernel/kernel_01_task_demo/BUILD.gn”中所指定的 static_library 的名字。这个名字必须一致,且注意大小写敏感。

2)修改 SDK 目录/build/config/target_config/ws63/config.py 文件,'ws63-liteos-app'中添加 "kernel_01_TaskDemo",,具体添加位置如代码 2.2-9 所示:

# 代码 2.2-9

'ws63-liteos-app': {
    # 以上代码省略
    'ram_component': [
        # 这里部分组件名称省略
        'xo_trim_port',
        "mqtt",
        "coap",
        "kernel_01_TaskDemo" # 添加 kernel_01_TaskDemo 到这里
    ],
    # 以下代码省略
},

3)修改 SDK 目录/libs_url/ws63/cmake/ohos.cmake 文件,在 COMPONENT_LIST 项中添加 kernel_01_TaskDemo,具体添加位置如代码 2.2-10 所示:

# 代码 2.2-10

# 以上代码省略
elseif(${TARGET_COMMAND} MATCHES "ws63-liteos-app")
set(COMPONENT_LIST "begetutil"   "hilog_lite_static" "samgr_adapter" "bootstrap" "fsmanager_static" "hal_update_static" "hilog_static" "inithook"   "samgr_source"
        "broadcast" "hal_file_static"   "init_log"  "native_file" "udidcomm"
        "cjson_static" "hal_sys_param" "hichainsdk" "hota" "init_utils"  "param_client_lite"
        "hiview_lite_static" "hal_sysparam" "hievent_lite_static" "huks_3.0_sdk"   "samgr" "blackbox_lite" "hal_iothardware" "wifiservice"
        "hidumper_mini" "kernel_01_TaskDemo")
endif()
# 以下代码省略
(5)编译工程

在 VS Code 工具中,打开内置终端工具,会自动进入当前OpenHarmony 的源码目录下,直接运行编译命令 hb build -f ,等待编译完成即可。

【说明】在执行 hb build -f 命令后,整个工程项目会被重新编译。每次执行该命令,都会在 out
目录的制品文件目录下将之前执行该命令时生成的 build.log
文件以创建文件时的时间戳重新命名,不仅如此,如果编译出现错误,也会创建对应的 error.log
文件。如此,随着编译次数的增多,会导致该目录下的这些日志文件以及编译过程中产生的临时文件越来越多。改进的方法是将命令改为:

rm -rf out && hb set -p nearlink_dk_3863 && hb build -f

该命令是三条独立命令的联合执行写法:

  • 首先执行 rm -rf out ,用于将 out 目录整个删除。
  • 接着执行 hb set -p nearlink_dk_3863 ,用于重新配置编译生成的制品目录。
  • 最后执行 hb build -f ,对这个工程进行重新编译。
(6)烧写

参考“第 1 章 1.4 镜像文件的烧写”一节的内容完成烧写。

【说明】如果已经对 BurnTool 工具进行过配置,那么这些配置会保存下来,无需每次使用时再重新配置。

(7)通过串口调试助手查看程序运行结果

由于内核应用开发中很多功能是通过执行 osal_printk 方法,将运行过程的部分情况通过串口打印输出的,因此,本章所有示例工程的运行结构均需要通过 Windows 上可以运行的串口调试助手工具来查看。

下图是本示例工程的运行结果,从图中可以看到:

  • 首先进入系统入口函数 TaskDemoEntry 执行了打印 “========= Hello, Task Demo! =========”。
  • 接着两个 task 分别被创建,并分别返回了指向 task 的指针的值:0xa34338 和 0xa4bea8。
  • 后面 task1 和 task2 的功能相继被调用。

图 2.2-2 线程测试实例串口消息内容

2.3 LiteOS-M 内核定时器功能

2.3.1 内核定时器原理及其功能

(1)定时器简介及原理

定时器(Timer)是一种在特定时间间隔后执行特定任务的机制或设备。然而硬件定时器受硬件的限制,数量上不足以满足实际需求,为提供更多的定时器,LiteOS-M 内核提供了软件定时器功能。这扩展了定时器的数量,允许创建更多的定时业务。

在 OpenHarmony 系统中,内核定时器是基于系统 Tick 时钟中断实现的软件定时器(定时精度与系统 Tick 时钟的周期有关)。当定时器到达设定的 Tick 计数值时,会触发用户定义的回调函数。这种机制使得用户可以通过编写回调函数来处理定时任务。

软件定时器的实现主要有以下几点:

软件定时器是系统资源,在模块初始化的时候已分配了一块连续的内存,系统支持的最大定时器个数由 los_config.h 中的 LOSCFG_BASE_CORE_SWTMR_LIMIT 宏配置

图 2.3-1 LOSCFG_BASE_CORE_SWTMR_LIMIT 宏配置

  • 软件定时器使用了系统的一个队列和一个任务资源,其触发遵循队列规则,先进先出。定时短的定时器总是比定时长的靠近队列头,满足优先被触发的准则。
  • 软件定时器以 Tick 为基本计时单位,当用户创建并启动一个定时器时,LiteOS-M 内核会根据当前系统 Tick 时间及用户设置的定时间隔确定该定时器的到期 Tick 时间,并将该定时器控制结构挂入计时全局链表。
  • 当 Tick 中断到来时,在 Tick 中断处理函数中扫描软件定时器的计时全局链表,看是否有定时器超时,若有则将超时的定时器记录下来。
  • Tick 中断处理函数结束后,软件定时器任务(优先级为最高)被唤醒,在该任务中调用之前记录下来的定时器的超时回调函数。
(2)定时器功能

LiteOS-M 内核提供软件定时器功能, 有:

  • 静态裁剪:能通过宏关闭软件定时器功能
  • 软件定时器创建
  • 软件定时器启动
  • 软件定时器停止
  • 软件定时器删除
  • 软件定时器剩余 Tick 数获取

通过以上功能,开发者可以灵活地创建管理启动停止软件定时器,以满足不同的定时需求。

2.3.2 内核定时器开发的具体步骤

OpenHarmony 为 LiteOS-M 内核定时器的相关操作提供了诸多接口函数,通过调用对应的接口函数,即可实现不同的功能。⼀般的开发步骤可以总结如下:

  • 编写定时器回调函数:根据实际需求, 编写回调函数,并定义函数参数。
  • 创建内核定时器:调用内核功能接口, 创建定时器, 并进行设定类型、属性等初始化操作。
  • 视需求进行内核定时器功能的使用, 如开启、停止定时器等。

2.3.3 内核定时器接口介绍

针对 WS63 的 LiteOS-M 内核定时器的接口均在 SDK 目录/kernel/osal/include/time/osal_timer.h 文件中声明。

(1)创建内核定时器
  • 原型:int osal_timer_init(osal_timer *timer);
  • 参数:
    • 参数 timer 为 osal_time 结构体类型指针,该结构体类型定义同样在 osal_timer.h 中,具体内容如下:
// 代码 2.3-1

typedef struct {
    void *timer;
    void (*handler)(unsigned long);
    unsigned long data;    // data for handler
    unsigned int interval; // timer timing duration, unit: ms.
} osal_timer;

其中:

1)timer 成员表示定时器句柄,实际指向一个内部结构体,这个成员必须被初始化成 NULL。

2)handler 成员为定时器回调函数指针,需要指向定时器回调函数。

3)data 成员为用于传递给定时器回调的用户参数。

4)interval 成员为定时器触发延时,单位为毫秒(ms)。

建议在初始化 osal_timer 结构体时,先将结构体整体赋初值 0,再手动进行必要的修改,如设置 handler 回调函数指针,设置延时值 interval 等。

  • 返回值:
    • 创建成功, 返回 OSAL_SUCCESS,
    • 创建失败,返回 OSAL_FAILURE 。
  • 调用:以下是一个完整的创建内核定时器的示例代码:
// 代码 2.3-2

// 创建一个定时器
static osal_timer timer1 = {0};
// 定时器回调函数
void Timer1(unsigned long data)
{
    (void)data;
    osal_printk("T1: timer 1 report!\r\n");
}

timer1.handler = Timer1; // 指向定时器回调函数
timer1.interval = 2000;

// 检测定时器是否创建成功
if(OSAL_SUCCESS != osal_timer_init(&timer1))
{
    osal_printk("Failed to initialize timer 1!\r\n");
}

【说明】osal_timer_init 函数被调用后,会分配一段内存空间存放定时器对象资源,该指针被存放在 timer
成员中。当不再需要用到定时器时,需要调用 osal_timer_destroy 函数对定时器资源进行销毁。

(2)启动内核定时器
  • 原型:int osal_timer_start(osal_timer *timer);
  • 参数:
    • timer 为 osal_timer 结构体类型指针,同 osal_timer_init 函数的参数用法。
  • 返回值:
    • 创建成功,返回 OSAL_SUCCESS,
    • 创建失败,返回 OSAL_FAILURE 。
  • 调用:在编写好定时器回调函数和初始化函数后, 就可以使调用 osal_timer_start 启动定时器。
// 代码 2.3-3

// 首先创建定时器 timer1
// 启动定时器
osal_timer_start(&timer1);
(3)停止内核定时器
  • 原型:int osal_timer_stop(osal_timer *timer);
  • 参数:
    • timer 为 osal_timer 结构体类型指针,同 osal_timer_init 函数的参数用法。
  • 返回值:
    • 创建成功,返回 OSAL_SUCCESS,
    • 创建失败,返回 OSAL_FAILURE 。
  • 调用:启动了内核定时器后可通过 osal_timer_stop停止定时器
// 代码 2.3-4

// 首先创建定时器 timer1
// 启动定时器
// 停止定时器
osal_timer_stop(&timer1);
(4)销毁(释放)定时器
  • 原型:int osal_timer_destroy(osal_timer *timer);
  • 参数:
    • timer 为 osal_timer 结构体类型指针,同 osal_timer_init 函数的参数用法。
  • 返回值:
    • 创建成功,返回 OSAL_SUCCESS,
    • 创建失败,返回 OSAL_FAILURE 。
  • 调用:创建好内核定时器后可通过 osal_timer_destroy销毁(释放)定时器
// 代码 2.3-5

// 首先创建定时器 timer1
// 停止定时器
osal_timer_destroy(&timer1);

更多关于定时器的功能接口,以及和它们相关的结构体、宏定义等内容,可以从 device/soc/hisilicon/ws63v100/sdk/kernel/osal/include/time/osal_timer.h 文件中进行检索。

2.3.4 内核定时器功能开发实现

以下的工程实例中,创建了两个定时器,并分别为其设置了定时周期以及回调函数。通过完成该实例代码的编写、编译后,将生成的镜像文件烧写至 WS63 开发板,可以通过串口调试助手查看这两个定时器的运行情况。

(1)创建工程

在内核开发工程目录下新建 “kernel_02_timer_demo” 文件夹,并在该文件夹中新建“kernel_02_timer_demo.c”文件和“BUILD.gn”文件。

(2)编写功能代码

kernel_02_timer_demo.c 的代码如下:

// 代码 2.3-6

#include "osal_debug.h"
#include "common_def.h"
#include "soc_osal.h"
#include "app_init.h"

#define THREAD_STACK_SIZE   0x1000
#define THREAD1_PRIO        OSAL_TASK_PRIORITY_MIDDLE
#define TIMER1_INTERVAL     2000    // timer1 的定时间隔为 2s
#define TIMER2_INTERVAL     1000    // timer2 的定时间隔是 1s

static osal_timer timer1 = {0}, timer2 = {0}; // 初始化两个定时器结构体变量

/* Task1 为内核线程
 * 该线程的功能是对定时器 timer1 和 timer2 进行控制
 * 包括:对两个定时器的启动、停止和销毁。
*/
int Task1(void * data)
{
    (void)data;
    // 等待 2s
    osal_msleep(2000);

    // 启动两个定时器
    osal_printk("starting timers ...\r\n");
    osal_timer_start(&timer1);
    osal_timer_start(&timer2);

    // 等待 10s
    osal_msleep(10000);

    // 停止两个定时器
    osal_timer_stop(&timer1);
    osal_timer_stop(&timer2);
    osal_printk("timers stopped.\r\n");

    // 销毁释放定时器资源
    osal_timer_destroy(&timer1);
    osal_timer_destroy(&timer2);
}

/* Timer1 是 timer1 定时时间到时的回调函数,
 * 功能是打印一句话“T1: timer 1 report!”到调试串口输出显示。
*/
void Timer1(unsigned long data)
{
    (void)data;
    osal_printk("T1: timer 1 report!\r\n");
}

/* Timer2 是 timer2 定时时间到时的回调函数,
 * 功能是打印一句话“T2: timer 2 report!”到调试串口输出显示,
 * 并再次启动 timer2
*/
void Timer2(unsigned long data)
{
    (void)data;
    osal_printk("T2: timer 2 report!\r\n");
    // 再次启动定时器 2 实现周期性定时
    osal_timer_start(&timer2);
}

void kernel_02_timer_demo(void)
{
    osal_printk("\r\n========= Hello, Event Demo! =========\r\n");
    osal_printk("Enter kernel_02_timer_example()!\r\n");
    osal_kthread_lock();

    osal_task * task1 = osal_kthread_create(Task1, NULL, "Task1", THREAD_STACK_SIZE);
    if(task1 != NULL)
        osal_kthread_set_priority(task1, THREAD1_PRIO);
    else
        osal_printk("Failed to create thread 1!\r\n");

    timer1.handler = Timer1;   // 为 timer1 指定定时结束的回调函数
    timer1.interval = TIMER1_INTERVAL;  // 为 timer1 指定定时间隔
    if(OSAL_SUCCESS != osal_timer_init(&timer1))  // 创建 timer1
    {
        osal_printk("Failed to initialize timer 1!\r\n");
    }

    timer2.handler = Timer2;   // 为 timer2 指定定时结束的回调函数
    timer2.interval = TIMER2_INTERVAL;  // 为 timer2 指定定时间隔
    if(OSAL_SUCCESS != osal_timer_init(&timer2))  // 创建 timer2
    {
        osal_printk("Failed to initialize timer 2!\r\n");
    }

    osal_kthread_unlock();
}
// 程序入口
app_run(kernel_02_timer_demo);
(3)编写配置文件

接着是编译配置 BUILD.gn 文件,内容如下:

# 代码 2.3-7

static_library("kernel_02_TimerDemo") {
    sources = [
        # 添加参与编译的 .c
        "kernel_02_timer_demo.c"
    ]

    include_dirs = [
        # 添加头文件搜索路径
        "//commonlibrary/utils_lite/include",
        "//device/soc/hisilicon/ws63v100/sdk/kernel/osal/include/debug",
        "//device/soc/hisilicon/ws63v100/sdk/include",
        "//device/soc/hisilicon/ws63v100/sdk/kernel/osal/include",
        "//device/soc/hisilicon/ws63v100/sdk/middleware/utils/app_init",
        "//device/soc/hisilicon/ws63v100/sdk/kernel/osal/include/time"
    ]
}
(4)修改工程配置文件

【说明】基于 OpenHarmony 的 WS63 工程在完成应用开发后,都要对“第 2 章 2.2.4
内核线程功能开发实现(4)修改工程配置文件”中所提到的三个文件做修改,为了节省篇幅,以下在涉及本次示例工程以及后续的示例工程配置文件修改时,都假设您已经学习完毕“第
2 章 2.2”小节的内容,并已经完成了该小节的所有操作的基础上进行说明。

1) 修改“工程目录”下的 BUILD.gn 文件,在 features 数组中添加 "kernel/kernel_02_timer_demo:kernel_02_TimerDemo",

2)修改 SDK 目录/build/config/target_config/ws63/config.py 文件,'ws63-liteos-app''ram_component': []'添加 "kernel_02_TimerDemo",

3)修改 SDK 目录/libs_url/ws63/cmake/ohos.cmake 文件,在COMPONENT_LIST项中添加 "kernel_02_TimerDemo"

(5)编译工程

在 VS Code 工具中,打开内置终端工具,进入当前OpenHarmony 的源码目录下,输入命令 rm -rf out && hb set -p nearlink_dk_3863 && hb build -f ,等待编译完成。

(6)烧写

参考“第 1 章 1.4 镜像文件的烧写”一节的内容完成烧写。

【说明】如果已经对 BurnTool 工具进行过配置,那么这些配置会保存下来,无需每次使用时再重新配置。

(7)通过串口调试助手查看程序运行结果

打开串口调试助手,设置正确的 COM 编号,波特率为 115200,其他选择默认,并打开串口。

按下 WS63 开发板上的 RST 按键,观察串口输出端口返回的打印信息,可以看到定时器 time1 和 timer2 按照既定的功能,在定时结束后执行了回调函数功能。具体效果如下图所示。

图 2.3-2 定时器测试实例串⼝消息内容

2.4 OpenHarmony 内核事件功能

2.4.1 内核事件与跨线程同步

(1)内核事件概述

事件是一种高效的任务线程间通信机制,主要用于实现跨线程同步操作。事件具有以下特点:

  • 支持一对多和多对多的线程间同步,即一个线程可以等待多个事件,多个线程也可以等待多个事件。但每次写事件仅能触发一个线程从阻塞状态唤醒。
  • 具备事件读超时机制,确保线程在指定时间内未收到事件时能够正确处理。
  • 事件仅用于跨线程同步,不涉及具体数据的传输。
(2)内核事件工作机制

画板

图 2.4-1 内核事件工作机制原理图

  • 事件集合void * 类型指针的event 来表示,表示所要创建的内核事件标志。任务线程通过创建事件控制块来实现对事件的触发和等待操作,线程通过“逻辑与”或“逻辑或”与一个事件或多个事件建立关联,形成一个事件集合(事件组),事件的“逻辑或”也称为独立型同步,事件的“逻辑与”也称为关联型同步。事件控制块如下:
// 代码 2.4-1

typedef struct {
    void *event;  //表示所要创建的内核事件标志
} osal_event;
- `event` 成员表示所要创建的内核事件标志
  • 在读事件操作(获取事件)时,可以在 mode 参数(后文函数接口中会再次提及)中设置读取模式,来选择用户感兴趣的事件,读取模式如下:
    • 所有事件OSAL_WAITMODE_AND (逻辑与):任务等待所有预期事件发生,读取掩码中所有事件类型,只有读取的所有事件类型都发生了,才能读取成功。
    • 任一事件OSAL_WAITMODE_OR(逻辑或):任务会等待其预期的任何一个事件发生,读取掩码中任一事件类型,读取的事件中任意一种事件类型发生了,就可以读取成功了。
    • 清除事件OSAL_WAITMODE_CLR:事件标志在事件被读取后立即被清除
(3)事件与信号量的区别

事件能够在一定程度上代替信号量,用于任务与任务,中断与任务间的同步,但与信号量不同的是:

  • 事件的发送操作是不可累计的,而信号量的释放动作时可以累计的。
  • 事件接收任务可以等待多种事件,信号量只能识别单一同步动作,而不能同时等待多个事件的同步。
  • 各个事件可以分别发送或一起发送给事件对象。

2.4.2 内核事件开发的具体步骤

OpenHarmony 为内核事件的相关操作提供了诸多接口函数,通过调用对应的接口函数,即可实现不同的内核事件功能。⼀般的开发步骤可以总结如下:

  • 创建内核线程:编写线程入口函数、新建并初始化新建内核线程等操作。有关线程操作详见 2.2 章节
  • 创建内核事件:调用内核功能接口, 创建事件标志对象
  • 视需求通过调度和管理事件, 如设置事件标记等操作来实现跨线程同步通信。

2.4.3 内核事件接口介绍

针对 WS63 的 LiteOS-M 内核事件的接口均在 SDK 目录/kernel/osal/include/event/osal_event.h 文件中声明。

(1)创建内核事件
  • 原型: int osal_event_init(osal_event *event_obj)
  • 参数:event_obj 为 osal_event 结构体类型指针,该结构体定义见代码 2.4-1。
  • 返回值:
    • 创建成功,返回 OSAL_SUCCESS,
    • 创建失败,返回 OSAL_FAILURE 。
  • 调用:以下代码演示了如何调用 osal_event_init 进行内核事件的创建。
// 代码 2.4-2

// 创建事件标志
osal_event event1_create;
// 检测事件是否创建成功
if(osal_event_init(&event1_create) == OSAL_SUCCESS)
{
    osal_printk("Event Create  is OK!\r\n");
}
else
    osal_printk("Event failed to Create!\r\n");
(2)写入事件控制块

当完成了内核事件标志初始化后,可以通过调用 osal_event_write 函数将由传入的事件掩码指定的事件写入由 osal_event 指向的事件控制块中。

  • 原型:int osal_event_write(osal_event *event_obj, unsigned int mask);
  • 参数:
    • event_obj 为 osal_event 结构体类型指针,该结构体定义见代码 2.4-1。
    • mask 是一个无符号的整数据,表示为用户期望发生的事件的掩码
  • 返回值:
    • 创建成功,返回 OSAL_SUCCESS,
    • 创建失败,返回 OSAL_FAILURE 。
  • 调用:可如下代码所示写入事件控制块。
// 代码 2.4-3

// 写入事件控制块
osal_event_write(&event1_create, 0x00000001U);
(3)读取事件

在 OSAL 的内核机制中,设置了可以通过指定事件控制块、事件掩码、读取模式和超时信息来阻塞或调度任务以读取事件的接口。

  • 原型:int osal_event_read(osal_event *event_obj, unsigned int mask, unsigned int timeout_ms, unsigned int mode);
  • 参数:
    • event_obj 为 osal_event 结构体类型指针,该结构体定义见代码 2.4-1。
    • mask 是一个无符号的整数据,表示为用户期望发生的事件的掩码。
    • timeout_ms 是一个无符号的整型数据,百世事件读取的超时时间间隔,单位是毫秒。
    • mode 是一个无符号的整型数据,表示事件读取模式,其中:
      • OSAL_WAITMODE_AND 表示任务等待所有预期事件发生
      • OSAL_WAITMODE_OR 表示任务会等待其预期的任何一个事件发生
      • OSAL_WAITMODE_CLR 表示事件标志在事件被读取后立即被清除
  • 返回值:
    • 创建成功,返回 OSAL_SUCCESS,
    • 创建失败,返回 OSAL_FAILURE 。
  • 调用:不能在中断服务中调用该函数,不建议在软件定时器回调中使用此 API,并且在 liteos 上禁止使用事件掩码的第 25 位。
(4)清除指定任务中的事件
  • 原型:int osal_event_clear(osal_event *event_obj, unsigned int mask);
  • 参数:
    • event_obj 为 osal_event 结构体类型指针,该结构体定义见代码 2.4-1。
    • mask 是一个无符号的整数据,表示为用户期望发生的事件的掩码
  • 返回值:
    • 创建成功,返回 OSAL_SUCCESS,
    • 创建失败,返回 OSAL_FAILURE 。
  • 调用:该函数被执行后,将会清除掉指向事件的指针的值为 0 。具体调用见代码 2.4-4。
(5)销毁指定事件
  • 原型:int osal_event_destroy(osal_event *event_obj);
  • 参数:event_obj 为 osal_event 结构体类型指针,该结构体定义见代码 2.4-1。
  • 返回值:
    • 创建成功,返回 OSAL_SUCCESS,
    • 创建失败,返回 OSAL_FAILURE 。
  • 调用:该函数用于销毁一个事件,并且可能释放与该事件相关的内存。具体调用见代码 2.4-4。

2.4.4内核事件功能开发实现

以下的示例工程中演示了如何创建两个任务,任务 1 每隔 1 秒钟设置一个事件标志位,总共设置 3 个事件标志位;任务 2 阻塞等待是否接收到 3 个事件标志位,如果接收到了,打印输出。

(1)创建工程

在内核开发工程目录下新建 “kernel_03_event_demo” 文件夹,并在该文件夹中新建“kernel_03_event_demo.c”文件和“BUILD.gn”文件。

(2)编写功能代码
// 代码 2.4-4

#include "osal_debug.h"
#include "common_def.h"
#include "soc_osal.h"
#include "app_init.h"

#define THREAD_STACK_SIZE   0x1000
#define THREAD1_PRIO        OSAL_TASK_PRIORITY_HIGH
#define THREAD2_PRIO        OSAL_TASK_PRIORITY_MIDDLE

uint32_t event1_Flags = 0x00000001U;  // 事件掩码 每一位代表一个事件
uint32_t event2_Flags = 0x00000002U;  // 事件掩码 每一位代表一个事件
uint32_t event3_Flags = 0x00000004U;  // 事件掩码 每一位代表一个事件

// 创建事件标志
static osal_event event_create;
//任务1线程 用于发送事件
int Task1(void*param)
{
    // 并不需要使用参数,告知编译器不要检查此参数是否使用
    (void)param;
    while (1)
    {
        osal_printk("enter Task 1.......\r\n");
        osal_event_write(&event_create, event1_Flags);    // 设置事件标记 
        osal_printk("send eventFlag1.......\r\n");
        osal_msleep(1000); // 1秒
        osal_event_write(&event_create, event2_Flags);    // 设置事件标记 
        osal_printk("send eventFlag2.......\r\n");
        osal_msleep(1000); // 1秒
        osal_event_write(&event_create, event3_Flags);    // 设置事件标记 
        osal_printk("send eventFlag3.......\r\n");
        osal_msleep(1000); // 1秒
    }
}

// 任务2线程 用于接受事件
int Task2(void*param)
{
    // 并不需要使用参数,告知编译器不要检查此参数是否使用
    (void)param;
    uint32_t flags = 0;
    while (1)
    {
        // 永远等待事件标记触发,当接收到 event1_Flags 和 event2_Flags 
        // 和 event3_Flags时才会执行osal_printk函数
        // OSAL_WAITMODE_AND :全部事件标志位接收到    
        // OSAL_WAITMODE_OR: 任意一个事件标志位接收到
        // 当只有一个事件的时候,事件的类型选择哪个都可以
        flags =  osal_event_read(&event_create, event1_Flags | event2_Flags | event3_Flags, OSAL_EVENT_FOREVER, OSAL_WAITMODE_AND);

        osal_printk("enter Task 2.......\r\n");
        osal_printk("receive event is OK!\r\n\r\n"); 
        // 事件已经标记

        //清除事件标志 
        osal_event_clear(&event_create,event1_Flags | event2_Flags | event3_Flags);     
    }
}
//系统入口函数
static void kernel_03_event_demo(void)
{
    osal_printk("\r\n========= Hello, Event Demo! =========\r\n");
    osal_printk("Enter kernel_03_event_demo()!\r\n");
    
    osal_kthread_lock();
    
    // 检测事件是否创建成功
    if(osal_event_init(&event_create) == OSAL_SUCCESS)
    {
        osal_printk("Event Create is OK!\r\n");
    }
    else
    osal_printk("Event failed to Create!\r\n");
    

    osal_task * task1 = osal_kthread_create(Task1, NULL, "Task1", THREAD_STACK_SIZE);
    if(task1 != NULL)
        osal_kthread_set_priority(task1, THREAD1_PRIO);
    else
        osal_printk("Failed to create thread 1!\r\n");

    osal_task * task2 = osal_kthread_create(Task2, NULL, "Task2", THREAD_STACK_SIZE);
    if(task2 != NULL)
        osal_kthread_set_priority(task2, THREAD2_PRIO);
    else
        osal_printk("Failed to create thread 2!\r\n");

    osal_kthread_unlock();

}
app_run(kernel_03_event_demo);
(3)编写配置文件

接着是编译配置 BUILD.gn 文件,内容如下:

# 代码 2.4-5

static_library("kernel_03_EventDemo") {
    sources = [
        # 添加参与编译的 .c
        "kernel_03_event_demo.c"
    ]

    include_dirs = [
        # 添加头文件搜索路径
        "//commonlibrary/utils_lite/include",
        "//device/soc/hisilicon/ws63v100/sdk/kernel/osal/include/debug",
        "//device/soc/hisilicon/ws63v100/sdk/include",
        "//device/soc/hisilicon/ws63v100/sdk/kernel/osal/include",
        "//device/soc/hisilicon/ws63v100/sdk/middleware/utils/app_init",
        "//device/soc/hisilicon/ws63v100/sdk/kernel/osal/include/event"
    ]
}
(4)修改工程配置文件

1) 修改“工程目录”下的 BUILD.gn 文件,在 features 数组中添加 "kernel/kernel_03_event_demo:kernel_03_EventDemo",

2)修改 SDK 目录/build/config/target_config/ws63/config.py 文件,在'ws63-liteos-app''ram_component': []'添加 "kernel_03_EventDemo",

3)修改 SDK 目录/libs_url/ws63/cmake/ohos.cmake 文件,在 COMPONENT_LIST 项中添加 "kernel_03_EventDemo"

(5)编译工程

在 VS Code 工具中,打开内置终端工具,进入当前OpenHarmony 的源码目录下,输入命令 rm -rf out && hb set -p nearlink_dk_3863 && hb build -f ,等待编译完成。

(6)烧写

参考“第 1 章 1.4 镜像文件的烧写”一节的内容完成烧写。

(7)通过串口调试助手查看程序运行结果

图 2.4-2 内核事件示例运行效果

2.5 OpenHarmony 互斥锁功能

2.5.1 互斥锁及其运行机制

(1)互斥锁简介

互斥锁是一种特殊的二值性信号量,用于实现对共享资源的独占式处理。互斥锁的状态只有两种,开锁或闭锁。当有线程持有时,互斥锁处于闭锁状态,此线程获得该互斥锁的所有权。当释放它时,该互斥锁被开锁,线程失去该互斥锁的所有权。当一个线程持有互斥锁时,其他线程将不能再对该互斥锁进行开锁或持有。

(2)互斥锁的作用

多线程环境下会存在多个线程访问同一公共资源的场景,而有些公共资源是非共享的,需要任务进行独占式处理,此时互斥锁可被用于对共享资源的保护从而实现独占式访问。另外互斥锁可以解决信号量存在的优先级翻转问题。

(3)互斥锁的运行机制

用互斥锁处理非共享资源的同步访问时,如果有线程访问该资源,则互斥锁为加锁状态。此时其他线程如果想访问这个公共资源则会被阻塞,直到互斥锁被持有该锁的线程释放后,其他线程才能重新访问该公共资源,此时互斥锁再次上锁,如此确保同一时刻只有一个任务正在访问这个公共资源,保证了公共资源操作的完整性。

互斥锁的运行机制如下图:

图 2.5-1 互斥锁运行机制

(4)申请互斥锁的模式

申请互斥锁可分为无阻塞模式、永久阻塞模式以及定时阻塞模式共三种模式。具体详见 2.5.3 互斥锁接口介绍

2.5.2 互斥锁使用的基本步骤

OpenHarmony 为内核互斥锁的使用提供了一些接口函数,通过调用对应的接口函数,即可实现不同的功能。一般的使用互斥锁的步骤可以总结如下:

  • 创建内核互斥锁:调用内核功能接口, 创建内核互斥锁,并记录互斥锁 ID。
  • 视需求调度和管理互斥锁, 如申请、释放互斥锁等操作来实现对共享资源的独占式处理。

2.5.3 互斥锁接口介绍

针对 WS63 的 LiteOS-M 内核互斥锁的接口均在 SDK 目录/kernel/osal/include/lock/osal_mutex.h 文件中声明。

(1)创建互斥锁

可调用 OSAL 提供的 osal_mutex_init 函数创建互斥锁。

  • 原型:int osal_mutex_init(osal_mutex *mutex);
  • 参数:mutex 是 osal_mutex 结构体类型的指针,该类型的定义同样在 osal_mutex.h 文件中,具体如下:
// 代码 2.5-1

typedef struct {
    void *mutex;
} osal_mutex;
  • 返回值:
    • 创建成功,返回 OSAL_SUCCESS。
    • 创建失败,返回 OSAL_FAILURE。
  • 调用:以下代码是创建一个互斥锁的示例。
// 代码 2.5-2

// 定义互斥锁结构体变量
osal_mutex mutex;
// 创建互斥锁
if(osal_mutex_init(&mutex) == OSAL_SUCCESS)
{
    osal_printk("Create mutex is OK!\r\n");
}
(2)获取并持有互斥锁
  • 原型:int osal_mutex_lock(osal_mutex *mutex);
  • 参数:mutex 是一个 osal_mutex 结构体类型的指针,即通过 osal_mutex_init 函数创建互斥锁时得到的互斥锁 ID。
  • 返回值:
    • 执行成功,将返回 TRUE
    • 执行失败,将返回 FALSE
  • 调用:获取互斥锁和释放互斥锁需要配对使用,具体调用示例参见代码 2.5-4 。
(3)释放互斥锁
  • 原型:void osal_mutex_unlock(osal_mutex *mutex);
  • 参数:mutex 是一个 osal_mutex 结构体类型的指针,即通过 osal_mutex_init 函数创建互斥锁时得到的互斥锁 ID。
  • 返回值:无。
  • 调用:以下代码演示如何在线程中获取持有互斥锁,执行操作后再释放互斥锁 :
// 代码 2.5-3

// 定义互斥锁变量
// 创建互斥锁

// 请求互斥锁
osal_mutex_lock(&mutex);

// 操作共享数据,如读数据
// ...

// 释放互斥锁
osal_mutex_unlock(&mutex);
(4)销毁互斥锁
  • 原型:void osal_mutex_destroy(osal_mutex *mutex);
  • 参数:mutex 是一个 osal_mutex 结构体类型的指针,即通过 osal_mutex_init 函数创建互斥锁时得到的互斥锁 ID。
  • 返回值:无。
  • 调用:当任务中不再需要互斥锁时,可以调用该函数将互斥锁释放掉,回收内存空间。

2.5.4 内核互斥锁功能开发实现

以下示例演示了如何创建两个线程,线程 1 向全局数组中存入数据,此时在线程 2 中不能读数据;线程 2 读全局数组中的数据,此时在线程 1 中不能写数据,以此演示互斥锁的作用。

(1)创建工程

在内核开发工程目录下新建 “kernel_04_mutex_demo” 文件夹,并在该文件夹中新建“kernel_04_mutex_demo.c”文件和“BUILD.gn”文件。

(2)编写功能代码

kernel_04_mutex_demo.c 文件内容如下:

// 代码 2.5-4

#include "osal_debug.h"
#include "common_def.h"
#include "soc_osal.h"
#include "app_init.h"

osal_task * task1;
osal_task * task2; 
osal_mutex mutex;  // 创建互斥锁变量
uint8_t buff[20] = {0};   

// 任务1 线程写数据
int Task1(void *argument)
{
    (void)argument;
    osal_printk("enter Task 1.......\r\n");
    while (1)
    {
        // 请求互斥锁
        osal_mutex_lock(&mutex);
        // 操作共享数据 写数据
        osal_printk("write Buff Data: \r\n");

        for (uint8_t i = 0; i < sizeof(buff); i++)
        {
            buff[i] = i;
        }
        osal_printk("\r\n");
        // 释放互斥锁
        osal_mutex_unlock(&mutex);

        osal_msleep(100);
    }
}

// 任务2 线程读数据
int Task2(void *argument)
{
    (void)argument;
    osal_printk("enter Task 2.......\r\n");
    while (1)
    {
        // 请求互斥锁
        osal_mutex_lock(&mutex);
        // 操作共享数据 读数据
        osal_printk("read Buff Data: \r\n");

        for (uint8_t i = 0; i < sizeof(buff); i++)
        {
            osal_printk("%d \r\n", buff[i]);
        }
        osal_printk("\r\n");

        // 释放互斥锁
        osal_mutex_unlock(&mutex);
        osal_msleep(100);
    }
}

// 系统入口函数
static void kernel_04_mutex_demo(void)
{
    osal_printk("\r\n========= Hello, Mutex Demo! =========\r\n");
    osal_printk("Enter kernel_04_mutex_demo()!\r\n");

    osal_kthread_lock();
    // 创建互斥锁
    if(osal_mutex_init(&mutex) == OSAL_SUCCESS)
    {
        osal_printk("Create mutex is OK!\r\n");
    }

    // 创建任务1 线程
    osal_task * task1 = osal_kthread_create((osal_kthread_handler)Task1, NULL, "Task1", 0x1000);
    if (task1 != NULL)
    {
        osal_printk("Create task1 is OK!\r\n");
    }

    // 创建任务2 线程
    osal_task * task2 = osal_kthread_create((osal_kthread_handler)Task2, NULL, "Task2", 0x1000);
    if (task2 != NULL)
    {
        osal_printk("Create task2 is OK!\r\n");
    }
    osal_kthread_unlock();
}

app_run(kernel_04_mutex_demo);
(3)编写配置文件

接着是编译配置 BUILD.gn 文件,内容如下:

# 代码 2.5-5

static_library("kernel_04_MutexDemo") {
    sources = [
        "kernel_04_mutex_demo.c"
    ]

    include_dirs = [
        "//commonlibrary/utils_lite/include",
        "//device/soc/hisilicon/ws63v100/sdk/kernel/osal/include/debug",
        "//device/soc/hisilicon/ws63v100/sdk/include",
        "//device/soc/hisilicon/ws63v100/sdk/kernel/osal/include",
        "//device/soc/hisilicon/ws63v100/sdk/middleware/utils/app_init"
    ]
}
(4)修改工程配置文件

1) 修改“工程目录”下的 BUILD.gn 文件,在 features 数组中添加 "kernel/kernel_04_mutex_demo:kernel_04_MutexDemo",

2)修改 SDK 目录/build/config/target_config/ws63/config.py 文件,在'ws63-liteos-app''ram_component': []'添加 "kernel_04_MutexDemo",

3)修改 SDK 目录/libs_url/ws63/cmake/ohos.cmake 文件,在 COMPONENT_LIST 项中添加 "kernel_04_MutexDemo"

(5)编译工程

在 VS Code 工具中,打开内置终端工具,进入当前OpenHarmony 的源码目录下,输入命令 rm -rf out && hb set -p nearlink_dk_3863 && hb build -f ,等待编译完成。

(6)烧写

参考“第 1 章 1.4 镜像文件的烧写”一节的内容完成烧写。

(7)通过串口调试助手查看程序运行结果

图 2.5-2 互斥锁测试实例串口消息

2.6 OpenHarmony 的信号量

2.6.1 信号量及其运行机制

(1)信号量

信号量(Semaphore)是一种实现任务间通信的机制,可以实现任务间同步或共享资源的互斥访问。一个信号量的数据结构中,通常有一个计数值用于对有效资源数的计数,表示剩下的可被使用的共享资源数,其值的含义分以下两种情况:

  • 信号量为负值,表示该信号量当前不可获取,因此可能存在正在等待该信号量的任务
  • 信号量为正值,表示该信号量当前可被获取
(2)信号量的运行机制

当某一任务申请信号量时,若申请成功,则信号量的计数值会减一;若申请失败,则挂起在该信号量的等待任务队列上,直到有任务释放该信号量,等待任务队列中的任务就会被唤醒并开始执行。

信号量的运行机制如下图所示:

图 2.6-1 信号量运行机制

由图可知,该信号量设置的信号量最大值为四,n个线程依次申请信号量时,只有前四个线程任务申请成功并执行,线程五之后的任务都挂起在该信号量的等待任务队列上,直到线程一任务释放信号量,线程五任务被唤醒并执行,依此类推。

(3)信号量的应用场景

信号量还有一些应用场景,可以用作多种场合中,比如可以实现互斥锁同步资源技术等功能,也能方便用于任务与任务,中断与任务的同步中。

  • 互斥型信号量

互斥是信号量的一个重要使用场景。当信号量用作互斥时,信号量在创建的时候将最大值设置为1,可以实现两个任务之间的资源进行互斥。如果想要使用临界资源时,先申请信号量,使其变为0,这样其他任务就会因为无法申请到信号量而阻塞,从而保证了临界资源的安全,在使用完临界资源之后,进行释放信号量。

  • 同步信号量

信号量用做同步时,信号量在创建时初始值设置为0,任务1要申请信号量而阻塞,任务2可以释放信号量,于是任务1得以进入等待态和运行态,从而达到了两个任务间的同步。

  • 计数型信号量

信号量用做资源计数时,信号量的作用是一个特殊的计数器,可以递增或递减,但是此值不能为负值,典型的应用场景是生产者与消费者的场景中。

2.6.2 信号量使用的基本步骤

OpenHarmony 为内核信号量的使用提供了一些接口函数,通过调用对应的接口函数,即可实现不同的功能。一般的使用信号量的步骤可以总结如下:

  • 预设置信号量的参数属性
  • 新建信号量
  • 视需求进行内核信号量调度、管理

2.6.3 信号量接口介绍

针对 WS63 的 LiteOS-M 内核信号量的接口均在 SDK 目录/kernel/osal/include/semaphore/osal_semaphore.h 文件中声明。

(1)创建内核信号量
  • 原型:int osal_sem_init(osal_semaphore *sem, int val);
  • 参数:
    • sem 是一个 osal_semaphore 结构体类型指针,表示信号量,该结构体类型的定义同样在 osal_semaphore.h 中,具体如下:
// 代码 2.6-1

typedef struct {
    void *sem;
} osal_semaphore;
- val 是一个 int 类型的数据,表示初始可用信号量的数量
  • 返回值:
    • 创建成功,返回 OSAL_SUCCESS,
    • 创建失败,返回 OSAL_FAILURE 。
  • 调用:
// 代码 2.6-2

// 创建信号量
osal_semaphore Sem1;
//检测是否创建成功
if((osal_sem_init(&Sem1, 1) == 0)
{
    osal_printk("Sem1 Create  is OK!\r\n");
}
else
    osal_printk("Sem1 failed to Create!\r\n");
(2)创建二进制信号量

如果工作模式为互斥型信号量,也可调用 OSAL 提供的 osal_sem_binary_sem_init 函数, 用于创建一个特殊的二进制信号量,并配置相关参数。

  • 原型:int osal_sem_binary_sem_init(osal_semaphore *sem, int val);
  • 参数:
    • sem 是一个 osal_semaphore 结构体类型指针,表示信号量
    • val 是一个 int 类型的数据,表示初始可用信号量的数量,取值范围是 0 或 1
  • 返回值:
    • 创建成功,返回 OSAL_SUCCESS,
    • 创建失败,返回 OSAL_FAILURE 。
  • 调用:以下示例代码演示如何创建一个特殊的二进制信号量来实现互斥型信号量的功能。
// 代码 2.6-3

// 创建信号量
osal_semaphore Sem1;
//检测是否创建成功
if((osal_sem_binary_sem_init(&Sem1, 1) == 0)
{
    osal_printk("Sem1 Create  is OK!\r\n");
}
else
    osal_printk("Sem1 failed to Create!\r\n");
(3)申请信号量(阻塞式)
  • 原型:int osal_sem_down(osal_semaphore *sem);
  • 参数:sem 是一个 osal_semaphore 结构体类型指针,表示信号量。
  • 返回值:
    • 创建成功,返回 OSAL_SUCCESS,
    • 创建失败,返回 OSAL_FAILURE 。
  • 调用:参考代码 2.6-4 。
(4)申请信号量(带超时)。
  • 原型:int osal_sem_down_timeout(osal_semaphore *sem, unsigned int timeout);
  • 参数:
    • sem 是一个 osal_semaphore 结构体类型指针,表示信号量。
    • timeout 为 unsigned int 型数据, 可设置超时时间,单位为毫秒, 最大可为 OSAL_SEM_WAIT_FOREVER。
  • 返回值:
    • 创建成功,返回 OSAL_SUCCESS,
    • 创建失败,返回 OSAL_FAILURE 。
  • 调用:参考代码 2.6-4 。
(5)释放信号量。
  • 原型: void osal_sem_up(osal_semaphore *sem);
  • 参数:sem 是一个 osal_semaphore 结构体类型指针,表示信号量。
  • 返回值:无
  • 调用:参考代码 2.6-4 。
(6)销毁信号量并释放信号量资源
  • 原型: void osal_sem_destroy(osal_semaphore *sem);
  • 参数: sem 是一个 osal_semaphore 结构体类型指针,表示信号量。
  • 返回值:无
  • 调用:参考代码 2.6-4 。

2.6.4 互斥型信号量开发实现

以下示例演示了通过创建两个任务线程,其中线程 1 率先获取并持有信号量,10s 后再释放信号量。线程 2 将同时尝试获取并持有信号量,2s 后释放。以此演示内核互斥信号量的用法。

(1)创建工程

在内核开发工程目录下新建 “kernel_05_mutex_semaphore_demo” 文件夹,并在该文件夹中新建“kernel_05_mutex_semaphore_demo.c”文件和“BUILD.gn”文件。

(2)编写功能代码

kernel_05_mutex_semaphore_demo.c 文件内容如下:

// 代码 2.6-4

#include "osal_debug.h"
#include "common_def.h"
#include "soc_osal.h"
#include "app_init.h"

#define THREAD_STACK_SIZE   0x1000
#define THREAD1_PRIO        OSAL_TASK_PRIORITY_HIGH
#define THREAD2_PRIO        OSAL_TASK_PRIORITY_MIDDLE

// 创建信号量
static osal_semaphore Sem1;

void Task1(void * param)
{
    // 并不需要使用参数,告知编译器不要检查此参数是否使用
    (void)param;
    // 获取信号量(信号量 -1),并检测是否获取成功
    if(osal_sem_down(&Sem1)==OSAL_SUCCESS)
    {
        osal_printk("Task1: Lock held.\r\n");
    }
    osal_msleep(10000);  // 等待10s
    
    osal_sem_up(&Sem1);
    // 释放信号量(信号量 +1)
    osal_printk("Task1: Lock released.\r\n");
}

void Task2(void * param)
{
    // 并不需要使用参数,告知编译器不要检查此参数是否使用
    (void)param;
    // 获取信号量(信号量 -1),并检测是否获取成功
    if(osal_sem_down_timeout(&Sem1, OSAL_SEM_WAIT_FOREVER)==OSAL_SUCCESS)
    {
        osal_printk("Task2: Lock held.\r\n");
    }
    osal_msleep(2000);  // 等待2s
    // 释放信号量(信号量 +1)
    osal_sem_up(&Sem1);
    osal_printk("Task2: Lock released.\r\n");
}

static void SemaphoreDemo(void)
{
    osal_printk("\r\n========= Hello, Semaphore Demo! =========\r\n");
    osal_printk("Enter kernel_05_mutex_semaphore_example()!\r\n");
    
    osal_kthread_lock();
    
    // 检测二进制信号量是否创建成功
    if(osal_sem_binary_sem_init(&Sem1, 1) == OSAL_SUCCESS)
    {
        osal_printk("Sem1 Create  is OK!\r\n");
    }
    else
    osal_printk("Sem1 failed to Create!\r\n");

    osal_task * task1 = osal_kthread_create(Task1, NULL, "Task1", THREAD_STACK_SIZE);
    if(task1 != NULL)
        osal_kthread_set_priority(task1, THREAD1_PRIO);
    else
        osal_printk("Failed to create thread 1!\r\n");

    osal_task * task2 = osal_kthread_create(Task2, NULL, "Task2", THREAD_STACK_SIZE);
    if(task2 != NULL)
        osal_kthread_set_priority(task2, THREAD2_PRIO);
    else
        osal_printk("Failed to create thread 2!\r\n");

    osal_kthread_unlock();

}
app_run(SemaphoreDemo);
(3)编写配置文件

接着是编译配置 BUILD.gn 文件,内容如下:

# 代码 2.6-5

static_library("kernel_05_MutexSemaphoreDemo") {
    sources = [
        "kernel_05_mutex_semaphore_demo.c"
    ]

    include_dirs = [
          "//commonlibrary/utils_lite/include",
          "//device/soc/hisilicon/ws63v100/sdk/kernel/osal/include/debug",
          "//device/soc/hisilicon/ws63v100/sdk/include",
          "//device/soc/hisilicon/ws63v100/sdk/kernel/osal/include",
          "//device/soc/hisilicon/ws63v100/sdk/middleware/utils/app_init",
          "//device/soc/hisilicon/ws63v100/sdk/kernel/osal/include/semaphore"
    ]
}
(4)修改工程配置文件

1) 修改“工程目录”下的 BUILD.gn 文件,在 features 数组中添加 "kernel/kernel_05_mutex_semaphore_demo:kernel_05_MutexSemaphoreDemo",

2)修改 SDK 目录/build/config/target_config/ws63/config.py 文件,在'ws63-liteos-app''ram_component': []'添加 "kernel_05_MutexSemaphoreDemo",

3)修改 SDK 目录/libs_url/ws63/cmake/ohos.cmake 文件,在 COMPONENT_LIST 项中添加 "kernel_05_MutexSemaphoreDemo"

(5)编译工程

在 VS Code 工具中,打开内置终端工具,进入当前OpenHarmony 的源码目录下,输入命令 rm -rf out && hb set -p nearlink_dk_3863 && hb build -f ,等待编译完成。

(6)烧写

参考“第 1 章 1.4 镜像文件的烧写”一节的内容完成烧写。

(7)通过串口调试助手查看程序运行结果

图 2.6-2 互斥型信号量实例串口消息

由输出可知,在线程 1 获取并持有信号量的 10s 期间,线程 2 一直无法获取到信号量,直到线程 1 将其释放,线程 2 才成功获取。

2.6.5 同步型信号量开发实现

以下示例演示了同步型信号量的相关开发方法。

(1)创建工程

在内核开发工程目录下新建 “kernel_06_sync_semaphore_demo” 文件夹,并在该文件夹中新建“kernel_06_sync_semaphore_demo.c”文件和“BUILD.gn”文件。

(2)编写功能代码

kernel_06_sync_semaphore_demo.c 文件内容如下:

// 代码 2.6-6

#include "osal_debug.h"
#include "common_def.h"
#include "soc_osal.h"
#include "app_init.h"

#define THREAD_STACK_SIZE   0x1000
#define THREAD_PRIO         OSAL_TASK_PRIORITY_MIDDLE
#define SEM_COUNT_INITIAL   0

// 创建信号量
static osal_semaphore sem;

int Task1(void * param)
{
    // 并不需要使用参数,告知编译器不要检查此参数是否使用
    (void)param;
    // 等待 2s
    osal_msleep(2000);

    // 先输出 5 行内容
    for(int i = 0; i < 5; i ++)
    {
        osal_printk("[1] task 1 report!\r\n");
        osal_msleep(1000);
    }

    // 获取信号量(信号量 -1),这将导致线程挂起,直到另一个线程释放信号量
    osal_sem_down(&sem);

    // 再输出 5 行内容,此时线程 1 和线程 2 同步
    for(int i = 0; i < 5; i ++)
    {
        osal_printk("[1] task 1 report!\r\n");
        osal_msleep(1000);
    }
}
int Task2(void * param)
{
    // 并不需要使用参数,告知编译器不要检查此参数是否使用
    (void)param;
    // 等待 2.3s,让两个线程错开一定时间
    osal_msleep(2300);

    // 先输出 5 行内容,此时线程 1 和线程 2 并未实现同步
    for(int i = 0; i < 5; i ++)
    {
        osal_printk("[2] task 2 report!\r\n");
        osal_msleep(1000);
    }

    // 等待1秒
    osal_msleep(1000);
    // 释放信号量(信号量 +1),这会解除 Thread1 的挂起
    osal_sem_up(&sem);

    // 再输出 5 行内容,此时线程 1 和线程 2 同步
    for(int i = 0; i < 5; i ++)
    {
        osal_printk("[2] task 2 report!\r\n");
        osal_msleep(1000);
    }
}

void SemaphoreExample(void)
{
    osal_printk("\r\n========= Hello, Sync Semaphore Demo! =========\r\n");
    osal_printk("Enter kernel_06_sync_semaphore_example()!\r\n");
    osal_kthread_lock();

    osal_sem_init(&sem, SEM_COUNT_INITIAL);

    osal_task * task1 = osal_kthread_create(Task1, NULL, "Task1", THREAD_STACK_SIZE);
    if(task1 != NULL)
        osal_kthread_set_priority(task1, THREAD_PRIO);
    else
        osal_printk("Failed to create task 1!\r\n");

    osal_task * task2 = osal_kthread_create(Task2, NULL, "Task2", THREAD_STACK_SIZE);
    if(task2 != NULL)
        osal_kthread_set_priority(task2, THREAD_PRIO);
    else
        osal_printk("Failed to create task 2!\r\n");

    osal_kthread_unlock();
}

app_run(SemaphoreExample);

(3)编写配置文件

接着是编译配置 BUILD.gn 文件,内容如下:

# 代码 2.6-7

static_library("kernel_06_SyncSemaphoreDemo") {
    sources = [
        "kernel_06_sync_semaphore_demo.c"
    ]

    include_dirs = [
          "//commonlibrary/utils_lite/include",
          "//device/soc/hisilicon/ws63v100/sdk/kernel/osal/include/debug",
          "//device/soc/hisilicon/ws63v100/sdk/include",
          "//device/soc/hisilicon/ws63v100/sdk/kernel/osal/include",
          "//device/soc/hisilicon/ws63v100/sdk/middleware/utils/app_init",
          "//device/soc/hisilicon/ws63v100/sdk/kernel/osal/include/semaphore"
    ]
}
(4)修改工程配置文件

1) 修改“工程目录”下的 BUILD.gn 文件,在 features 数组中添加 "kernel/kernel_06_sync_semaphore_demo:kernel_06_SyncSemaphoreDemo",

2)修改 SDK 目录/build/config/target_config/ws63/config.py 文件,在'ws63-liteos-app''ram_component': []'添加 "kernel_06_SyncSemaphoreDemo",

3)修改 SDK 目录/libs_url/ws63/cmake/ohos.cmake 文件,在 COMPONENT_LIST 项中添加 "kernel_06_SyncSemaphoreDemo"

(5)编译工程

在 VS Code 工具中,打开内置终端工具,进入当前OpenHarmony 的源码目录下,输入命令 rm -rf out && hb set -p nearlink_dk_3863 && hb build -f ,等待编译完成。

(6)烧写

参考“第 1 章 1.4 镜像文件的烧写”一节的内容完成烧写。

(7)通过串口调试助手查看程序运行结果

图 2.6-3 同步型信号量测试实例串口消息

整个程序的功能是创建 Task1 线程,线程将等待 2s,随后每隔 1s 输出一行消息,输出 5 次后等待 Task2 进行同步。创建 Task2 线程,线程将等待 2.3s(以确保两个线程初始处于不同步状态),随后同样每隔 1s 输出一行消息,5 次后等待一段时间,然后与 Task1 进行同步。两个线程在同步之后会再次输出 5 行消息,间隔 1s,此时两个线程的消息应同时打印出来。从串口的输出信息可以看到,在同步操作执行之前,两个线程存在时间差,在同步之后,两个线程的节奏保持一致,输出的时间值完全相同。

2.6.6 计数型信号开发实现

在以下示例工程中,创建两个任务线程,用计数型信号量模拟客人用餐的座位情况。其中线程 1 模拟客人入座用餐,线程 2 模拟客人结束用餐离开。

(1)创建工程

在内核开发工程目录下新建 “kernel_07_count_semaphore_demo” 文件夹,并在该文件夹中新建“kernel_07_count_semaphore_demo.c”文件和“BUILD.gn”文件。

(2)编写功能代码

kernel_07_count_semaphore_demo.c 文件内容如下:

// 代码 2.6-8

#include "osal_debug.h"
#include "common_def.h"
#include "soc_osal.h"
#include "app_init.h"

//宏定义:定义本例程创建信号量的个数
#define SEM_COUNT_MAX   5

// 创建信号量
static osal_semaphore sem;

int Task1(void * param)
{
    // 并不需要使用参数,告知编译器不要检查此参数是否使用
    (void)param;

    osal_msleep(1000);

    //请求信号量
    osal_sem_down(&sem);

    // 请求信号量
    osal_sem_down(&sem);

    osal_printk("[person 1] and [person 2] are already in seats.\r\n");

    for(int i = 2; i < 10; i ++)
    {
          //请求信号量
        if(osal_sem_down(&sem)==OSAL_SUCCESS)
        {
            osal_printk("Task1: Lock held.\r\n");  //请求信号量
        }
    }

    osal_printk("No more guests!\r\n");
}

int Task2(void * param)
{
    // 并不需要使用参数,告知编译器不要检查此参数是否使用
    (void)param;

    osal_msleep(1000);

    int i = 0;
    int a=0;
    for(a=0;a<10;a++)
    {
        osal_msleep(1000);
        osal_printk("[person %d] left seat\r\n", ++ i);
        osal_sem_up(&sem);
        osal_msleep(10);
    }

    osal_printk("Closing!\r\n");   
}

void SemaphoreDemo(void)
{
    osal_printk("\r\n========= Hello,Count Semaphore Demo! =========\r\n");
    osal_printk("Enter kernel_07_count_semaphore_example()!\r\n");
    osal_kthread_lock();
        
    if(osal_sem_init(&sem,SEM_COUNT_MAX) != OSAL_SUCCESS)
    {
        osal_printk("Failed to create semaphore!\r\n");
        return;
    }
    // 创建线程1
    osal_task * task1 = osal_kthread_create((osal_kthread_handler)Task1, NULL, "Task1", 0x1000);
    if(task1 != NULL)
    {
        osal_printk("Task1 created with handle: %p\r\n, task1");
    }
        
    // 创建线程2
    osal_task * task2 = osal_kthread_create((osal_kthread_handler)Task2, NULL, "Task2", 0x1000);
    if(task2 != NULL)
    {
        osal_printk("Task2 created with handle: %p\r\n, task2");
    }

    osal_kthread_unlock();
}

app_run(SemaphoreDemo);
(3)编写配置文件

接着是编译配置 BUILD.gn 文件,内容如下:

# 代码 2.6-9

static_library("kernel_07_CountSemaphoreDemo") {
    sources = [
        "kernel_07_count_semaphore_demo.c"
    ]

    include_dirs = [
        "//commonlibrary/utils_lite/include",
        "//device/soc/hisilicon/ws63v100/sdk/kernel/osal/include/debug",
        "//device/soc/hisilicon/ws63v100/sdk/include",
        "//device/soc/hisilicon/ws63v100/sdk/kernel/osal/include",
        "//device/soc/hisilicon/ws63v100/sdk/middleware/utils/app_init",
        "//device/soc/hisilicon/ws63v100/sdk/kernel/osal/include/semaphore",
        "//device/soc/hisilicon/ws63v100/sdk/kernel/osal/include/time"
    ]
}
(4)修改工程配置文件

1) 修改“工程目录”下的 BUILD.gn 文件,在 features 数组中添加 "kernel/kernel_07_count_semaphore_demo:kernel_07_CountSemaphoreDemo",

2)修改 SDK 目录/build/config/target_config/ws63/config.py 文件,在'ws63-liteos-app''ram_component': []'添加 "kernel_07_CountSemaphoreDemo"

3)修改 SDK 目录/libs_url/ws63/cmake/ohos.cmake 文件,在 COMPONENT_LIST 项中添加 "kernel_07_CountSemaphoreDemo"

(5)编译工程

在 VS Code 工具中,打开内置终端工具,进入当前OpenHarmony 的源码目录下,输入命令 rm -rf out && hb set -p nearlink_dk_3863 && hb build -f ,等待编译完成。

(6)烧写

参考“第 1 章 1.4 镜像文件的烧写”一节的内容完成烧写。

(7)通过串口调试助手查看程序运行结果

图 2.6-4 计数型信号量实例串口消息

可以看到,当目前空余位置(信号量最大计数 - 当前计数)不为 0 时,新来的客人可直接入座(获取并持有信号量),当同时有 5 个客人入座时,剩下的客人需要等待前面的客人就餐完毕(释放信号量)。

2.7 OpenHarmony 的消息队列

2.7.1 内核消息队列及其处理机制

(1)消息队列

**消息队列(message queue)**又称队列,是一种常用于任务间通信的数据结构。队列接收来自任务或中断的不固定长度消息,并根据不同的接口确定传递的消息是否存放在队列空间中。

任务能够从队列里面读取消息,当队列中的消息为空时,挂起读取任务;当队列中有新消息时,挂起的读取任务被唤醒并处理新消息。任务也能够往队列里写入消息,当队列已经写满消息时,挂起写入任务;当队列中有空闲消息节点时,挂起的写入任务被唤醒并写入消息。

(2)阻塞模式

可以通过调整读队列和写队列的超时时间来调整读写接口的阻塞模式,如果将读队列和写队列的超时时间设置为 0,就不会挂起任务,接口会直接返回,这就是非阻塞模式。反之,如果将都队列和写队列的超时时间设置为大于 0 的时间,就会以阻塞模式运行。

(3)处理机制

消息队列提供了异步处理机制,允许将一个消息放入队列,但不立即处理。同时队列还有缓冲消息的作用,可以使用队列实现任务异步通信,队列具有如下特性:

  • 消息以先进先出的方式排队,支持异步读写。
  • 读队列和写队列都支持超时机制。
  • 每读一条消息,就会将该消息节点设置为空闲。
  • 发送的消息类型由通信双方约定,可以允许不同长度(不超过列队的消息节点大小)的消息。
  • 一个任务能够从任意一个消息列队接受和发送消息。
  • 多个队列能够从同一个消息队列接受和发送消息。
  • 创建对联时所需的队列空间,接口内系统自行动态申请内存。
(4)运行机制

图 2.7-1 消息队列运行机制

2.7.2 消息队列使用的基本步骤

OpenHarmony 为消息队列的相关操作提供了诸多接口函数,通过调用对应的接口函数,即可实现队列消息的收发功能。一般的使用步骤可以总结如下:

  • 配置消息队列的参数属性。
  • 创建消息队列:调用内核功能接口, 创建消息队列。
  • 视需求进行内核消息队列的使用,如发送消息、接收消息。

2.7.3 内核消息队列接口介绍

针对 WS63 的 LiteOS-M 内核消息队列接口均在 SDK 目录/kernel/osal/include/msgqueue/osal_msgqueue.h 文件中声明。

(1)设置消息队列的参数属性

osMessageQueueAttr_t 结构体类型用于描述消息队列的属性,如名称、属性位、控制块的内存等。

//代码 2.7-1

/// Attributes structure for message queue.
typedef struct {
  const char                   *name;   ///< name of the message queue
  uint32_t                 attr_bits;   ///< attribute bits
  void                      *cb_mem;    ///< memory for control block
  uint32_t                   cb_size;   ///< size of provided memory for control block
  void                      *mq_mem;    ///< memory for data storage
  uint32_t                   mq_size;   ///< size of provided memory for data storage 
} osMessageQueueAttr_t;
(2)创建消息队列

可以调用 OSAL 提供的 osal_msg_queue_create 函数创建消息队列。

  • 原型:
// 代码 2.7-2 

int osal_msg_queue_create(const char *name, 
                           unsigned short queue_len, 
                           unsigned long *queue_id, 
                           unsigned int flags,
                           unsigned short max_msgsize);
  • 参数:
    • name 是一个字符串常量,表示显示的消息队列名称。
    • queue_line 是一个无符号短整型数据,表示消息队列的长度大小。
    • queue_id 是一个无符号长整型指针,表示消息队列句柄。
    • flag 是无符号整型数据,表示消息队列的模式。
    • max_msgsize 是无符号短整型数据,表示消息队列的节点大小。
  • 返回值:
    • 创建成功,返回OSAL_SUCCESS,
    • 创建失败,返回OSAL_FAILURE。
  • 调用:具体参见代码 2.7-5 。
(3)向消息队列写入数据

在消息队列被创建后,就可以向其中写入消息来实现任务间的消息共享。

  • 原型:
int osal_msg_queue_write_copy(unsigned long queue_id, 
                              void *buffer_addr, 
                              unsigned int buffer_size,
                              unsigned int timeout);
  • 参数:
    • queue_id 是无符号长整型数据,是只通过 osal_msg_queue_create 所创建的消息队列 id。
    • buffer_addr 是 void 类型的指针,在使用时可以指向要写入消息的地址
    • buffer_size 是无符号整型数据,表示要写入消息的大小
    • timeout 是无符号整型数据,表示消息写入时间的限定
  • 返回值:
    • 执行成功,返回OSAL_SUCCESS,
    • 执行失败,返回OSAL_FAILURE。

调用:具体参见代码 2.7-5 。

(4)从消息队列中读出消息
  • 原型:
// 代码 2.7-4

int osal_msg_queue_read_copy(unsigned long queue_id, 
                             void *buffer_addr, 
                             unsigned int *buffer_size,
                             unsigned int timeout);
  • 参数:
    • queue_id 是无符号长整型数据,是只通过 osal_msg_queue_create 所创建的消息队列 id。
    • buffer_addr 是 void 类型的指针,在使用时可以指向要读出后消息的存放地址。
    • buffer_size 是无符号整型数据,表示待读入消息的长度。
    • timeout 是无符号整型数据,表示消息读入时间的限定。
  • 返回值:
    • 执行成功,返回OSAL_SUCCESS,
    • 执行失败,返回OSAL_FAILURE。

调用:具体参见代码 2.7-5 。

2.9.4 消息队列功能开发实现

以下示例中创建两个任务线程,其中任务1用于发送消息,并在控制台打印发送状态,任务2用于接收消息队列,并在控制台打印接收到的消息。

(1)创建工程

在内核开发工程目录下新建 “kernel_08_message_queuedemo” 文件夹,并在该文件夹中新建“kernel08_message_queue_demo.c”文件和“BUILD.gn”文件。

(2)编写功能代码

kernel_08_message_queue_demo.c 文件内容如下:

// 代码 2.7-5

#include "osal_debug.h"
#include "common_def.h"
#include "soc_osal.h"
#include "app_init.h"

#define MSG_QUEUE_SIZE 18 // 每数据最大18字节
#define MSG_MAX_LEN 18    // 可存储18条数据

static unsigned long g_msg_queue;
uint8_t abuf[] = "test is message x";

// 任务1 线程 发送消息
void Task1(void *argument)
{
    (void)argument;
    uint32_t i = 0;
    uint32_t uwlen = sizeof(abuf);
    while (i < 5)
    {
        osal_printk("\r\nenter Task 1.......\r\n");
        abuf[uwlen - 2] = '0' + i;
        i ++;
        if(OSAL_SUCCESS == osal_msg_queue_write_copy(g_msg_queue, abuf, sizeof(abuf), OSAL_WAIT_FOREVER))
        {
            osal_printk("osal_msg_queue_write_copy is ok.\r\n");
        }
        osal_msleep(100);
    }
}

// 任务 2 线程 接收消息
void Task2(void *argument)
{
    (void)argument;
    uint8_t msg[2024];
    while (1)
    {
        uint32_t msg_rev_size = 2024;
        osal_printk("\r\nenter Task 2.......\r\n");
        if(OSAL_SUCCESS != osal_msg_queue_read_copy(g_msg_queue, msg, &msg_rev_size, OSAL_WAIT_FOREVER))
        {
            osal_printk("osal_msg_queue_read_copy failed.\r\n");
            break;
        }
        osal_printk("osal_msg_queue_read_copy is ok.\r\n");
        osal_printk("recv message:%s\r\n", (char *)msg);
        osal_msleep(100);
    }
}

// 系统入口函数
static void kernel_08_message_queue_demo(void)
{
    osal_printk("\r\n========= Hello, Message Queue Demo! =========\r\n");
    osal_printk("[demo] Enter kernel_08_message_queue_demo()!\r\n");
    osal_kthread_lock();

    // 创建消息队列
    if(OSAL_SUCCESS == osal_msg_queue_create(NULL,MSG_QUEUE_SIZE, &g_msg_queue, 0, MSG_MAX_LEN))
    {
        osal_printk("Create MsgQueue is OK!\r\n");
    }

    // 创建任务1 线程
    osal_task * task1 = osal_kthread_create((osal_kthread_handler)Task1, NULL, "Task1", 0x1000);
    if (task1 != NULL)
    {
        osal_printk("Create task1 is OK!\r\n");
    }
   
    // 创建任务2 线程
    osal_task * task2 = osal_kthread_create((osal_kthread_handler)Task2, NULL, "Task2", 0x1000);
    if (task2 != NULL)
    {
         osal_printk("Create task2 is OK!\r\n");
    }
    osal_kthread_unlock();
}
app_run(kernel_08_message_queue_demo);
(3)编写配置文件

接着是编译配置 BUILD.gn 文件,内容如下:

# 代码 2.7-6

static_library("kernel_08_MessageQueueDemo") {
    sources = [
        # 添加参与编译的 .c
        "kernel_08_message_queue_demo.c"
    ]

    include_dirs = [
        # 添加头文件搜索路径
        "//commonlibrary/utils_lite/include",
        "//device/soc/hisilicon/ws63v100/sdk/kernel/osal/include/debug",
        "//device/soc/hisilicon/ws63v100/sdk/include",
        "//device/soc/hisilicon/ws63v100/sdk/kernel/osal/include",
        "//device/soc/hisilicon/ws63v100/sdk/middleware/utils/app_init"
    ]
}
(4)修改工程配置文件

1) 修改“工程目录”下的 BUILD.gn 文件,在 features 数组中添加 "kernel/kernel_08_message_queue_demo:kernel_08_MessageQueueDemo",

2)修改 SDK 目录/build/config/target_config/ws63/config.py 文件,在'ws63-liteos-app''ram_component': []'添加 "kernel_08_MessageQueueDemo",

3)修改 SDK 目录/libs_url/ws63/cmake/ohos.cmake 文件,在 COMPONENT_LIST 项中添加 "kernel_08_MessageQueueDemo"

(5)编译工程

在 VS Code 工具中,打开内置终端工具,进入当前OpenHarmony 的源码目录下,输入命令 rm -rf out && hb set -p nearlink_dk_3863 && hb build -f ,等待编译完成。

(6)烧写

参考“第 1 章 1.4 镜像文件的烧写”一节的内容完成烧写。

(7)通过串口调试助手查看程序运行结果

图 2.7-2 消息队列测试实例串口消息内容


网站公告

今日签到

点亮在社区的每一天
去签到