4. Hello World 与工程结构:编写第一个 OpenHarmony 程序
终于到了动手写代码的环节。说实话,很多初学者一上来就盯着源码目录看,很容易被吓到。我的建议是——先跑通一个 Hello World,再回头理解结构,这样心里才有底。
4.1 第一个程序:点亮一盏灯
在嵌入式世界里,Hello World 通常不是打印一句话,而是点亮一盏 LED。OpenHarmony 南向开发也不例外。我们以 Hi3861 开发板为例,写一个让板载 LED 闪烁的程序。
先看代码,我贴一个最精简的版本:
#include "ohos_init.h"
#include "cmsis_os2.h"
#include "iot_gpio.h"
#define LED_GPIO 9 // Hi3861 板载 LED 对应 GPIO9
void LedTask(void *arg)
{
(void)arg;
IoTGpioInit(LED_GPIO);
IoTGpioSetDir(LED_GPIO, IOT_GPIO_DIR_OUT);
while (1) {
IoTGpioSetOutputVal(LED_GPIO, IOT_GPIO_VALUE1); // 亮
osDelay(500);
IoTGpioSetOutputVal(LED_GPIO, IOT_GPIO_VALUE0); // 灭
osDelay(500);
}
}
void LedExampleEntry(void)
{
osThreadAttr_t attr = {0};
attr.name = "LedTask";
attr.stack_size = 4096;
attr.priority = osPriorityNormal;
if (osThreadNew(LedTask, NULL, &attr) == NULL) {
printf("Failed to create LedTask\n");
}
}
SYS_RUN(LedExampleEntry);
这段代码做了三件事:初始化 GPIO、创建任务、在任务里循环闪烁。注意最后一行 SYS_RUN(LedExampleEntry),这是 OpenHarmony 的入口宏,告诉系统在启动时自动执行这个函数。
SYS_RUN 时,总忘记它是在系统初始化阶段执行的。如果你在里面调用了还没初始化的外设,系统会直接挂掉。记得先初始化,再操作硬件。
4.2 理解源码目录结构
跑通程序后,我们来看看 OpenHarmony 的源码到底长什么样。我第一次打开源码目录时,说实话有点懵——文件夹太多了。但别怕,核心结构其实很清晰。
下面这张图帮你快速建立全局认知:
四个核心目录各司其职:
- foundation/ — 系统服务层。分布式软总线、AI 框架都在这里。南向开发一般不动它。
- kernel/ — 内核层。Hi3861 用的是 LiteOS-M,一个轻量级实时内核。
- device/ — 设备适配层。芯片厂商的 BSP、驱动、HAL 实现都放这。我们写 LED 程序时调用的
iot_gpio.h就来自这里。 - vendor/ — 产品配置层。定义你的产品用哪个芯片、编译哪些模块、打包成什么镜像。
device/ 和 vendor/。前者写驱动,后者配编译。这两个目录搞明白了,开发效率能翻一倍。
4.3 认识编译构建系统:GN + Ninja
OpenHarmony 的编译系统用的是 GN + Ninja。GN 负责生成构建规则,Ninja 负责真正执行编译。说白了,GN 是「设计师」,Ninja 是「施工队」。
我第一次接触 GN 时,觉得它的语法有点怪——不像 Makefile 那么直观。但用顺手后会发现,GN 的变量作用域和依赖管理比 Makefile 清晰得多。
4.3.1 GN 的核心文件
每个模块目录下都有一个 BUILD.gn 文件,它告诉编译系统:这个模块有哪些源文件、依赖哪些库、生成什么目标。看一个实际例子:
# device/hisilicon/hi3861v100/sdk_liteos/BUILD.gn
import("//build/lite/config/component/lite_component.gni")
lite_component("hi3861_sdk") {
sources = [
"src/init/init.c",
"src/led/led_example.c",
]
include_dirs = [
"include",
"//kernel/liteos_m/include",
]
deps = [
"//kernel/liteos_m:kernel",
"//device/hisilicon/hi3861v100/adapter:hal",
]
}
这个文件定义了一个叫 hi3861_sdk 的组件。它编译两个源文件,依赖内核和 HAL 层。注意 // 表示源码根目录,这是 GN 的路径写法。
4.3.2 编译流程
整个编译过程分三步:
- 配置 — 执行
hb set选择产品和开发板。 - 生成 — GN 读取
BUILD.gn,生成.ninja文件。 - 编译 — Ninja 根据
.ninja文件并行编译,输出镜像。
实际操作用的是 hb build 命令,它会自动调用 GN 和 Ninja。你不需要手动敲 gn 或 ninja 命令。
BUILD.gn 后忘了重新 hb set,结果编译出来的还是旧版本。记住:改了 vendor/ 或 device/ 下的 BUILD.gn,最好重新 hb set 一次,确保配置生效。
4.3.3 常用 GN 语法速查
| 语法 | 说明 | 示例 |
|---|---|---|
sources |
源文件列表 | sources = ["main.c", "gpio.c"] |
include_dirs |
头文件搜索路径 | include_dirs = ["//include"] |
deps |
依赖的其他模块 | deps = ["//kernel/liteos_m:kernel"] |
libs |
链接的静态/动态库 | libs = ["m", "c"] |
output_name |
指定输出文件名 | output_name = "my_driver" |
4.4 把 LED 程序加入编译
理论讲完了,我们实操一下——把刚才的 LED 闪烁程序加到工程里,让它真正编译进镜像。
假设你的代码文件叫 led_example.c,放在 device/hisilicon/hi3861v100/sdk_liteos/src/led/ 下。你需要修改同目录下的 BUILD.gn:
lite_component("hi3861_sdk") {
sources = [
"src/init/init.c",
"src/led/led_example.c", # 新增这一行
]
# 其余不变
}
然后重新编译:
hb build -f
-f 参数强制全量编译。第一次加文件时建议用全量编译,避免增量编译漏掉新文件。
sources 列表里按模块分组,用注释隔开。比如 # LED 驱动 下面放 LED 相关文件,# 按键驱动 下面放按键相关文件。这样后期维护时一目了然。
4.5 本章小结
这一章我们做了三件事:写了一个 LED 闪烁程序、理清了源码目录结构、学会了 GN 编译的基本用法。你可能会觉得内容有点多,但这些都是南向开发的「基本功」。尤其是 GN 的 BUILD.gn 文件,后面每个模块都要跟它打交道。
嗯,现在你可以打开终端,试着编译一下自己的第一个 OpenHarmony 程序了。看到 LED 闪烁起来的那一刻,你会觉得前面的折腾都值了。