1. HDF框架初探:鸿蒙南向生态概览、HDF框架设计哲学、HDF与Linux驱动的区别、第一个HDF驱动HelloWorld
1.1 鸿蒙南向生态:不只是换个内核
做嵌入式这么多年,我见过太多「换皮」方案了。鸿蒙南向不一样。它不是为了替代Linux而生的,而是为了解决一个更棘手的问题——碎片化。
你想想看,一个智能家居网关里,可能同时跑着Wi-Fi模组、蓝牙芯片、ZigBee协调器、传感器Hub。每个芯片都有自己的驱动,每个驱动都跟内核紧耦合。换一颗Wi-Fi芯片?好,整个驱动栈重写。这就是传统嵌入式开发的痛点。
鸿蒙南向生态的核心,是「硬件能力抽象化」。它不关心你用的是哪家的GPIO、哪家的I2C控制器,它只关心「这个设备能干什么」。说白了,就是把硬件接口标准化,让上层应用和中间件不再跟具体芯片绑定。
南向生态三要素:
- 硬件厂商:提供芯片、模组、外设,按照HDF规范实现驱动
- 驱动开发者:我们这些人,负责把硬件能力映射到HDF框架
- 系统集成商:把不同厂商的驱动拼装成完整系统
我记得刚接触鸿蒙时,有个项目需要同时适配海思和瑞芯微的板子。按以前的做法,得维护两套驱动代码。但用HDF框架,我只写了一套驱动描述文件,换平台时改几行配置就行。嗯,这就是南向生态的价值。
1.2 HDF框架设计哲学:分层、解耦、可迁移
HDF(Hardware Driver Foundation)的设计哲学,我总结成三个词:分层、解耦、可迁移。
先看这张图,你就明白HDF在系统里的位置了:
看到红色那层了吗?HDF框架就像个「驱动路由器」。它不直接操作硬件,而是管理驱动对象、分发消息、解析配置。具体来说:
- 分层:驱动被拆成平台驱动、设备驱动、总线驱动三层,各司其职
- 解耦:驱动之间通过HDF消息总线通信,不直接调用函数
- 可迁移:驱动描述用HCS配置文件,换平台只需改配置,不动代码
个人经验: 我刚开始用HDF时,总觉得分层太多,写个GPIO驱动要过好几层。后来遇到一个项目,需要把同一套驱动从Hi3516移植到RK3568,只改了HCS配置文件和几个平台宏定义,两天搞定。要是用传统Linux驱动,至少两周。
1.3 HDF vs Linux驱动:到底差在哪?
很多朋友问我:「HDF不就是Linux驱动的鸿蒙版吗?」还真不是。我列个表,你一看就明白:
| 对比维度 | Linux驱动 | HDF驱动 |
|---|---|---|
| 驱动模型 | 基于设备树 + platform总线 | 基于HDF设备对象 + 消息总线 |
| 配置方式 | 设备树(DTS),编译时绑定 | HCS配置文件,运行时加载 |
| 跨平台能力 | 弱,换SoC要改DTS和驱动代码 | 强,换SoC只需改HCS配置 |
| 驱动通信 | 函数直接调用 / IOCTL | HDF消息总线,支持同步/异步 |
| 生命周期 | module_init / module_exit | HDF驱动入口表,框架统一管理 |
| 调试手段 | printk / ftrace / procfs | HDF log / HDF测试框架 / 模拟器 |
核心区别在哪?Linux驱动是「内核模块」,跟内核绑死;HDF驱动是「框架组件」,可以独立加载、卸载、替换。我曾经在调试一个USB摄像头驱动时,Linux下每次改驱动都要重新编译内核模块、重启设备。HDF下呢?改完HCS配置,用hdf_install命令热加载,几秒钟搞定。
注意: HDF框架虽然支持热加载,但硬件资源(如中断、DMA)的释放和重新申请需要驱动开发者自行保证。我曾经踩过坑——热加载时没正确释放中断,导致新驱动申请中断失败。嗯,后来在驱动卸载回调里加了资源清理,问题解决。
1.4 第一个HDF驱动:HelloWorld实战
理论说再多,不如跑一个例子。我们来写个最简单的HDF驱动——它不操作硬件,只在内核日志里打印「Hello, HDF!」。
1.4.1 驱动代码
#include "hdf_device_desc.h"
#include "hdf_log.h"
#define HDF_LOG_TAG hello_driver
// 驱动初始化
static int32_t HelloDriverBind(struct HdfDeviceObject *deviceObject)
{
HDF_LOGI("HelloDriver: Bind called");
return HDF_SUCCESS;
}
// 驱动启动
static int32_t HelloDriverInit(struct HdfDeviceObject *deviceObject)
{
HDF_LOGI("HelloDriver: Init called");
HDF_LOGI("Hello, HDF!");
return HDF_SUCCESS;
}
// 驱动卸载
static void HelloDriverRelease(struct HdfDeviceObject *deviceObject)
{
HDF_LOGI("HelloDriver: Release called");
}
// 驱动入口表
struct HdfDriverEntry g_helloDriverEntry = {
.moduleVersion = 1,
.moduleName = "hello_driver",
.Bind = HelloDriverBind,
.Init = HelloDriverInit,
.Release = HelloDriverRelease,
};
HDF_INIT(g_helloDriverEntry);
1.4.2 HCS配置文件
驱动写好了,还得告诉HDF框架「这个驱动在哪、叫什么」。在HDF里,这是通过HCS文件配置的:
root {
platform {
hello_driver {
match_attr = "hello_driver_config";
moduleName = "hello_driver";
deviceName = "hello_device";
deviceMatchAttr = "hello_driver_config";
serviceName = "hello_service";
}
}
}
1.4.3 编译与运行
把驱动代码放到 drivers/hdf_core/adapter/khdf/linux/platform/ 目录下,修改 Makefile 添加编译目标。然后:
# 编译内核
./build.sh --product-name rk3568 --ccache
# 烧录后,查看内核日志
dmesg | grep HelloDriver
如果一切顺利,你会看到:
[ 0.123456] HelloDriver: Bind called
[ 0.123457] HelloDriver: Init called
[ 0.123458] Hello, HDF!
避坑指南: 我曾经在写第一个HDF驱动时,忘了在HCS文件里配置 moduleName 和驱动入口表的 moduleName 保持一致。结果驱动加载时一直报「device not found」。查了半天,发现是名字拼写差了一个下划线。嗯,HDF对名字匹配是严格区分大小写的,切记。
1.4.4 驱动生命周期
从上面的代码可以看到,HDF驱动的生命周期有三个阶段:
- Bind:驱动与设备绑定,分配资源
- Init:驱动初始化,注册服务、申请硬件资源
- Release:驱动卸载,释放资源
这个流程跟Linux驱动的 probe / remove 很像,但HDF把绑定和初始化分开了。为什么?我个人理解是:绑定阶段只做资源预留,初始化阶段才真正操作硬件。这样在资源紧张时,可以先绑定但不初始化,等需要时再启动。这种设计在低功耗场景下特别有用。
好了,第一个HDF驱动跑起来了。虽然它什么都没做,但你已经掌握了HDF驱动的基本骨架。后面我们会在这个骨架上,一步步加上GPIO、I2C、SPI等真实硬件操作。