前言
本篇具体介绍OpenHarmony在智能开发套件Hi3861上的内核编程学习。
编程入门[Hello,OpenHarmony]
在正式开始之前,对于刚接触OpenHarmony的伙伴们,面对大篇幅的源码可能无从下手,不知道怎么去编码写程序,下面用一个简单的例子带伙伴们入门。
任务
编写程序,让开发板在串口调试工具中输出”Hello,OpenHarmony“。
操作
在源码的根目录中有名为”applications“的文件,他存放着应用程序样例,下面是他的目录结构:
我们要编写的程序样例就在源码根目录下的:applications/sample/wifi-iot/app/。
下面将具体演示如何编写程序样例。
- 新建样例目录
applications/sample/wifi-iot/app/hello_demo - 新建源文件和gn文件
applications/sample/wifi-iot/app/hello_demo/hello.c
applications/sample/wifi-iot/app/hello_demo/BUILD.gn
- 编写源文件
hello.c:
#include <stdio.h>
#include "ohos_init.h"
void hello(void){
printf("Hello,OpenHarmony!");
}
SYS_RUN(hello);
第一次操作的伙伴们可能会在引入”ohos_init.h“库时报错,面对这个问题我们只需要修改我们的include path即可,一般我们直接在目录下的 .vscode/c_cpp_properties.json文件中直接修改includePath
笔者的代码版本是OpenHarmony3.2Release版,不同版本的源码可能库所存放的路径不同,那么怎么去找到对应的库呢,对于不熟悉源码结构的伙伴们学习起来很不友好。
对于在纯Windows环境开发的伙伴们,笔者推荐使用everything这款工具,它可以快速查找主机中的文件,比在资源管理器的搜索快上不少。
everything似乎不能找到我WSL中的Ubuntu中的文件,因此对于Windows + Linux环境下的伙伴们,这款工具又不那么适用。那就可以根据Linux的查询指令来定位文件所在目录,下面提供查询案例防止有不熟悉Linux的伙伴们。我们使用locate指令来查找文件。
首先安装locate。
sudo apt install mlocate
更新mlocate.db。
sudo updatedb
查询文件目录。
locate ohos_init.h
找到我们源码根目录下 include路径下的ohos_init.h文件。
- 编写gn文件。
static_library("sayHello"){
sources = [
"hello.c"
]
include_dirs = [
"//commonlibrary/utils_lite/include"
]
}
static_library表示我们编写的静态模块,名为"sayHello", sources表示我们要编译的源码,include_dirs表示我们引入的库,这里的双斜杠就代表我们的源码根目录,”/commonlibrary/utils_lite/include“就是我们ohos_init.h的所在目录。
- 编写app下的gn文件。
在app的目录下也有一个gn文件,我们只需要去修改他即可。
这表示我们的程序将会执行hello_demo样例中的sayHello模块。
- 编译,烧录,串口调试。
这一步就属于基础操作了,不做过多赘述,不会的伙伴们可以看我之前发布的[环境搭建篇],里面也详细介绍了操作流程。
- 观察控制台的输出。
至此编码完成了编码入门,下面就具体介绍OpenHarmony的内核编程。
内核
内核介绍
什么是内核?或者说内核在一个操作系统中起到一个什么样的作用?相信初次接触这个词的伙伴们也会有同样的疑问。不过不用担心,笔者会尽可能地通俗地介绍内核的相关知识,以便大家能够更好地去体会内核编程。
我们先来看一张图,这是OpenHarmony官网发布的技术架构图。
我们可以看到最底层叫做内核层,有Linux,LiteOS等。内核在整个架构,或者操作系统中起到一个核心作用,他负责管理计算机系统内的资源和硬件设备,提供给顶层的应用层一个统一规范的接口,从而使得整个系统能够完成应用与硬件的交互。
具体点来说,内核可以做以下相关的工作:
- 进程管理
- 内存管理
- 文件资源管理
- 网络通信管理
- 设备驱动管理
当然不局限于这些,这里只是给出具体的例子供伙伴们理解,如果实在难以理解,那么笔者再举一个例子,进程。可能你没听过进程,但你一定打开过任务管理器。
这些都是进程,一个进程又由多个线程组成。那么CPU,内存,硬盘,网络这些硬件层面资源是怎么合理分配到我们软件的各个进程中呢?这就是内核帮助我们完成的事情,我们并不关心我们设备上的应用在哪里执行,如何分配资源,内核会完成这些事情。我们日常与软件交互,而内核会帮助我们完成软件和硬件的交互。
OpenHarmony内核
明白了什么是内核后,我们来看看OpenHarmony的内核是怎么样设计的吧。
OpenHarmony采用的是多内核设计 有基于Linux内核的标准系统,有基于LiteOS-A的小型系统,也有基于LiteOS-M的轻量系统。他们分别适配不同的设备,比如说智能手表就是轻量级别的,智能汽车就是标准级别的等等。本篇并不介绍标准系统和小型系统,轻量系统更加适合初学者。
LiteOS-M内核
下面是一张LiteOS-M的架构图。
下面重点介绍KAL抽象层 和 基础内核的操作。
KAL抽象层
相信大家还是会有疑惑,什么是KAL抽象层?
Kernel Abstraction Layer。
在刚刚的内核中我们提到了,内核主要完成的是软件与硬件的交互,他会给应用层提供统一的规范接口,而KAL抽象层正是内核对应用层提供的接口集合。应用程序可以通过KAL抽象层完成对硬件的控制交互。
抽象层是因为他隐藏了与硬件接口具体的交互逻辑,开发人员只需要关心如何操作硬件,而无需关心硬件底层的细节,大大提高了可移植性和维护性。
以笔者的角度去看,KAL简单来说就是一堆接口,帮助你去操控硬件。CMSIS与POSIX就是具有统一规范的一些接口。通过他们我们就可以去控制一些基础的内核,线程,软件定时器,互斥锁,信号量等等。概念就先简单介绍这么多,感兴趣的伙伴们可以上官网查看更多的关于OpenHarmony内核的信息。下面笔者会带着大家编码操作,从实际去体会内核编程。
内核编程
线程管理
在管理线程前,我们需要了解线程,线程是调度的基本单位,具有独立的栈空间和寄存器上下文,相比与进程,他是轻量的。举一个实际的例子,动物园卖票。
对于动物园卖票这件事本身而言是一个进程,而每一个买票的人可以看作一个线程,在多个售票口处,我们并发执行,并行计算,共同消费动物园的门票,像享受共同的内存资源空间一样。为什么要线程管理呢?你我都希望买到票,但是票有限,我们都不希望看到售票厅一篇混乱,因此对线程进行管理是非常重要的一件事情。
任务
创建一个线程,每间隔0.1秒,输出“Hello,OpenHarmony”,1秒后终止线程。
操作
回忆第一个hello.c的例子。
我们要编写的程序样例就在源码根目录下的:applications/sample/wifi-iot/app/。
下面将具体演示如何编写程序样例。
- 新建样例目录
applications/sample/wifi-iot/app/thread_demo。 - 新建源文件和gn文件
applications/sample/wifi-iot/app/thread_demo/singleThread.c
applications/sample/wifi-iot/app/thread_demo/BUILD.gn
- 编写源码
注意:我们需要使用到cmsis_os2.h这个库,请伙伴们按照笔者介绍的方法把includePath修改好。
问题一:怎么创建线程?
typedef struct {
/** Thread name */
const char *name;
/** Thread attribute bits */
uint32_t attr_bits;
/** Memory for the thread control block */
void *cb_mem;
/** Size of the memory for the thread control block */
uint32_t cb_size;
/** Memory for the thread stack */
void *stack_mem;
/** Size of the thread stack */
uint32_t stack_size;
/** Thread priority */
osPriority_t priority;
/** TrustZone module of the thread */
TZ_ModuleId_t tz_module;
/** Reserved */
uint32_t reserved;
} osThreadAttr_t;
这是线程的结构体,它具有以下属性:
- name:线程的名称。
- attr_bits:线程属性位。
- cb_mem:线程控制块的内存地址。
- cb_size:线程控制块的内存大小。
- stack_mem:线程栈的内存地址。
- stack_size:线程栈的大小。
- priority:线程的优先级。
- tz_module:线程所属的TrustZone模块。
- reserved:保留字段。
问题二:怎么把线程启动起来呢?
osThreadId_t osThreadNew (osThreadFunc_t func, void *argument, const osThreadAttr_t *attr);
这是创建线程的接口函数,他有三个参数,一个返回值,我们来逐个解析。
func: 是线程的回调函数,你创建的这个线程会执行这段函数的内容。
arguments:线程回调函数的参数。
attr:线程的属性,也就是我们之前创建的线程
返回值:线程的id 如果id不为空则说明成功。
问题三:怎么终止线程呢?
osStatus_t osThreadTerminate (osThreadId_t thread_id);
显然我们只要传入线程的id就会让该线程终止,返回值是一个状态码,下面给出全部的状态码。
typedef enum {
/** Operation completed successfully */
osOK = 0,
/** Unspecified error */
osError = -1,
/** Timeout */
osErrorTimeout = -2,
/** Resource error */
osErrorResource = -3,
/** Incorrect parameter */
osErrorParameter = -4,
/** Insufficient memory */
osErrorNoMemory = -5,
/** Service interruption */
osErrorISR = -6,
/** Reserved. It is used to prevent the compiler from optimizing enumerations. */
osStatusReserved = 0x7FFFFFFF
} osStatus_t;
回调函数怎么写?当然是结合我们的任务,每间隔0.1秒,输出“Hello,OpenHarmony”,1秒后终止。讲到这里,代码的整体逻辑是不是就清晰了很多,直接上完整代码。
#include <stdio.h>
#include "ohos_init.h"
// CMSIS
#include "cmsis_os2.h"
// POSIX
#include <unistd.h>
// 线程回调函数
void printThread(void *args){
(void)args;
while(1){
printf("Hello,OpenHarmony!\r\n");
// 休眠0.1秒
osDelay(10);
}
}
void threadTest(void){
// 创建线程
osThreadAttr_t attr;
attr.name = "mainThread";
// 线程
attr.cb_mem = NULL;
attr.cb_size = 0U;
attr.stack_mem = NULL;
attr.stack_size = 1024;
attr.priority = osPriorityNormal;
// 将线程启动
osThreadId_t tid = osThreadNew((osThreadFunc_t)printThread, NULL, &attr);
if(tid == NULL){
printf("[Thread Test] Failed to create printThread!\r\n");
}
// 休眠5秒
osDelay(500);
// 终止线程
osStatus_t status = osThreadTerminate(tid);
printf("[Thread Test] printThread stop, status = %d.\r\n", status);
}
APP_FEATURE_INIT(threadTest);
- 编写gn文件。
static_library("thread_demo"){
sources = [
"singleThread.c"
]
include_dirs = [
"//commonlibrary/utils_lite/include",
"//device/soc/hisilicon/hi3861v100/hi3861_adapter/kal/cmsis"
]
}
- 编写app下的gn文件。
注意的是,这次的写法与上次不同,是因为笔者的样例文件名和静态模块的名字是一样的就可以简写。
执行效果
多线程的封装
在处理业务的时候,我们一般是多线程的背景,下面笔者将创建线程函数封装起来,方便大家创建多线程。
osThreadId_t newThread(char *name, osThreadFunc_t func, void *arg){
// 定义线程和属性
osThreadAttr_t attr = {
name, 0, NULL, 0, NULL, 1024, osPriorityNormal, 0, 0
};
// 创建线程
osThreadId_t tid = osThreadNew(func, arg, &attr);
if(tid == NULL){
printf("[newThread] osThreadNew(%s) failed.\r\n", name);
}
return tid;
}
线程部分先体会到这里,想要探索更过线程相关的API,笔者这里提供了API网站,供大家参考学习。
软件定时器
下面我们介绍软件定时器,老样子我们先来介绍以下软件定时器。软件定时器是一种在软件层面上实现的计时器机制,用于在特定的时间间隔内执行特定的任务或触发特定的事件。它不依赖于硬件定时器,而是通过软件编程的方式实现。举一个例子,手机应用。
当你使用手机上的某个应用时,你可能会注意到,如果你在一段时间内没有进行任何操作,应用程序会自动断开连接并要求你重新登录。这是为了保护你的账号安全并释放服务器资源。类似的设定都是有软件定时器实现的,下面进行实际操作,让大家体会一下软件定时器。
任务
创建一个软件定时器,用来模拟上述手机应用的例子。为了方便理解,假设从此刻开始,我们不对手机做任何操作,也就是说,我们的回调函数只需要单纯的计算应用不被操作的时常即可。
操作
- 新建样例目录
applications/sample/wifi-iot/app/thread_demo。 - 新建源文件和gn文件
applications/sample/wifi-iot/app/thread_demo/singleThread.c。
applications/sample/wifi-iot/app/thread_demo/BUILD.gn。 - 编写源码
创建软件定时器。
osTimerId_t osTimerNew (osTimerFunc_t func, osTimerType_t type, void *argument, const osTimerAttr_t *attr);
- func: 软件定时器的回调函数。
- type:软件定时器的种类。
- argument:软件定时器回调函数的参数。
- attr:软件定时器的属性。
返回值:返回软件定时器的id, id为空则说明软件定时器失败。
typedef enum {
/** One-shot timer */
osTimerOnce = 0,
/** Repeating timer */
osTimerPeriodic = 1
} osTimerType_t;
软件定时器的种类有两个,分为一次性定时器和周期性定时器,一次性在执行完回调函数后就会停止计数,而周期性定时器会重复触发,每次触发重新计时。根据不同的需求我们可以选择使用不同的软件定时器。
启动软件定时器。
osStatus_t osTimerStart (osTimerId_t timer_id, uint32_t ticks);
- timer_id:软件定时器的参数,指定要启动哪个软件定时器。
- ticks:等待多少个ticks执行回调函数,在Hi3861中 100个ticks为1秒。
- 返回值:软件定时器的状态码,在线程部分已经展示给大家了全部的状态码。
停止定时器。
osStatus_t osTimerStop (osTimerId_t timer_id);
这个函数很简单,只需要传软件定时器的id,即可停止软件计时器,并且返回他的状态码。
删除定时器。
osStatus_t osTimerDelete (osTimerId_t timer_id);
删除和停止类似,就不多说明了。
下面是源代码。
#include <stdio.h>
#include "ohos_init.h"
// CMSIS
#include "cmsis_os2.h"
// POSIX
#include <unistd.h>
// 为操作软件的时间
static int times = 0;
// 软件定时器回调函数
void timerFunction(void){
times++;
printf("[Timer Test] Timer is Running, times = %d.\r\n", times);
}
// 主函数
void timerMain(void){
// 创建软件定时器
osTimerId_t tid = osTimerNew(timerFunction, osTimerPeriodic, NULL, NULL);
if(tid == NULL){
printf("[Timer Test] Failed to create a timer!\r\n");
return;
} else {
printf("[Timer Test] Create a timer success!\r\n");
}
// 启动软件定时器,每1秒执行一次回调函数
osStatus_t status = osTimerStart(tid, 100);
// 当超过三个周期位操作软件时,关闭软件
while(times <= 3){
osDelay(100);
}
// 停止软件定时器
status = osTimerStop(tid);
// 删除软件定时器
status = osTimerDelete(tid);
printf("[Timer Test] Time Out!\r\n");
}
void TimerTest(void){
// 创建测试线程
osThreadAttr_t attr;
attr.name = "timerMain";
attr.attr_bits = 0U;
attr.cb_mem = NULL;
attr.cb_size = 0U;
attr.stack_mem = NULL;
attr.stack_size = 0U;
attr.priority = osPriorityNormal;
// 启动测试线程
osThreadId_t tid = osThreadNew((osThreadFunc_t)timerMain, NULL, &attr);
if(tid == NULL){
printf("[Timer Test] Failed to created timerMain!\r\n");
}
}
APP_FEATURE_INIT(TimerTest);
- 编写gn文件。
static_library("timer_demo"){
sources = [
"timer.c"
]
include_dirs = [
"//commonlibrary/utils_lite/include",
"//device/soc/hisilicon/hi3861v100/hi3861_adapter/kal/cmsis"
]
}
- 编写app下的gn文件。
执行效果
软件定时器的API相对较少,这里还是提供所有的软件定时器API。
结束语
本篇主要介绍了一些基础内核编程相关的内容,希望能够帮助到学习OpenHarmony的伙伴们,考虑到篇幅问题,剩余的基础内核编程将在OpenHarmony智能开发套件[内核编程·上]中介绍。