探秘鸿蒙南向开发:Hi3861 架构、编译与实战全解析
欢迎来到 OpenHarmony 的南向设备开发世界!作为技术布道师,今天我将带大家拨开鸿蒙系统的层层迷雾。我们将以经典的 Hi3861 物联网芯片为切入点,从宏观的源码架构到微观的编译原理,再到五个循序渐进的实战项目,为你构建一套完整的鸿蒙嵌入式开发知识体系。
一、庖丁解牛:OpenHarmony 源码架构全景
OpenHarmony 的源码组织就像一座精密的城市,每一座建筑(文件夹)都有其明确的职能。当我们拿到源码包时,不要被庞大的目录吓倒,掌握以下核心模块即可游刃有余:
- 内核层(Kernel & Drivers) :这是系统的基石。
kernel/目录存放着 LiteOS-M 等轻量级内核代码;drivers/则包含了 HDF(硬件驱动框架),为所有外设提供统一的访问标准。 - 基础能力层(Base & Foundation) :
base/是鸿蒙的核心能力集合,涵盖了分布式软总线、图形、安全等子系统;foundation/则提供了 Ability 框架、包管理等支撑上层应用运行的服务。 - 设备适配层(Device & Vendor) :这是南向开发者的主战场。
device/目录包含了芯片级(SoC)和板级(Board)的适配代码,例如 Hi3861 的底层驱动和引脚配置都藏在device/soc/hisilicon/hi3861v100/中。 - 应用与示例层(Applications) :
applications/存放着系统预置应用和各类 Demo。在教学或原型开发中,我们的业务代码通常就放在这里的applications/sample/wifi-iot/app/目录下。
二、思维转换:告别 int main(),拥抱鸿蒙入口机制
在传统 C 语言开发中,无论是控制台程序还是单片机裸机程序,我们都习惯了从 int main() 开始编写代码。但在鸿蒙的南向设备开发中,你会惊讶地发现:在业务代码中,我们几乎不再手写 int main() 函数。
这并非鸿蒙“抛弃”了 C 语言,而是因为鸿蒙作为一个操作系统,其底层架构和启动流程发生了根本性的改变。
1. 为什么不能直接用 int main()?
在标准 C 程序中,main() 是绝对的起点,程序执行完 main() 里的 return 0; 后,整个进程就结束了。但在鸿蒙系统中,main() 函数实际上被底层的启动文件(如 app_main.c,位于 vendor/hisi/hi3861/hi3861/app/wifiiot_app/src 目录下)和内核接管了。鸿蒙的启动流程是:系统上电 → 内核初始化 → 系统服务启动 → 调用 app_main()。如果开发者在业务代码中强行写一个 int main(),不仅无法被系统识别为入口,反而可能导致链接冲突或系统启动失败。
2. 鸿蒙的入口机制:SYS_RUN 宏注册
既然没有 main(),鸿蒙应用是如何启动的?答案是事件驱动与组件化注册。
在鸿蒙的轻量系统(LiteOS-M)中,官方提供了一套优雅的入口注册机制。开发者只需编写一个普通的 C 函数(例如命名为 HelloWorld),然后使用 SYS_RUN(HelloWorld); 宏将其注册到系统中。
#include <stdio.h>
#include "ohos_init.h"
#include "ohos_types.h"
void HelloWorld(void)
{
printf("[DEMO] Hello world.\n");
}
SYS_RUN(HelloWorld); // 告诉系统:这就是我的应用入口
SYS_RUN 定义在 ohos_init.h 头文件中。其背后的原理是:SYS_RUN(app_entry) 定义了一个名为 __zinitcall_run_app_entry 的函数指针,并通过强制编译的方式将其放入 .zinitcall.run2.init 段中。系统在初始化阶段会遍历该段中的所有函数指针并依次调用——这就是鸿蒙“组件化自动注册”的底层魔法。
3. 从“单线阻塞”到“多任务并发”
理解了入口机制,还要理解鸿蒙的任务模型。在传统 C 语言中,我们习惯在 main() 里写 while(1) 来轮询硬件状态。但在鸿蒙中,SYS_RUN 注册的入口函数必须快速执行并返回,绝不能在里面写死循环——因为 SYS_RUN 运行在系统根线程中,一旦阻塞,整个系统的核心服务启动都会被拖累。
正确的做法是:在入口函数中,利用 osThreadNew() 创建独立的业务线程(Task),将 LED 闪烁、按键检测、屏幕刷新等耗时逻辑放入各自的线程中。这不仅符合鸿蒙的架构规范,更是发挥操作系统多任务并发优势的关键。
总结来说:传统 C 语言开发是“以 main 函数为核心的单线执行”,而鸿蒙南向开发则是“以 SYS_RUN 注册为起点,以多线程任务为核心的并发执行”。跨越这道认知门槛,才算真正推开了鸿蒙开发的大门。
三、编译引擎:GN 与 Ninja 的协作艺术
理解了架构,我们来看看鸿蒙是如何将成千上万个 C 文件变成可执行固件的。OpenHarmony 摒弃了传统的 Makefile,采用了更高效的 GN + Ninja 构建体系。
- GN(Generate Ninja) :它相当于“建筑师”,负责读取
BUILD.gn配置文件,解析模块之间的依赖关系,最终生成机器可读的 Ninja 构建文件。GN 脚本采用类似 Python 的语法风格,通过声明式配置定义构建规则。 - Ninja:它是“施工队”,专注于以最快的速度执行 GN 生成的编译和链接任务。
构建工具的版本演进:从 build.py 到 hb
这里有必要梳理一下 OpenHarmony 构建工具的演进脉络,因为不同版本的构建命令截然不同。
OpenHarmony 1.0 时代:python build.py
在 OpenHarmony 1.0 版本中,编译构建的核心工具是源码根目录下的 build.py 脚本。一个典型的编译流程是在源码根目录下执行:
python build.py wifiiot
这条命令会启动整个编译构建流程。顶层的 build.py 文件本身只是一个链接,它实际指向的是 build/build_scripts/build.py。编译成功后,生成的二进制文件会存放在 out/wifiiot 目录下。
OpenHarmony 1.1.0 及之后:hb 工具
hb(HarmonyOS/OpenHarmony Build)是在后续版本中才被正式引入并成为主流构建工具的。它可以通过 pip 包管理器安装,其 Python 包名是 ohos-build。
从源码演进的角度看,hb 可以看作是 build.py 的进化版——build.py 脚本的核心工作之一,正是去调用 build/hb/main.py。因此,hb 并非一个凭空创造的新工具,而是对原有 Python 构建脚本的系统化、工程化封装。
作为开发者,我们不需要直接敲冗长的 GN 命令。hb 充当了“包工头”的角色:
hb set # 选择产品
hb build # 一键启动编译
它会自动调度底层的 GN 和 Ninja 完成所有工作。值得注意的是,hb 的版本需要与源码版本严格匹配,否则可能出现兼容性问题。
| 版本 | 主要构建方式 | 特点 |
|---|---|---|
| OpenHarmony 1.0 | python build.py wifiiot |
直接调用 Python 脚本,是当时的官方标准 |
| OpenHarmony 1.1.0 及之后 | hb set + hb build |
成为官方主推的构建工具,提供更统一便捷的命令行体验 |
| 现代版本(如 4.0) | hb 工具 |
hb 已成为绝对主流 |
四、核心法则:BUILD.gn 的目录结构与依赖调度
在多项目并行的教学场景中,如何优雅地管理 1~5 课的代码并实现一键切换?这依赖于 OpenHarmony 构建系统底层的目录结构与依赖法则。GN 文件之间不是通过传统的“导入(import)”关联的,而是通过 “依赖(deps)” 和 “标签(Labels)” 来建立树状的调用图。
1. 目录结构与聚合调度(Group)
在我们的教学工程中,所有的子项目(project_01_led 到 project_05_oled_display)都统一存放在 applications/sample/wifi-iot/app/ 目录下。在这个根目录中,我们定义了一个核心的 BUILD.gn 文件作为总调度室。
通过定义一个 group("all_projects") 目标,我们将不同子项目的路径(如 ./project_01_led:led_app)写入其 deps 列表。当你执行 hb build -T applications/sample/wifi-iot:all_projects 时,GN 会顺着 deps 的指引,像树状图一样递归加载并编译对应的模块。这种设计既保证了各个实验模块的独立性,又实现了全局的统一调度。
2. 跨目录的依赖与接口暴露(Deps & Include)
在具体的实验(如实验一 LED 闪烁)中,子目录下的 BUILD.gn 需要编译业务源文件。由于业务代码中调用了鸿蒙底层的 hi_gpio.h 等 API,这些头文件并不在子目录中。
此时,必须通过 include_dirs 显式声明头文件路径,并通过 deps 声明对底层 SDK 的依赖。GN 的隔离机制要求:如果 A 依赖 B,B 的头文件不会自动暴露给 A,必须通过 public_configs 或明确的 include_dirs 来建立关联。这就解释了为什么每个子项目的 BUILD.gn 中都需要配置 SDK 的依赖。
一个典型的子项目 BUILD.gn 结构如下:
static_library("led_app") {
sources = [
"main.c"
]
include_dirs = [
"//utils/native/lite/include",
"//device/soc/hisilicon/hi3861v100/sdk_liteos/include"
]
deps = [
"//device/soc/hisilicon/hi3861v100/sdk_liteos_m:iot_sdk"
]
}
3. 教学场景的无缝切换——GN 编译参数动态注入
基于上述结构,当教师需要切换课程时,传统做法是修改根目录 BUILD.gn 中 group 的 deps 列表——注释掉上一课,取消注释当前课。但频繁修改根目录文件容易引发 Git 冲突。
这里赠大家一招 “GN 编译参数动态注入” 的绝学,让所有项目共存,永不修改 BUILD.gn:
在根目录 BUILD.gn 中定义调度组:
group("all_projects") {
deps = []
if (build_lesson == "led") {
deps += [ "./project_01_led:led_app" ]
} else if (build_lesson == "pwm") {
deps += [ "./project_04_pwm:breath_app" ]
} else if (build_lesson == "oled") {
deps += [ "./project_05_oled:oled_app" ]
}
# 默认编译第一课
if (deps == []) {
deps += [ "./project_01_led:led_app" ]
}
}
编译时,只需一句命令便可神游五课之间:
# 切换到 PWM 呼吸灯实验
hb build --build-args build_lesson="pwm"
这不仅是编译技巧的展示,更是向学员传递“工程化思维”的种子——让代码配置脱离硬编码,是走向专业开发的必经之路。
五、从点亮到互联:Hi3861 五步进阶实战
理论必须结合实践。基于上述架构与编译原理,我们为 Hi3861 设计了五个循序渐进的实战项目:
-
LED 闪烁控制:这是鸿蒙开发的“Hello World”。我们学习了使用
SYS_RUN宏注册应用入口,通过osThreadNew创建独立任务避免阻塞系统,并掌握了hi_io_set_func和hi_gpio_set_dir等底层 API 来控制 GPIO 输出。 -
按键中断检测:告别低效的轮询,我们引入了硬件中断机制。通过配置 GPIO 输入模式和
hi_gpio_register_isr_function注册回调函数,实现了 CPU 资源的极致释放。 -
按键控 LED(含软件消抖) :将输入与输出结合。针对机械按键的物理抖动,我们巧妙地利用
osKernelGetTickCount()获取系统滴答时钟,用时间戳差值实现了优雅的软件消抖。 -
PWM 呼吸灯:引入脉冲宽度调制技术。通过动态调整占空比,配合状态机思维控制占空比的平滑增减,让 LED 呈现出如呼吸般自然的明暗过渡。
-
OLED 屏幕显示:迈向人机交互。我们配置了 I2C 通信协议,将引脚复用为 SCL 和 SDA,并引入了第三方的 SSD1306 驱动。在这个过程中,我们还学到了为涉及字符串格式化的任务分配更大栈空间(2048 字节)的防溢出技巧。更深一层:如果 SSD1306 驱动中那块 1024 字节的显存缓冲区定义为 局部变量,它会压在任务栈上;若改为
static修饰,显存便会从“栈”迁移到“静态存储区”,内存压力瞬间释放。用好static,是嵌入式开发者对稀缺 RAM 最基本的尊重。
六、硬件基石:Hi3861 核心引脚与外设复用对照表
在嵌入式开发中,引脚复用(Pin Multiplexing)是最容易踩坑的环节。选错引脚或者忘记配置复用功能,硬件就会毫无反应。结合 Hi3861 芯片的硬件手册,我们整理了这份核心引脚对照表,方便大家在实战中快速查阅和接线:
| GPIO 编号 | 默认功能 | 复用功能 1(常用) | 复用功能 2(常用) | 教学实验应用 | 备注说明 |
|---|---|---|---|---|---|
| GPIO_5 | GPIO | UART1_RXD | PWM0_OUT | 实验四:PWM 呼吸灯 | 支持 PWM 输出 |
| GPIO_9 | GPIO | I2C0_SCL / UART2_RTS | PWM0_OUT | 实验一:LED 闪烁 | 开发板板载 LED 默认连接此引脚 |
| GPIO_10 | GPIO | I2C0_SDA / UART2_CTS | PWM1_OUT | 实验二/三:按键检测 | 开发板 User 按键默认连接此引脚 |
| GPIO_11 | GPIO | UART2_TXD | PWM2_OUT | 扩展实验:电机控制 | 支持 PWM 输出 |
| GPIO_12 | GPIO | UART2_RXD / SDIO_CLK | PWM3_OUT | 扩展实验:舵机控制 | 支持 PWM 输出 |
| GPIO_13 | GPIO | I2C0_SDA / UART0_LOG_TXD | PWM4_OUT | 实验五:OLED 显示 | 作为 I2C1_SCL 使用 |
| GPIO_14 | GPIO | I2C0_SCL / UART0_LOG_RXD | PWM5_OUT | 实验五:OLED 显示 | 作为 I2C1_SDA 使用 |
给开发者的“避坑”指南:
-
复用功能的唯一性:一个 GPIO 引脚在同一时刻只能作为一种功能使用。在代码中必须通过
hi_io_set_func()明确指定,且后配置的功能会覆盖先配置的功能。 -
I2C 引脚的固定搭配:在使用 I2C 协议(如实验五的 OLED 屏幕)时,SCL 和 SDA 必须成对出现。Hi3861 的 I2C1 固定对应 GPIO_13(SCL) 和 GPIO_14(SDA) ,接线时切勿接反。
-
PWM 通道的对应关系:并非所有引脚都支持 PWM。Hi3861 共有 6 路 PWM(PWM0~PWM5),在初始化 PWM 外设时,必须确保传入的通道号与引脚的复用功能严格匹配。
-
查阅头文件:在实际开发中,如果遇到不确定的引脚,建议直接在代码编辑器中查看
hi_io.h或相关头文件,查看官方定义的宏枚举值,这是最准确的排错方式。
七、版本演进:构建体系的“穿越”指南
本文的示例主要基于 OpenHarmony 1.1.0 ~ 3.0 LTS 代码分支。若你下载的是 OpenHarmony 3.2 Release 或 4.0+ 版本,部分构建配置会有所不同。
在 3.2 及更早版本中,部件配置通过 bundle.json 文件完成。而在 4.0 版本中,构建系统演进为使用 ohos.build 配合 config.json 进行部件和子系统的声明。正如一位社区开发者所言:“OpenHarmony 3.2 和 4.0 的编译规则不一样,一定要仔细看官方的编译构建教程。”
若你在 4.0 版本中寻不见 bundle.json 的踪迹,不必惊慌——那正是鸿蒙系统走向成熟的标志。届时只需聚焦 ohos.build 中的 module_type 声明,万变不离其宗。
八、结语
从一行代码到一套系统,从点亮一盏灯到驱动一块屏幕,OpenHarmony 的南向开发虽然有一定的学习门槛,但其分层解耦、组件化的设计思想,为万物互联时代提供了无限可能。
布道师的最后叮嘱:鸿蒙南向开发的核心在于三个“转变”——从 main 到 SYS_RUN 的入口思维转变,从 Makefile 到 GN/Ninja 的构建思维转变,从单线程轮询到多任务并发的并发思维转变。跨过这三道门槛,你便真正推开了鸿蒙设备开发的大门。🚀
更多推荐




所有评论(0)