OpenHarmony智能開發套件—內核編程(上)
前言
本篇具體介紹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 = [
"http://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內核的信息。下面筆者會帶著大家編碼操作,從實際去體會內核編程。
內核編程
線程管理
在管理線程前,我們需要了解線程,線程是調度的基本單位,具有獨立的??臻g和寄存器上下文,相比與進程,他是輕量的。舉一個實際的例子,動物園賣票。
對于動物園賣票這件事本身而言是一個進程,而每一個買票的人可以看作一個線程,在多個售票口處,我們并發執行,并行計算,共同消費動物園的門票,像享受共同的內存資源空間一樣。為什么要線程管理呢?你我都希望買到票,但是票有限,我們都不希望看到售票廳一篇混亂,因此對線程進行管理是非常重要的一件事情。
任務
創建一個線程,每間隔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 = [
"http://commonlibrary/utils_lite/include",
"http://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 = [
"http://commonlibrary/utils_lite/include",
"http://device/soc/hisilicon/hi3861v100/hi3861_adapter/kal/cmsis"
]
}
- 編寫app下的gn文件。
執行效果
軟件定時器的API相對較少,這里還是提供所有的軟件定時器API。
結束語
本篇主要介紹了一些基礎內核編程相關的內容,希望能夠幫助到學習OpenHarmony的伙伴們,考慮到篇幅問題,剩余的基礎內核編程將在OpenHarmony智能開發套件[內核編程·上]中介紹。