如果你跟我一样,拿到一块新板子第一反应不是看业务代码,而是纠结“内核配置到底怎么加”“驱动到底走哪条路”,那这篇应该能帮你省下不少时间。这是OpenHarmony系统实战开发系列里偏底层又绕不开的一篇,标题里的“三条路径”不是口号,是我实际跑项目时反复验证过的最稳的三种切入方式:第一条路径是内核配置与裁剪,第二条路径是基于HDF框架的驱动开发,第三条路径是外设驱动的移植与集成。三条路各有各的适用场景,但都要先把环境、配置和编译链路吃透。
这套内容适合谁看?准备在开发板上跑OpenHarmony标准系统、想把某个外设(串口芯片、电机驱动、调试器、摄像头)用起来的开发者,或者刚编译完源码想知道内核配置要改哪个文件的初学者。我会把配置入口、驱动骨架、设备节点、权限这些最容易卡人的点全部展开讲,尽量不给平台做广告,就是纯粹把实操细节摆出来。
1. 三条路径的整体拆解:配置、开发与移植
1.1 这三条路径分别解决什么问题
先说清楚什么是“内核配置与驱动三条路径”。第一路径对应的是内核配置,解决的是“默认内核里没有这个功能,我怎么把它打开”,比如你要用CH340串口芯片,默认配置里没有开对应的USB串口驱动,你在用户态写再多代码都没用,因为系统根本没有生成ttyUSB0节点。第二路径对应的是驱动开发,解决的是“内核里也没有现成驱动,我得自己写一个”,这时候要面对的是OpenHarmony的HDF驱动框架,或者传统的Linux字符设备驱动框架,两种写法都在用。第三路径对应的是驱动移植与集成,解决的是“这个外设的驱动已经有了,但不在我的板级配置里,我要怎么把它集成进系统”,常见于厂商SDK驱动、第三方调试器工具链。
这三条路径不是互斥的,实际开发中经常串起来用。举个例子,你想通过电机驱动板控制PMSM电机,第一步是在内核配置里把PWM相关的控制器打开(第一条路径),第二步是用HDF或者GPIO字符设备接口写一个控制逻辑(第二条路径),第三步是把电机驱动的dts配置、板级引脚复用配置整合进你的产品目录(第三条路径)。所以标题里写“三条路径”,其实是把整个驱动开发链路拆成了三个递进的阶段。
1.2 路径选型思路与环境准备
做选型之前先想清楚:你这个功能,是内核原生的还是外设芯片自带的?如果是USB转串口这种有现成内核驱动的场景,就别自己写驱动,直接动内核配置就够了,这是性价比最高的路径。如果是新定义的一个外设,比如自定义的电机控制板,那就走HDF开发路径。如果手里有一份厂商提供的Linux驱动源码,目标是把它跑在OpenHarmony上,那属于第三条移植路径,核心工作是适配内核版本、编译配置、hcs设备描述。
环境准备这里有个经验:OpenHarmony标准系统默认用的是Linux 5.10内核,你最好提前把下面几样东西备齐,省得到时候来回折腾:
- 一台Ubuntu 20.04或22.04的编译服务器,内存建议16G以上,磁盘剩余空间至少200G,因为整个源码加产物非常占空间。
- OpenHarmony源码,版本建议选3.2 Release之后的标准系统分支,内核代码在kernel/linux/linux-5.10目录下。
- hb工具链,也就是OpenHarmony的编译构建工具,需要先source环境变量脚本。
- 一块能跑标准系统的开发板,比如RK3568、RK3588这类,或者直接用开源鸿蒙PC版的x86环境,不过x86环境的驱动场景和ARM板子差异比较大,建议ARM板起步。
1.3 用到的工具链和命令速查
很多人第一次接触内核配置,最大的困惑是“我改了配置到底在哪编译”。OpenHarmony的构建体系里,内核不是单独拿Makefile编译的,而是由hb统一调度。常见的命令就这几个,先混个脸熟:
# 设置产品 hb set # 编译整个产品 hb build # 单独编译内核 hb build -T kernel进入内核源码目录之后,又可以退回到经典Linux内核的操作方式,比如:
# 进入内核源码目录 cd kernel/linux/linux-5.10 # 生成默认配置 make ARCH=arm64 your_board_defconfig # 打开图形化配置界面 make ARCH=arm64 menuconfig这套组合拳就是第一条路径的入口。后面我会详细说,这些命令的执行顺序错了都会导致配置不生效,比如直接在menuconfig里改了配置,但没看清楚当前.config是从哪个defconfig生成的,下一次全量编译又把老配置覆盖回来了。
2. 第一条路径:内核配置与裁剪实操
2.1 OpenHarmony内核配置的层次结构
OpenHarmony的内核配置不是只存在于一个文件,而是分了好几层,这是很多人找不到配置项在哪里的根本原因。最常见的三层结构是:产品层、板级层、内核defconfig层。
产品层的配置一般在productdefine/products/xxx.json里,里面会指定内核的defconfig名称。板级层的配置在device/board/xxx/下,比如RK3568的板级目录里有kernel.config之类的文件,里面可以追加配置项。真正执行编译时,内核目录内核会把defconfig作为基础配置,再把板级附加配置合并进来,最后生成.config文件。
我举个例子,假设你在productdefine/products/xxx.json里看到:
"kernel": { "defconfig": "rk3568_standard_defconfig" }那实际使用的配置文件就是kernel/linux/config/linux-5.10/arch/arm64/configs/rk3568_standard_defconfig。如果你想往里面加一个CONFIG项,最直接的办法是改这个defconfig,但更推荐的做法是改板级kernel.config,因为板级配置优先级更高,而且不容易在代码同步时把自己改的东西丢掉。
2.2 用menuconfig快速开启一个驱动开关
还是用串口芯片举例,比如你手上有一个CH340模块,插到开发板的USB口之后,发现/dev下没有ttyUSB0节点。这种情况九成是内核没有开启USB Serial CH341驱动。打开这个开关的操作路径是这样的:
cd kernel/linux/linux-5.10 # 先生成你板子对应的defconfig make ARCH=arm64 rk3568_standard_defconfig # 再打开图形配置 make ARCH=arm64 menuconfig在menuconfig里依次进入Device Drivers -> USB support -> USB Serial Converter support,然后找到“USB CH341/CH340 driver”这一项,按Y键把它编进内核,而不是编译成模块。这里有个关键点:在OpenHarmony标准系统里,驱动模块的动态加载机制并不是所有场景都默认可用,实操中最好把驱动直接编进内核镜像,省去模块加载的麻烦。
选完后保存退出,然后检查一下.config文件里是不是真的有了:
grep CH341 .config正常会看到CONFIG_USB_SERIAL_CH341=y。确认无误后,再回到源码根目录执行hb build -T kernel进行单编,把内核镜像烧进板子,插上CH340就能看到ttyUSB0节点了。
2.3 defconfig、config与编译缓存:配置失效的坑
这条路径上最容易翻车的就是“我明明改了开关,编译出来还是没有对应功能”。原因通常是下面几个:
第一个坑,改了menuconfig但没改defconfig。你只是把当前.config改了,但下一次执行make your_defconfig或者hb全量编译时,系统会用defconfig把当前.config覆盖掉,你改的东西直接没了。解决方法是改完后同步把改动写回defconfig,或者写到板级kernel.config里。
第二个坑,编译缓存导致内核没有重新生成。hb构建默认有缓存,单编内核时偶尔不会重新跑menuconfig相关的那一步。我遇到过好多次,改完配置之后必须手动touch一下内核源码目录里的相关文件,或者干净地删掉out目录里的内核产物再编。粗暴但有效的方法是:
rm -rf out/rk3568/kernel hb build -T kernel第三个坑,是配置项的依赖关系没满足。比如你想开启CONFIG_USB_SERIAL_CP210X,但它依赖的USB支持、TTY层如果没开,你即使把选项改成y,编译时也会被静默忽略。最终检查时不要只看menuconfig里的高亮,要看.config里实际生成的配置。
3. 第二条路径:基于HDF框架开发一个真实驱动
3.1 HDF驱动的三个核心对象
说到OpenHarmony的驱动开发,绕不开HDF(Hardware Driver Foundation)框架。这个框架解决的是设备驱动与系统服务之间的解耦问题,不再像传统Linux驱动那样只写一个file_operations结构体就完事,而是把驱动抽象成服务,由HDF框架统一管理。
理解HDF,先抓住三个核心对象:HdfDriverEntry、HdfDeviceObject、DriverHost。HdfDriverEntry是驱动入口,相当于传统驱动的module_init入口,里面定义了Bind、Init、Release三个回调。HdfDeviceObject是设备对象,驱动初始化时会拿到这个对象,里面包含了从hcs配置里解析出来的设备信息。DriverHost可以理解为驱动宿主进程,一个DriverHost可以承载多个驱动,驱动加载和卸载都由它管理。
写代码之前,先想清楚一个问题:你需要这个驱动提供什么样的能力给用户态?如果只是简单的点灯、读GPIO,那么用Linux原生的字符设备驱动路子就够了,很多OpenHarmony发行版也保留了这种方式。如果要对接系统服务,让上层应用通过SAMGR(系统服务管理器)调用硬件能力,那就必须走HDF服务化那套,把驱动发布成服务。
3.2 驱动代码骨架:从Bind到Init再到Release
写一个HDF驱动最核心的三件事:驱动入口、服务接口、hcs配置。先看驱动入口的代码骨架:
#include "hdf_device_desc.h" #include "hdf_log.h" #include "hdf_base.h" #define HDF_LOG_TAG my_driver static int32_t MyDriverBind(struct HdfDeviceObject *device) { HDF_LOGI("MyDriver bind called"); return HDF_SUCCESS; } static int32_t MyDriverInit(struct HdfDeviceObject *device) { HDF_LOGI("MyDriver init called"); // 在这里做硬件初始化,比如ioremap、request_irq、注册miscdevice return HDF_SUCCESS; } static void MyDriverRelease(struct HdfDeviceObject *device) { HDF_LOGI("MyDriver release called"); // 释放资源,注销设备 } struct HdfDriverEntry g_myDriverEntry = { .moduleVersion = 1, .moduleName = "my_driver", .Bind = MyDriverBind, .Init = MyDriverInit, .Release = MyDriverRelease, }; HDF_INIT(g_myDriverEntry);有个细节值得说一下,Bind和Init的执行时机是不同的。Bind阶段主要用于把驱动绑定到对应的设备上,如果你要做的是服务发布,通常放在Bind里;Init阶段才是真正的硬件初始化,比如配置GPIO、注册中断、创建设备节点。我在项目里见过不少人把硬件初始化写进Bind里,导致驱动加载顺序一乱就出问题,这个习惯建议改掉。
3.3 hcs配置与设备节点创建
服务端写好了,还要告诉HDF框架“这个驱动挂在哪个DeviceHost下”。这里要用到hcs(HDF Configuration Source)配置,一种类似dts的文本配置格式。一般会在vendor或者device目录下的hdf配置里增加节点,比如:
root { device_info { match_attr = "hdf_manager"; template host { hostName = ""; priority = 100; device_template { device0 { deviceName = ""; driverName = ""; serviceName = ""; permission = 0666; } } } host_my_host { hostName = "my_host"; device0 { deviceName = "my_device"; driverName = "my_driver"; serviceName = "my_service"; permission = 0666; } } } }deviceName和driverName必须和驱动入口里的moduleName对应上,否则HDF框架找不到驱动。permission字段也很关键,它决定了设备节点在用户态的访问权限。0666意味着所有用户都可以读写,开发阶段图省事可以这么干,但正式产品里建议收紧到0660,配合用户组做权限管理。
很多人在这一步卡住,是因为改完hcs之后没有重新编译对应的资源文件,framework侧的hdf配置没有更新。我建议每次改完hcs,都先确认改动有没有进入最终的镜像,可以在板子上用hdc shell进去查看/system/etc/hdfconfig下的文件内容。
3.4 用户态访问驱动的权限与验证
驱动加载成功之后,如果驱动内部注册了设备节点,用户态就可以通过设备节点访问。这里有两类节点要区分:一类是Linux原生的miscdevice或者字符设备节点,路径在/dev下;另一类是HDF框架创建的设备服务,用户态通过HDF接口来访问。
如果你在Init里用miscdevice注册了一个字符设备,那么用户态代码可以直接用open、read、write来操作。示例代码:
int fd = open("/dev/my_device", O_RDWR); if (fd < 0) { perror("open failed"); return -1; } write(fd, "hello", 5); close(fd);如果open的时候返回Permission denied,第一反应去排查SELinux。OpenHarmony标准系统默认开启了 SELinux,设备节点如果没有配置对应的SELinux标签,用户态进程会被拦截。解决方式是在对应的te文件里加上allow规则,或者临时用setenforce 0验证(仅限开发环境)。
我再分享一个排障技巧:驱动加载失败时不要只盯着dmesg,HDF框架有自己的hilog日志系统。用hilog | grep HDF查看驱动加载过程,能看到Bind、Init是否被调用,如果看到“driver bind failed”但找不到原因,大概率是hcs里deviceName、driverName、serviceName三者没有对齐。
4. 第三条路径:外设驱动移植与集成实战
4.1 串口芯片驱动:CH340、CP210x、FT232的集成
第三条路径里,最常遇到的是USB转串口芯片的驱动集成。尤其在做调试、对接上位机的时候,CH340、CP2102、FT232几乎人手一个。这些芯片在内核里其实都有现成驱动,问题只在于你没有把对应的CONFIG打开。
三个常见芯片对应的内核配置项分别是:
| 芯片 | 配置项 | 生成的设备节点 |
|---|---|---|
| CH340/CH341 | CONFIG_USB_SERIAL_CH341 | /dev/ttyUSB0 |
| CP210x | CONFIG_USB_SERIAL_CP210X | /dev/ttyUSB0 |
| FT232/FT231X | CONFIG_USB_SERIAL_FTDI_SIO | /dev/ttyUSB0 |
集成步骤分为三步:先在menuconfig里把对应开关打开,然后重新单编内核,最后插上USB转串口模块,用dmesg查看设备识别情况。正常会看到类似usb 1-1: ch341-uart converter now attached to ttyUSB0的日志。如果没有日志,先用lsusb确认芯片有没有被USB控制器识别到,排除硬件接线和供电问题。
这类驱动移植最折腾的是版本匹配。比如CP210x芯片有旧版CP2102和新版CP2102N,内核驱动如果版本太老,CP2102N可能识别异常。这个时候优先升级内核小版本,或者找官方驱动源码走模块编译,不要试图在旧内核里打补丁。
4.2 电机驱动:TB6612与L293D的GPIO/PWM控制
电机驱动板是另一个高频场景,尤其是做机器人、智能车的项目。TB6612、L293D这类电机驱动模块原理差不多,控制脚由GPIO和PWM组成:GPIO控制正反转,PWM控制速度。在OpenHarmony上的集成,本质上是配置GPIO和PWM两个子系统的过程。
先用第一路径解决内核侧的PWM配置。以RK3568为例,需要确认内核配置里开启了CONFIG_PWM_ROCKCHIP,并在设备树或板级hcs里把对应PWM控制器的引脚复用配置好。然后是应用侧的PWM操作,打开系统PWM接口:
echo 0 > /sys/class/pwm/pwmchip0/export echo 1000000 > /sys/class/pwm/pwmchip0/pwm0/period echo 500000 > /sys/class/pwm/pwmchip0/pwm0/duty_cycle echo 1 > /sys/class/pwm/pwmchip0/pwm0/enable如果你在板子上找不到/sys/class/pwm目录,说明内核没有使能PWM子系统,需要回到第一条路径打开CONFIG_PWM和CONFIG_PWM_SYSFS。这个知识点经常考,属于“一看就会、一查就废”的典型。
GPIO部分可以使用LDOS等接口,或者直接操作/sys/class/leds/下的节点(很多板子把GPIO复用成了LED),更规范的做法是通过GPIO字符设备接口操作。我自己的习惯是用HDF提供GPIO接口,代码里配置方向、电平时比较干净,也方便跟业务逻辑打包成一个系统服务。
4.3 调试器与视觉设备:ST-Link、J-Link、摄像头相关配置
调试器驱动这块,ST-Link和J-Link的USB驱动在Linux内核里也都以USB设备驱动形式存在,正常情况下插上就能识别。但我们在OpenHarmony环境里经常遇到设备节点看不到的问题,原因通常是USB设备节点的权限或者SELinux上下文不对。开发阶段最简单的做法是配置udev规则,把设备节点权限放开:
SUBSYSTEM=="usb", ATTR{idVendor}=="0483", ATTR{idProduct}=="374b", MODE="0666"J-Link的话,厂商给的驱动安装包本质也是放udev规则加上库文件,在OpenHarmony上如果不想装厂商工具,直接把udev规则放到/etc/udev/rules.d/下即可。
再说视觉驱动。现在很多项目会接海康相机或者其他USB摄像头做ROS录制、视觉识别。内核侧主要开启V4L2框架和UVC协议支持,配置项包括CONFIG_VIDEO_DEV、CONFIG_VIDEO_V4L2、CONFIG_USB_VIDEO_CLASS。如果只是做预览,用户态直接用V4L2接口即可;如果要做拍照,还需要对应Sensor的驱动,这个往往依赖厂商的BSP包,属于第三条路径里难度较高的移植场景。
5. 编译烧录与问题排查技巧
5.1 单编内核并快速验证
驱动配置和代码改完之后,最需要的是快速验证方式。全量编译一次OpenHarmony耗时很长,所以必须学会单编内核。操作是这样的:
hb set # 选择你的产品 hb build -T kernel编译产物一般会输出到out/产品名/kernel/目录下,比如out/rk3568/kernel/xxx.img。拿到这个img文件,可以用烧录工具单独烧内核分区。这里有个经验:如果你改的是内核配置或者驱动代码,只要烧内核分区就可以,不需要刷整个系统镜像,能省下大量等待时间。
如果你修改的内容涉及hcs配置但不在内核目录里,那么光编内核可能不够,还要编译对应产品的资源镜像。最稳妥的办法是编完kernel后,再全量编一次,确认hcs资源是否进入最终镜像。
5.2 烧录后的驱动加载验证
板子起来之后,第一件事就是确认驱动是否加载成功。我的验证顺序一般是:
- 用hdc shell进入到板子系统。
- 执行ls /dev,看看有没有预期的设备节点。
- 执行dmesg | grep -i "usb|tty|pwm|gpio",看内核日志里有没有报错。
- 执行hilog | grep HDF,看HDF驱动的加载链路是否正常。
如果设备节点出现了但读写报错,检查权限:ls -l /dev/my_device,确认权限位和属主是否正确。如果权限正确但仍读不了,大概率是SELinux的avc denied,执行dmesg | grep avc,把缺失的权限加进te文件再重新打镜像。
5.3 常见问题速查表
最后把这套流程里高频踩坑点整理成一张速查表,遇到问题直接对照着排查:
| 现象 | 大概率原因 | 解决方向 |
|---|---|---|
| 改了菜单配置但编译后没有生效 | 改动没有写回defconfig/板级config,被覆盖 | 同步修改kernel.config,并清理内核产物 |
| usb设备插上后dmesg无任何打印 | USB控制器未识别或供电不足 | 先lsusb确认设备是否在USB链路层被发现 |
| 驱动Bind失败 | hcs里driverName/deviceName对不上 | 比对hcs配置与HdfDriverEntry.moduleName |
| Init成功但设备节点不存在 | 未注册miscdevice或注册失败 | 检查代码是否调用了misc_register,查看返回值 |
| 用户态open返回Permission denied | 设备节点权限不足或SELinux拦截 | chmod 0666节点或增补SELinux策略 |
| PWM节点找不到 | 内核未开启PWM_SYSFS | 回第一条路径检查CONFIG_PWM及板级PWM配置 |
| 新驱动编译报未定义符号 | 依赖的子系统未开启 | 回内核配置开启对应的CONFIG依赖项 |
5.4 我的几条实操心得
顺着这个排查表,再分享几点我在项目里总结出的经验。
第一,内核配置这种东西最怕“凭感觉”。每一步操作之后用grep去确认.config里真的有那个选项,再往下走,尤其是自动化编译脚本改配置的时候,一定要在脚本里加一个生成后校验的步骤。
第二,HDF驱动排障不要只盯着内核日志。HDF框架的日志走的是hilog通道,和内核的dmesg是两套体系。很多人在dmesg里搜不到HDF的记录就开始怀疑环境坏了,其实是日志看错了地方。
第三,不要把外设驱动的硬件问题错判成软件问题。我有一次调CH340串口,一直卡在读不到数据,查配置、查驱动、查权限弄了一整天,最后发现是USB转串口模块的杜邦线接触不良。硬件问题在驱动开发里出现的频率比我预想的高得多。
第四,如果你是做产品而不是做实验,尽早把权限策略和驱动配置放到版本管理里。defconfig的改动、hcs配置、SELinux策略、udev规则,这些都应该是项目的一部分,而不是某台机器的本地改动。我在实际使用中最深的一点体会是,跑通一套驱动并不难,难的是让这套改动在下次全量构建时还能稳定复现。把三条路径的每一步都固化成配置文件和脚本,后面再做其他板子或者换内核版本时,你就能明显感觉到底层工作没有白做。这套方法后续还可以继续扩展到更多外设上,只要记住:先配内核、再写驱动、后做移植,按这个顺序走,大多数硬件问题都能被稳稳拿下。