在上一篇文章中,我记录了如何在 OpenHarmony 移植初期,通过 hdc shell 强行手动构建目录、配置网络,最终让 RISC-V 开发板连上公网的过程。

那种通过命令行直接操控底层硬件的感觉很爽,但爽过之后,必须回归工程现实。一个成熟的系统软件工程师,不应只停留在“能把问题修好”,更要做到“把解决方案沉淀到架构中”。今天这篇文章,我将详细记录我是如何将一次临时的“手工 Hack”,一步步翻译成 OpenHarmony 的源码补丁(Patch),并交付给队友进行自动化整合的。

 经过对内核日志(dmesg)的持续监测,我定位到了问题的底层触发机制:系统中负责射频模块供电的 spacemit-rf-pwrseq 进程与蓝牙驱动节点发生逻辑冲突,导致硬件陷入高频断电循环。WiFi 芯片在初始化阶段物理供电被不断切断,直接导致总线通信失败。

本文将详细记录我针对这一供电死循环问题所进行的两次技术尝试。第一部分记录了通过直接修改内核驱动和设备树进行的失败尝试;第二部分记录了最终成功解决问题的方案——通过 OpenHarmony 初始化子系统注入自启动服务。

开始的思路是修改对应原始代码,然后进行全量编译

第一部分:初次尝试(源码级全量修改与失败分析)

在发现物理供电被异常切断后,我的第一直觉是直接深入底层,从 Linux 内核驱动源码和设备树(DTS)层面彻底阻断这一行为。这一阶段的修改涉及内核 C 代码、设备树配置以及 OpenHarmony 的构建产物配置。

1. 内核驱动修改:拦截断电指令

最初的逻辑是,既然 spacemit-rf-pwrseq 进程在疯狂发送断电指令,我可以直接在内核源码中进行拦截。

目标文件: kernel/linux/spacemit_kernel-6.6/drivers/soc/spacemit/spacemit-rf/spacemit-pwrseq.c

我修改了该驱动文件中的 power_off 函数逻辑。在函数执行的最前端,我加入了一个直接返回的操作。预期效果是:斩断内核中该进程的高频断电循环逻辑。当系统下发物理电源断电请求时,函数直接 return,从而强制底层物理引脚保持常亮状态,解除 WiFi 芯片无法维持供电的限制。

2. 设备树(DTS)驱动节点修正

单纯拦截断电指令是不够的,还需要确保系统在初始化时分配了正确的硬件资源。

目标文件: kernel/linux/spacemit_kernel-6.6/arch/riscv/boot/dts/spacemit/k1-x_MUSE-Paper2.dts

在该设备树文件中,我进行了两项操作:

  • 修正 GPIO 使能引脚: 重新配置了 wlan-pwrseq 节点的 GPIO 参数,使其严格对齐原厂的引脚定义。

  • 添加电源常驻属性: 在节点属性中显式添加了 keep-power-in-suspend 字段。这一配置的理论作用是,即使系统进入休眠状态,底层也不会拉低 WiFi 模块的供电电平。

3. 上层网络工具链与构建配置

为了确保 WiFi 模块被系统识别后能够正常进行网络协议握手,我同步修改了 OpenHarmony 的构建脚本,注入了网络层的必要组件。

  • 新增 WiFi 配置目录: 在源码的 vendor/spacemit/musepaper2/ 目录下新建了 wifi_config/ 文件夹。该文件夹用于存放 wpa_supplicant.conf 配置文件,并编写了对应的 BUILD.gn 规则,指明文件的安装路径。

  • 修改 ohos.build 注入依赖:device/board/spacemit/musepaper2/ohos.build 文件的 module_list 数组中,添加了上述 WiFi 工具链和配置文件的编译依赖。

  • 修改 BUILD.gn 增加打包目标:device/board/spacemit/musepaper2/BUILD.gn 文件的 deps 列表中,加入了 wpa_supplicantwpa_cli。这确保了在执行 Ninja 构建时,无线网络认证程序会被打包进最终的镜像文件。

  • 修改 init.musepaper2.cfg 拉起服务:device/board/spacemit/musepaper2/cfg/init.musepaper2.cfg 中,添加了系统开机时自动创建 /data/service/el1/public/wifi 等运行所需目录的指令,并配置了直接拉起 wpa_supplicant 的服务节点。

4. 构建脚本编译故障修复

