本篇具体介绍OpenHarmony在智能开发套件Hi3861上的内核编程学习。
在正式开始之前,对于刚接触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"voidhello(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采用的是多内核设计 有基于Linux内核的标准系统,有基于LiteOS-A的小型系统,也有基于LiteOS-M的轻量系统。他们分别适配不同的设备,比如说智能手表就是轻量级别的,智能汽车就是标准级别的等等。本篇并不介绍标准系统和小型系统,轻量系统更加适合初学者。
下面是一张LiteOS-M的架构图。
下面重点介绍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_tosThreadNew(osThreadFunc_tfunc,void*argument,constosThreadAttr_t*attr);
这是创建线程的接口函数,他有三个参数,一个返回值,我们来逐个解析。
func: 是线程的回调函数,你创建的这个线程会执行这段函数的内容。
arguments:线程回调函数的参数。
attr:线程的属性,也就是我们之前创建的线程
返回值:线程的id 如果id不为空则说明成功。
问题三:怎么终止线程呢?
osStatus_tosThreadTerminate(osThreadId_tthread_id);
显然我们只要传入线程的id就会让该线程终止,返回值是一个状态码,下面给出全部的状态码。
typedefenum{/** 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>// 线程回调函数voidprintThread(void*args){(void)args;while(1){printf("Hello,OpenHarmony!\r\n");// 休眠0.1秒osDelay(10);}}voidthreadTest(void){// 创建线程osThreadAttr_tattr;attr.name="mainThread";// 线程attr.cb_mem=NULL;attr.cb_size=0U;attr.stack_mem=NULL;attr.stack_size=1024;attr.priority=osPriorityNormal;// 将线程启动osThreadId_ttid=osThreadNew((osThreadFunc_t)printThread,NULL,&attr);if(tid==NULL){printf("[Thread Test] Failed to create printThread!\r\n");}// 休眠5秒osDelay(500);// 终止线程osStatus_tstatus=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_tnewThread(char*name,osThreadFunc_tfunc,void*arg){// 定义线程和属性osThreadAttr_tattr={name,0,NULL,0,NULL,1024,osPriorityNormal,0,0};// 创建线程osThreadId_ttid=osThreadNew(func,arg,&attr);if(tid==NULL){printf("[newThread] osThreadNew(%s) failed.\r\n",name);}returntid;}
线程部分先体会到这里,想要探索更过线程相关的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_tosTimerNew(osTimerFunc_tfunc,osTimerType_ttype,void*argument,constosTimerAttr_t*attr);
func: 软件定时器的回调函数。
type:软件定时器的种类。
argument:软件定时器回调函数的参数。
attr:软件定时器的属性。
返回值:返回软件定时器的id, id为空则说明软件定时器失败。
typedefenum{/** One-shot timer */osTimerOnce=0,/** Repeating timer */osTimerPeriodic=1}osTimerType_t;
软件定时器的种类有两个,分为一次性定时器和周期性定时器,一次性在执行完回调函数后就会停止计数,而周期性定时器会重复触发,每次触发重新计时。根据不同的需求我们可以选择使用不同的软件定时器。
启动软件定时器。
osStatus_tosTimerStart(osTimerId_ttimer_id,uint32_tticks);
timer_id:软件定时器的参数,指定要启动哪个软件定时器。
ticks:等待多少个ticks执行回调函数,在Hi3861中 100个ticks为1秒。
返回值:软件定时器的状态码,在线程部分已经展示给大家了全部的状态码。
停止定时器。
osStatus_tosTimerStop(osTimerId_ttimer_id);
这个函数很简单,只需要传软件定时器的id,即可停止软件计时器,并且返回他的状态码。
删除定时器。
osStatus_tosTimerDelete(osTimerId_ttimer_id);
删除和停止类似,就不多说明了。
下面是源代码。
#include<stdio.h>#include"ohos_init.h"// CMSIS#include"cmsis_os2.h"// POSIX#include<unistd.h>// 为操作软件的时间staticinttimes=0;// 软件定时器回调函数voidtimerFunction(void){times++;printf("[Timer Test] Timer is Running, times = %d.\r\n",times);}// 主函数voidtimerMain(void){// 创建软件定时器osTimerId_ttid=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_tstatus=osTimerStart(tid,100);// 当超过三个周期位操作软件时,关闭软件while(times<=3){osDelay(100);}// 停止软件定时器status=osTimerStop(tid);// 删除软件定时器status=osTimerDelete(tid);printf("[Timer Test] Time Out!\r\n");}voidTimerTest(void){// 创建测试线程osThreadAttr_tattr;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_ttid=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智能开发套件[内核编程·上]中介绍。