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 的源码到底长什么样。我第一次打开源码目录时,说实话有点懵——文件夹太多了。但别怕,核心结构其实很清晰。

下面这张图帮你快速建立全局认知:

OpenHarmony 源码顶层目录结构 openharmony/ foundation/ kernel/ device/ vendor/ distributedsdk/ liteos_m/ hisilicon/ hisilicon/ hals/ kernel/ hi3861v100/ hispark_pegasus/ 说明: foundation/ — 系统服务层,包含分布式能力、AI 框架等 kernel/ — 内核层,liteos_m 是轻量级内核,用于 MCU 设备 device/ — 设备适配层,存放各芯片厂商的 BSP 和驱动 vendor/ — 产品配置层,定义具体产品的编译和打包规则

四个核心目录各司其职:

  • 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 编译流程

整个编译过程分三步:

  1. 配置 — 执行 hb set 选择产品和开发板。
  2. 生成 — GN 读取 BUILD.gn,生成 .ninja 文件。
  3. 编译 — 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 闪烁起来的那一刻,你会觉得前面的折腾都值了。