在全量编译验证上述修改时,我还遇到了一个编译阻塞点。执行 ./build.sh 时,编译过程卡死。检查发现问题出在内核拷贝脚本上。

目标文件: /Workspace/oh61/device/board/spacemit/musepaper2/kernel/build_kernel.sh

原脚本逻辑:

function copy_kernel(){
    rm -rf ${KERNEL_BUILD_ROOT}
    mkdir -p ${OHOS_SOURCE_ROOT}/out/kernel/OBJ
    echo cp -rL ${KERNEL_SOURCE_DIR} ${KERNEL_BUILD_ROOT}
    cp -rL ${KERNEL_SOURCE_DIR} ${KERNEL_BUILD_ROOT}
    cp -rf ${OHOS_SOURCE_ROOT}/device/board/${DEVICE_BOARD}/common/kernel_logo/kernel_logo_spacemit_0.ppm ${KERNEL_BUILD_ROOT}/drivers/video/logo/logo_linux_clut224.ppm
}

原脚本使用了 cp -rL 命令。-L 参数会强制解引用所有的符号链接。在庞大的 Linux 内核源码树中,存在大量的跨目录架构级符号链接,使用 -L 会导致目录无限展开并陷入循环死锁。

我将其修改为:

function copy_kernel(){
    rm -rf ${KERNEL_BUILD_ROOT}
    mkdir -p ${OHOS_SOURCE_ROOT}/out/kernel/OBJ
    echo cp -r ${KERNEL_SOURCE_DIR} ${KERNEL_BUILD_ROOT}
    cp -r ${KERNEL_SOURCE_DIR} ${KERNEL_BUILD_ROOT}
    cp -rf ${OHOS_SOURCE_ROOT}/device/board/${DEVICE_BOARD}/common/kernel_logo/kernel_logo_spacemit_0.ppm ${KERNEL_BUILD_ROOT}/drivers/video/logo/logo_linux_clut224.ppm
}

去掉 -L 后,仅执行常规递归拷贝,成功解决了编译卡死的问题。

5. 失败结果总结

尽管上述所有步骤都顺利通过了全量编译并成功生成了镜像包,但在烧录实测后,该方案被证明是失败的。

问题在于,强行在内核源码中阻断 spacemit-pwrseq.c 的状态流转,虽然维持了物理层面的供电,但破坏了内核硬件状态机的一致性。导致原本应该与 WiFi 驱动协同加载的其他总线控制逻辑产生时序错乱。开机后,尽管 ps -ef | grep wpa 能看到进程,但底层的 8852bs 驱动已经因为初始化的电压波动进入了僵死状态。

这种对内核级电源逻辑的直接暴力切断,在复杂的异构系统上引发了连锁反应,并不能作为稳定的工程解决方案。

第二部分:基于系统服务的状态重置机制)

在直接修改内核源码失败后,我转变了思路。既然在极早期的系统上电阶段不可避免地会发生电源闪断,导致提前加载的内核模块处于僵死状态。那么,我可以通过操作系统的用户态初始化子系统(Init System),在系统启动完成、电源彻底稳定之后,执行一次环境清理与驱动重载。

具体方案是编写一个定制的 Shell 脚本,并利用 OpenHarmony 的 .cfg 配置文件将其注册为开机任务。

1. 编写环境重置与驱动重载脚本

首先,需要在源码指定的配置路径下创建一个处理驱动状态的脚本文件。

脚本路径: device/board/spacemit/musepaper2/cfg/wifi_fix.sh

脚本的执行逻辑分为四步:解绑冲突节点、延时等待、清理僵死内存、重新挂载驱动。完整内容如下:

#!/bin/sh
# 1. 杀死蓝牙进程并解绑驱动 (确保电源稳住)
echo "rf-pwrseq:bt-pwrseq" > /sys/bus/platform/drivers/spacemit-bt/unbind

# 2. 等待电源稳定 (可选)
sleep 1

# 3. 卸载旧驱动
rmmod 8852bs 2>/dev/null

# 4. 重新注入 WiFi 灵魂
insmod /vendor/modules/8852bs.ko

逻辑深度解析:

  • echo ... > unbind:这是 Linux sysfs 文件系统提供的一种标准机制。通过向指定的 unbind 节点写入设备名称,可以强制内核从该设备上解绑 spacemit-bt 驱动程序。这在运行态动态切断了蓝牙驱动对电源控制器的干扰,使得底层供电停止闪断,进入稳态。

  • rmmod 8852bs:由于开机过程中的高频断电,此时内核内存中驻留的 8852bs 驱动状态已经失效。使用该命令将其从内核空间强制移除。2>/dev/null 用于屏蔽模块不存在时的标准错误输出。

  • insmod /vendor/modules/8852bs.ko:在电源确认稳定且旧模块清理完毕的安全环境下,从设备的 /vendor 分区重新读取并加载原始的内核模块对象。这一步确保驱动能够执行一次完整的、干净的初始化过程。

2. 将脚本注册为 OpenHarmony 开机任务

脚本编写完成后,需要确保系统开机时能以管理员权限自动执行。这需要修改 OpenHarmony 的初始化配置文件。

目标文件: device/board/spacemit/musepaper2/cfg/init.musepaper2.cfg

在配置文件的 services JSON 数组中,我追加了以下服务声明:

{
    "name": "wifi_fix_service",
    "path": ["/vendor/bin/wifi_fix.sh"],
    "uid": "root",
    "gid": "root",
    "once": 1,
    "bootevent": "boot_finished"
}

参数解析:

  • name:定义了系统内部分配给该任务的服务标识符。

  • path:定义了系统执行的绝对路径及参数。注意这里的路径是最终固件打包后在设备文件系统中的挂载位置(/vendor/bin/),而不是当前源码环境的路径。

  • uidgid:指定该任务以 root 用户组的最高权限执行,因为涉及操作 /sys 驱动节点和内核模块的装载,需要系统级特权。

  • once: 设置为 1,指示 init 进程该任务只需启动一次,执行完毕后即可退出,不需要像常规守护进程那样在崩溃后重启。

  • bootevent: 设置为 boot_finished,这是极为关键的一步。它指示系统只有在所有的基础文件系统挂载完成、核心框架启动完毕的最后阶段,才触发此服务。这保证了在执行驱动重载时,相关的依赖路径已经完全可用。

3. 配置打包规则(BUILD.gn)

为了让编译系统读取源码目录下的 wifi_fix.sh 并将其放入系统镜像的 /vendor/bin/ 目录,需要定义 GN 构建规则。

目标文件: device/board/spacemit/musepaper2/BUILD.gn

我在该文件中新增了一个 ohos_prebuilt_etc 构建目标:

ohos_prebuilt_etc("wifi_fix_script") {
    source = "cfg/wifi_fix.sh"
    module_install_dir = "bin" # 指定放入 /vendor/bin/ 目录下
    install_images = [ "vendor" ]
}

这段配置指示 Ninja 编译器将工程下的脚本文件作为预编译产物直接输出,并指定部署到 vendor 分区的 bin 子目录中。

4. 将服务加入全量编译列表

最后,需要将上述创建的构建目标引入到具体产品的构建上下文中。

目标文件: device/board/spacemit/musepaper2/ohos.build

module_list 数组中,我注册了该模块名称:

{
  "parts": {
    "device_musepaper2": {
      "module_list": [
        "//device/board/spacemit/musepaper2:wifi_fix_script"
      ]
    }
  }
}

这使得整个构建流水线在处理 device_musepaper2 部件时,会自动触发 wifi_fix_script 的打包流程。

5. 验证

采用第二套方案后,我重新执行了 ./build.sh 全量编译并将生成的固件刷入 MusePaper2 开发板。开发板启动后,我通过串口终端观察系统的行为。发现仍然失败。

所有我转变了思路,使用了一种更简单有效的方式

理解源码修改的第一步,是思维模式的转换。

在板子上敲命令,就像是拿到了一套盖好的毛坯房,发现没水没电,于是自己找管子、接电线,勉强能住。但是一旦开发板被重新烧录,房子被推倒重来,一切配置又要从零开始。

而修改源码,是去修改这栋楼的建筑图纸。我要在图纸上标明:“以后出厂的每一套房子,必须自带 wpa_supplicant 这个家具,必须把网络配置文件放在指定抽屉里,且一通电必须自动把网连上”。

按照这个思路,我将本次源码固化分为了三个关键战役。

战役一:组件注入 —— 让系统出厂自带“工具箱”

之前我们在 /vendor/bin/ 下找不到 WiFi 工具,是因为编译系统默认没有把它们打包进去。我们需要在板子的配置清单里显式声明。

我定位到了开发板的核心配置文件 device/board/spacemit/musepaper2/ohos.build 和具体的编译逻辑文件 BUILD.gn

ohos.build 中,我在 spacemit_productsmodule_list 末尾加入了依赖:

"module_list": [
    ...
    "//third_party/wpa_supplicant:wpa_supplicant",
    "//third_party/wpa_supplicant:wpa_cli",
    "//vendor/spacemit/musepaper2/wifi_config:wifi_config_file"
]

并在 BUILD.gnmusepaper2_groupdeps 中做了同样的补充。 思考: OpenHarmony 的 gn/ninja 编译系统是高度模块化的。通过修改这两处,等于告诉编译服务器:“请在下一次构建镜像时,把这些第三方工具和我的自定义配置一同打包进 /vendor/ 分区”。

战役二:配置持久化 —— “无中生有”的说明书

工具进去了,还需要配置文件。之前在板子上我是用 echo 临时写的 wpa_supplicant.conf。在源码层面,我需要在 vendor/spacemit/musepaper2/ 目录下新建一个 wifi_config 文件夹,并创建这个 .conf 文件。

但仅仅放一个文件在那儿,编译器是不会理睬的。必须配上专门的打包规则。我编写了一个 BUILD.gn

import("//build/ohos.gni")

ohos_prebuilt_etc("wifi_config_file") {
  source = "wpa_supplicant.conf"
  module_install_dir = "etc/wifi"
  part_name = "spacemit_products"
}

这短短几行代码非常优雅地解决了一个底层问题:使用 ohos_prebuilt_etc 模板,系统会在编译时自动将我的配置文件挪到镜像的 /vendor/etc/wifi/ 目录下,实现了真正的“出厂预置”。

战役三:开机自启逻辑 —— 雇佣一个“智能管家”

工具和配置都有了,最后也是最关键的一步:系统重启后,谁来拉起这个服务?

在 OpenHarmony 中,系统启动由 init 进程接管,它的行为完全由 .cfg 文件定义。我找到了 device/board/spacemit/musepaper2/cfg/init.musepaper2.cfg 这个“开机剧本”。

这里需要分两步走: 1. 文件系统准备(fs 阶段): WiFi 服务启动前需要套接字目录。我在 jobsfs 阶段加入了目录创建和权限分配指令:

"cmds" : [
    ...
    "mkdir /data/service/el1/public/wifi 0771 wifi wifi",
    "mkdir /data/service/el1/public/wifi/wpa_supplicant 0771 wifi wifi",
    "mkdir /data/service/el1/public/wifi/sockets 0771 wifi wifi"
]

注意这里的 wifi wifi 权限设定。Linux 的权限管理是严苛的,不赋予正确的用户组,服务根本跑不起来。

2. 注册常驻服务(services 阶段): 接着,我在剧本的 services 数组中添加了 WiFi 守护进程:

{
    "name" : "wpa_supplicant",
    "path" : ["/vendor/bin/wpa_supplicant", "-B", "-i", "wlan0", "-c", "/vendor/etc/wifi/wpa_supplicant.conf"],
    "uid": "wifi",
    "gid": ["wifi", "system"],
    "secon": "u:r:wpa_supplicant:s0"
}

这段配置定义了进程的启动路径、依赖的配置文件、运行的 UID/GID,以及非常重要的 SELinux 安全上下文(secon)。

交付闭环:项目团队的协作之道

完成上述所有文件的备份、修改与本地代码审查后,我将这 5 个关键文件的绝对路径和作用整理成了一份标准的《补丁合并指南》,打包发送给了负责主线代码整合的队友 。

虽然我们的完整镜像编译目前还在不断调优中,但可以确定的是,只要这份源码补丁合入主线,底层网络的基建就算彻底扫清了障碍。

总结与感悟: 从应用层开发转向底层系统移植,最大的挑战在于没有 IDE 惯着你。所有的错误都在冷冰冰的日志里,所有的配置都要遵循严苛的系统生命周期。但当你理清了 BUILD.gn 的依赖关系,看懂了 init.cfg 的启动时序,看着自己写的一行行脚本随着固件烧录,赋予了一块冷冰冰的电路板连接世界的能力时,那种属于程序员的极致快乐,是难以言表的。

接下来,有了稳定的网络支撑,我终于可以放开手脚,去推进我们项目实训的重头戏——3D 虚拟陪伴助手的研发了!期待在后续的博客中与大家分享应用层的实战经验。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