鸿蒙南向开发环境搭建全链路:从源码同步到编译烧录避坑指南

发布时间:2026/11/2 5:20:33
鸿蒙南向开发环境搭建全链路:从源码同步到编译烧录避坑指南

鸿蒙南向环境搭建这件事,我前后在三台机器上从零做过一遍,第一次折腾了整整两天,第二次半天,第三次两小时——省下来的时间全是被报错教育出来的。这里说的南向,是面向设备侧的那一半:内核、驱动、硬件适配、系统镜像,跟写页面的北向开发完全是两套东西。南向环境搭建的链路特别长,从操作系统版本、依赖包、Python 版本,到源码同步、编译配置、镜像烧录,中间任何一环崩了,最后表现往往都是"编译不过"或者"板子没反应",新手很难判断问题到底出在哪一层。这篇把鸿蒙南向环境搭建里高频出现的问题按链路顺序摊开,从主机准备一路讲到板子上电,包括报错原文、排查思路和我实际验证过的解法。适合两类人看:一是刚接手设备侧开发、准备把源码跑通的新人,二是已经能编译但总在某个环节卡住的同行。所有步骤都是可复现的,参数和命令可以直接抄。

1. 先把南向环境这件事讲透:你到底在搭什么

1.1 南向和北向的分界线在哪

很多人第一次接触鸿蒙开发,脑子里只有一个印象:写页面、装 DevEco Studio、跑个 Hello World。那套流程属于北向,产物是应用包,跑在别人做好的系统上。南向完全反过来,你要交付的是那个"别人做好的系统"本身——内核配置、驱动适配、系统服务裁剪、最终打包成可以烧进硬件的镜像。

这个区别决定了环境搭建的复杂度差了一个量级。北向装个 IDE 就完事,IDE 里自带 SDK 和模拟器;南向必须在 Linux 主机上拉取几十个仓库的源码,编译出交叉工具链能识别的产物,再通过烧录工具写进具体开发板的存储介质。中间任何一层不匹配,比如主机 GCC 版本太新、Python 版本不对、磁盘空间不够,都不会给你友好提示,只会甩一串 C++ 模板报错或者No space left on device。

从我踩过的坑来看,南向环境的问题有个很明显的规律:90% 的失败不是鸿蒙本身的问题,而是主机环境的问题。Ubuntu 版本太新、依赖包少装、路径带中文、文件放在共享目录、Python 默认版本不对——这五类基本能覆盖一半以上的求助。所以真正该做的不是背命令,而是先把主机这一层夯实。

1.2 一套南向环境的最小闭环

把整条链路拆开,其实只有五个环节,缺一个都跑不起来:

  1. 主机系统——跑编译的 Linux 环境,通常用 Ubuntu,虚拟机和物理机都可以。
  2. 基础工具链——git、repo、Python、gn、ninja、交叉编译工具链(一般由源码里的 prebuilts 提供,不需要自己编)。
  3. 源码——通过 repo 从远端拉取整套工程,包含内核、驱动框架、系统服务和构建脚本。
  4. 构建系统——hb 命令行工具驱动 gn 生成构建文件,再调用 ninja 执行实际编译,最终在 out 目录产出镜像。
  5. 烧录与调试——通过烧录工具把镜像写进板子,再用串口和 hdc 做运行期调试。

这五环的依赖关系是串行的,前一环不通,后面全部无意义。所以排查顺序也应该是这个顺序,不要跳步。我见过有人一上来就怀疑驱动写错了,结果发现是源码根本没同步完整,编译出来的镜像压根没包含他的改动。这种方向性错误最消耗时间。

提示:每完成一个环节,都做一个明确的验证动作。比如装完依赖就跑一次gcc -v和python3 --version,同步完源码就检查顶层目录是否存在build、kernel、drivers这几个关键目录,编译完就ls out/看产物。验证点越前置,排查成本越低。

1.3 为什么问题总集中在四个地方

把社区里高频提问过一遍,会发现报错扎堆出现在四个位置:环境准备、源码同步、编译构建、烧录调试。原因不复杂——这四处都有"隐式前置条件"。

环境准备阶段的隐式条件是依赖包和版本,缺一个 lib 就会在半小时后的链接阶段才炸出来,报错位置和真实原因完全脱节。源码同步阶段的隐式条件是网络稳定性和磁盘空间,repo 是批量拉取几十个仓库,中途断一个,整个工作区就处于半残状态,而 repo 不会每次都告诉你哪个仓库没拉全。编译构建阶段的隐式条件是产品配置和工具链路径,选错产品型号,编译能过但产物根本不是你要的。烧录调试阶段的隐式条件是硬件链路,USB 线、串口线、供电、BOOT 引脚状态,任何一项不对都是"没反应"。

理解了这层,排查心态会稳很多:遇到报错先问"这是哪一环的隐式条件没满足",而不是"鸿蒙是不是有 bug"。带着这个思路往下看,很多坑其实是同一个坑的不同表现。

2. 主机环境准备:从系统版本到依赖包

2.1 Ubuntu 版本选择与磁盘内存门槛

系统版本是第一个岔路口。我的建议很明确:用 Ubuntu 20.04 LTS 或 22.04 LTS,优先 20.04。理由不是 20.04 更先进,而是大量构建脚本和预编译工具链是在这个版本上验证过的,GCC 版本、glibc 版本、Python 默认版本都在预期范围内。

版本选太新的麻烦很具体。比如 24.04 默认 GCC 版本更高,编译某些内核模块或者旧版构建脚本时会报unrecognized command-line option或者更严格的语法检查错误,这类问题搜出来答案很少,因为踩的人不多。反过来选 16.04 也不行,Python 默认版本太低,很多构建脚本要求 Python 3.8 以上。

硬件门槛这一项经常被低估。轻量级设备(就是那种只有几百 KB 内存、跑轻内核的模组)源码规模小,50 GB 磁盘、8 GB 内存勉强够;但标准系统(带 Linux 内核、图形、应用框架的那类)源码拉全之后,光工作区就可能上百 GB,加上编译中间产物,建议直接给 200 GB 以上磁盘、16 GB 内存。

虚拟机的坑要单独说。如果宿主机内存只有 16 GB,给虚拟机分 12 GB 是合理的,但一定要开交换分区,否则链接阶段几个并行编译任务会直接把内存打满,进程被 OOM Killer 杀掉,现象是编译到某一步突然"Killed",没有任何错误信息。我第一次遇到这个情况,反复重编三次都卡在同一位置,最后dmesg | tail才看到是内存不够被系统杀掉的。

还有一个反直觉的点:编译耗时对 CPU 核心数非常敏感。8 核大概比 4 核快一倍多,因为 ninja 会按核心数并行。如果你在用笔记本,插上电源再编译,很多机器电池模式下会降频,编译时间能差出 40%。

2.2 依赖包清单与逐条解释

依赖包是最容易"少装一两个"的地方。下面这份清单是按常见构建需求整理的,建议一次性装完,别等报错了再补:

sudo apt-get update sudo apt-get install -y \ build-essential gcc g++ make cmake ninja-build pkg-config \ git git-lfs curl wget unzip zip tar rsync \ python3 python3-pip python3-venv python3-setuptools \ bc cpio flex bison libssl-dev libelf-dev \ libncurses5-dev libncursesw5-dev libxml2-utils \ device-tree-compiler scons \ dosfstools mtools mtd-utils \ liblz4-tool libgl1-mesa-dev libxi-dev libx11-dev \ texinfo libc6-dev-i386 gperf

逐条说几个容易被认为"没必要"但实际很关键的:

  • git-lfs:不少仓库里的大文件(预编译二进制、工具链)是用 LFS 管理的,不装的话拉下来是一堆指针文件,体积只有几百字节,编译时才会提示某个工具"不存在"或者"不是可执行文件"。
  • device-tree-compiler:设备树编译工具,做硬件适配必须有,缺了会报dtc: command not found。
  • libncurses5-dev:内核menuconfig依赖它,缺了进不去配置菜单。
  • libc6-dev-i386:32 位兼容库,某些交叉编译工具链是 32 位的,缺了会报No such file or directory却找不到到底缺什么。
  • dosfstools/mtools/mtd-utils:打包镜像文件系统时用,缺了在制作镜像的最后一步失败。
  • libssl-dev:签名、哈希相关工具依赖,有些构建脚本会用它生成校验信息。

注意:装完依赖别急着往下走,先跑sudo apt-get install -f修一下可能的依赖冲突。我遇到过一次是之前装过某个旧版本库,导致新装的包处于 half-configured 状态,后面 cmake 配置阶段莫名其妙失败。

2.3 Python 与 Node 版本共存的老问题

Python 这块的坑非常集中,几乎每个新手都会撞一次。

第一个坑是python命令不存在。Ubuntu 20.04 之后默认只提供python3,而某些构建脚本里写死了#!/usr/bin/env python,执行时报bad interpreter: No such file or directory。解决办法不是改脚本,而是给个软链:

sudo ln -s /usr/bin/python3 /usr/bin/python

这行命令我建议在装完系统就做掉,能省掉后面好几次莫名其妙的失败。

第二个坑是多个 Python 版本打架。系统自带一个 Python 3,可能又用 conda 装了一个,pip 装的包不知道落到哪个环境里。典型现象是pip install xxx显示成功,但脚本运行时报ModuleNotFoundError。处理办法是统一用python3 -m pip的方式安装,确保 pip 和解释器是同一个:

python3 -m pip install --user requests pyyaml pycryptodome

第三个坑是 Node.js。部分构建流程(尤其是涉及前端资源、打包工具的)需要特定版本的 Node,版本太高或太低都会报语法错误。稳妥做法是用 nvm 管理,装一个经过验证的 LTS 版本,比如 16.x,不要图新鲜装最新的。

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash source ~/.bashrc nvm install 16 nvm use 16

这里有个很隐蔽的顺序问题:nvm 装的 Node 只对当前用户生效,如果你后来用sudo跑构建脚本,环境变量里的 nvm 路径就丢了,会 fallback 到系统 Node 或者直接找不到。原则是:整个构建链路要么全用普通用户,要么全用 root,不要混着来。

2.4 路径、权限、共享目录这三件小事

这三件事单独看都是小事,凑一起能让编译失败得毫无线索。

路径不要带中文和空格。编译工具链里有大量路径拼接,遇到空格会被截断。工作区就放在/home/用户名/下面,或者干脆/opt/workspace,别搞~/我的项目/鸿蒙 源码/这种。

不要放在 Windows 共享目录里。用虚拟机的人特别容易犯这个错:图方便把源码放在 VMware 或 VirtualBox 的共享文件夹里,宿主机上还能用编辑器直接改。问题是共享目录(vboxsf、vmhgfs 之类)不支持 Unix 权限位和软链接,而源码里有大量软链接和可执行脚本。表现是Permission denied、Too many levels of symbolic links、脚本没执行权限。这条我踩过,排查了整整一个下午,最后换到虚拟机内部磁盘就全好了。

不要用 root 编译又用普通用户改文件。混用导致out/目录里一部分文件属主是 root,普通用户没权限覆盖,编译时报Permission denied而且路径指向一个看起来完全正常的文件。解决方式是sudo chown -R $USER:$USER 工作区路径,然后统一用普通用户编译。也别图省事整个流程挂sudo,交叉工具链和构建脚本对权限很敏感。

磁盘文件系统也要留意。ext4 是默认且最稳的;如果你把工作区放在 NTFS 挂载分区上,大小写敏感性和权限模型都不同,会出现"文件明明存在但编译器说找不到",因为大小写不匹配在 NTFS 上不报错,在 Linux 工具链里就是致命问题。

3. 源码获取:repo 同步阶段的典型故障

3.1 repo 和 git 的分工,别搞混

很多人第一次看到repo init和repo sync会懵:不是有 git 吗,为什么还要 repo?

简单类比:如果整个工程是一栋楼,git 是管理单个房间的工具,repo 是管理整栋楼的清单。鸿蒙南向工程的源码由几十上百个 git 仓库组成,每个仓库有自己的版本和提交记录。手动一个个 clone 不现实,所以用一个 manifest 文件(也是一份 XML 清单)记录"哪个仓库、哪个分支、放在哪个目录",repo 读这份清单批量操作。

理解了这一点,就能理解为什么 repo 的报错往往和 git 不一样:repo 层面的问题是"清单拉不到""某个仓库同步失败";git 层面的问题是"某个仓库本地有改动导致冲突"。

初始化命令大致是这样:

mkdir ~/oh_workspace && cd ~/oh_workspace repo init -u <manifest仓库地址> -b <分支名> --no-repo-verify repo sync -c -j8 --force-sync --no-clone-bundle --no-tags

几个参数值得解释,别抄完不知道为什么:

  • -c:只拉当前分支,不拉所有分支。不加这个参数,同步体积能翻好几倍。
  • -j8:并发数,一般设成 CPU 核心数。设太大容易触发远端限流,反而更慢。
  • --force-sync:强制覆盖本地改动。第一次同步用不上,但如果你之前同步到一半失败了,再同步时某些仓库状态不一致,加这个能强制对齐。
  • --no-clone-bundle:跳过打包下载优化。网络条件一般时加上更稳,能避免 bundle 下载中断导致的奇怪错误。

3.2 目录结构速览与"同步完整"的判断标准

同步完成后,顶层目录通常能看到这些关键部分:

目录/文件作用同步缺失后的表现
build/构建脚本、hb 工具源码、产品配置hb: command not found、产品列表为空
kernel/内核源码(轻内核或 Linux 内核)编译到内核阶段报路径不存在
drivers/驱动框架与外设驱动驱动相关目标找不到
base/系统基础服务链接阶段大量 undefined reference
third_party/三方库编译中途缺头文件
prebuilts/预编译工具链、编译器等报clang: not found、gn: not found
vendor/厂商/单板适配代码hb set里看不到自己的板子

判断同步是否完整,别只看repo sync有没有报错,要看三个东西:顶层目录数量对不对、prebuilts里工具链能不能执行、.repo目录大小是否合理。我有一次同步到 98% 断了,repo 只提示了某个仓库失败,整体看起来"成功",结果prebuilts里少了交叉编译器,编译直接卡在第一步。

验证工具链能不能用:

ls prebuilts/clang/linux-x86/bin/ 2>/dev/null | head ./prebuilts/build-tools/linux-x86/bin/ninja --version

如果这两条命令报"没有那个文件或目录",说明同步不完整,别往下走,重新repo sync补齐。

3.3 同步失败的几种典型表现与处理

表现一:卡在某个仓库很久不动。通常是网络到该仓库的连接有问题。处理方式不是干等,而是 Ctrl+C 之后单独同步那个仓库:

repo sync -c <仓库路径> --force-sync

<仓库路径>从.repo/manifest.xml或者报错信息里能找到。

表现二:error.GitError: xxx revs/heads/xxx not found。说明 manifest 指定的分支在远端不存在,多半是分支名写错了,或者该分支已经被合并删除。检查repo init时-b后面跟的分支名,去远端仓库页面确认一下分支是否还在。

表现三:fatal: unable to access ... Could not resolve host。DNS 或网络代理问题。这时候要去检查主机的 DNS 配置,ping一下域名看能不能解析。如果之前配过全局代理,把环境变量清理干净再试,残留的http_proxy会让 git 走上一条不存在的通路。

表现四:磁盘满。报错可能是No space left on device,也可能更隐蔽,比如 git 写入中途失败导致仓库损坏。建议在同步前df -h看一眼剩余空间,标准系统源码建议留 200 GB 以上。

表现五:repo 自身版本太旧。repo: command not found或者执行时报语法错误,通常是 repo 脚本太老。重新下载一份即可:

mkdir -p ~/bin curl https://storage.googleapis.com/git-repo-downloads/repo > ~/bin/repo chmod a+x ~/bin/repo export PATH=~/bin:$PATH

注意 repo 下载地址在国内经常不可达,如果这条命令长时间无响应,就要想办法从能访问的地方拿一份 repo 脚本,别在这上面耗时间。

4. 编译构建:hb、gn、ninja 三者到底怎么配合

4.1 一次完整编译的命令序列

编译这块,先搞清楚三个工具的分工,报错才好定位。

  • gn:生成构建文件。读.gn、.gni和BUILD.gn,产出 ninja 能识别的build.ninja。
  • ninja:执行构建。按依赖关系并行编译,速度快。
  • hb:鸿蒙自己的命令行封装,负责选产品、调 gn、调 ninja、把产物放到out/下。

所以 hb 本身不编译,它是个调度器。理解了这点,hb build失败的时候你就知道该去看 gn 阶段的错还是 ninja 阶段的错。

标准流程:

# 1. 安装 hb(两种方式,选一种) python3 -m pip install --user build/hb # 或者 ./build/build_pycache.sh source build/envsetup.sh # 2. 选择产品 hb set # 交互式列表里用方向键选,回车确认 # 3. 全量编译 hb build -f # 4. 增量编译(改了少量文件后) hb build

hb set这一步是整个编译最关键的决策点。它会读vendor/和productdefine/下的产品定义,列出所有可选产品。选错产品的后果是编译能过,但产物不是你要的,烧进去板子跑不起来或者功能缺失,而且很难联想到是产品选错。判断依据是看你手里的板子属于哪一类:轻量设备(MCU 类、内存几百 KB)选轻量产品,标准设备(带完整 Linux 内核)选标准产品。

选完之后会生成一个ohos_config.json之类的配置文件,记录当前选中的产品和板子。想看当前选的是什么,直接hb show或者打开那个文件看。

4.2 产品配置为什么能编译出不同的东西

产品配置本质是一组开关。同一份源码,通过不同的配置文件组合,能编译出完全不同的系统:裁剪掉图形子系统、去掉某些服务、换一个内核、换一套驱动。

以常见的配置文件分布来看:

  • productdefine/common/products/下是产品级配置,决定这个产品包含哪些子系统。
  • productdefine/common/device/下是板级配置,决定芯片平台、内存布局、外设使能。
  • vendor/下是厂商适配代码,包含实际的驱动实现和板级初始化。

这三者的关系是:产品决定"要什么功能",板子决定"跑在什么硬件上",厂商代码决定"怎么操作硬件"。三者必须对齐,否则就会出现"驱动编进去了但没被调用"或者"功能开关打开了但底层没实现"。

有个常见的困惑:为什么我在vendor/下改了驱动代码,编译过了但板上行为没变?大概率是产品配置里没把对应组件打开,或者你改的目录没被任何BUILD.gn引用。gn 的构建是严格按BUILD.gn声明的依赖来的,没被引用的文件会被完全忽略——这跟很多传统构建系统"把目录里所有 .c 都编一遍"的直觉不同。验证方法是看编译日志里有没有出现你的文件名,或者在目标文件目录里找.o文件。

# 确认某个文件是否参与了编译 find out/ -name "你的文件名.o" 2>/dev/null

如果找不到,说明它压根没进构建图,改再多也没用。

4.3 编译报错逐条拆解

编译阶段的报错可以按出现时机分成三类,每类的处理思路完全不同。

第一类:gn 生成阶段失败。表现为hb build刚开始就退出,报gn gen相关错误。

报错片段真实原因处理方式
product not found产品名或路径不对,hb set选的产品在配置里不存在检查productdefine下产品目录名,重新hb set
Unable to find .gn file工作目录不对,不在源码根目录cd到源码根,重试
Duplicate target name两个BUILD.gn定义了同名 target检查新增的构建文件,改名
assertion failed且指向某个.gni配置变量缺失或类型不对看.gni里那条 assert 的变量,补上或改正

gn 的报错定位其实很准,行号和文件都给了,耐心读那条 assert 的条件,基本都能明白要什么。

第二类:编译阶段失败。进入 ninja 后,报 C/C++ 编译错误。

最常见的三种:

  • fatal error: xxx.h: No such file or directory——头文件找不到。可能是缺少deps声明,也可能是你引用了另一个子系统的头文件但没在BUILD.gn里加public_deps。
  • undefined reference to 'xxx'——链接阶段符号找不到。典型原因是只声明了没实现,或者实现所在的库没加进deps。
  • error: 'xxx' was not declared in this scope——语法或版本问题。有一种容易误判的情况:代码本身没问题,但你引用了新版本才有的接口,而当前源码分支比较旧。确认一下接口在你这版源码里是否存在。

第三类:链接后处理失败。编译都过了,最后打镜像时报错。

  • xxx.img exceeds partition size——镜像超出分区大小。产品配置里的分区表要改,或者裁剪掉一些组件。
  • No space left on device——主机磁盘满了,去df -h看out/所在分区。
  • sign failed——签名相关工具或密钥路径不对,检查签名配置。

我印象最深的一次编译失败,报的是某个驱动文件undefined reference,但那个文件明明存在且已经编译出.o了。最后发现是 gn 里把源文件加进了sources,但没加进include_dirs的传递链,导致另一个模块看不到这个符号。这种问题看报错信息根本推不出来,只能顺着BUILD.gn的依赖链一层层往上捋。

4.4 增量编译的陷阱与清理策略

hb build不带-f是增量编译,快是快,但会保留旧的目标文件。改动了BUILD.gn、改了产品配置、换了编译选项之后,增量编译很可能产出不一致的结果——你以为改了,其实用的还是旧的.o。

我的经验规则很简单:只要动了构建脚本、产品配置、编译宏、头文件目录结构,就hb build -f全量编一次。纯改.c/.cpp的内容,增量就行。头文件改动的风险中等,因为 ninja 有依赖追踪,但跨模块的头文件改动偶尔会漏,改了公共头文件也建议全量。

彻底清理:

# 只清产物,保留配置 rm -rf out/ # 重新选产品后编译 hb set hb build -f

注意:别用hb clean之后不重新hb set就编译,某些版本下ohos_config.json会被一起清掉,导致编译时找不到产品,报一个和当前操作完全不相关的错误。

5. 烧录与板端调试:最后一百米最容易翻车

5.1 烧录链路拆开看:每个断点在哪

编译成功之后,out/<board>/<product>/下会出现一组镜像文件,通常包括引导程序、分区表、内核、系统镜像、文件系统。烧录就是把这些按分区地址写进板子的存储介质。

整条链路有五个断点,任何一环不通都表现为"烧录失败"或者"烧完没反应":

  1. 主机识别不到设备——USB 线、USB 口、驱动、权限。
  2. 烧录工具找不到镜像——路径配错、镜像名和配置不匹配。
  3. 分区表与镜像不匹配——镜像比分区大,或者分区地址写错。
  4. 写入过程失败——线材质量、供电不足、传输中断。
  5. 写入成功但启动失败——BOOT 引脚状态、镜像本身有问题、串口没接对。

Linux 下第一个断点最常见的原因是串口权限。普通用户不在dialout组里,访问/dev/ttyUSB0会报Permission denied。

sudo usermod -aG dialout $USER # 重新登录生效,或者临时用 newgrp newgrp dialout

USB 转串口芯片的驱动也要确认,lsusb能看到设备但/dev/ttyUSB*不存在,就是驱动没加载或者芯片型号不被当前内核支持。

5.2 hdc 与串口:两种调试通道各管什么

板子起来之后,两条调试通道的分工要清楚。

串口是底层通道,从上电那一刻就有输出,能看到引导日志、内核日志、崩溃现场。参数通常是 115200 波特率、8 数据位、无校验、1 停止位、无流控(8N1)。但不是所有板子都是 115200,有的用 1500000,接上去全是乱码就是波特率不对。乱码先试波特率,别怀疑板子坏了。

hdc是上层通道,需要系统起来、服务跑通之后才能用,能传文件、执行命令、抓日志。

hdc list targets # 看设备是否连上 hdc shell # 进设备 shell hdc file send 本地 远端 # 推文件 hdc hilog # 抓日志

hdc list targets返回空是最常见的问题。排查顺序:

  1. 板子是否真的启动了系统(串口有没有正常输出)。
  2. USB 线是不是只充电的数据线(这个坑极常见,换一根确认能传数据的线)。
  3. hdc 服务是不是卡死了。可以杀掉重启:
hdc kill hdc start
  1. 端口冲突。hdc 服务默认占用 8710 端口,如果这个端口被别的进程占了,服务起不来。
lsof -i:8710 # 或者 netstat -tunlp | grep 8710

有冲突就换端口,通过环境变量指定一个别的。

5.3 上电没反应:一份可执行的排查顺序

"烧录成功但板子没反应"是最让人抓狂的情况,因为没有任何报错。我整理了一套按成本从低到高的排查顺序,基本能覆盖绝大多数情况:

  1. 确认供电。用万用表量一下板子供电电压,很多模组要求 3.3V 或者 5V,供电不足会导致上电即复位。USB 口供电能力有限,带外设的板子容易供电不足。
  2. 确认串口接线。TX 接 RX、RX 接 TX,共地。接反了完全没输出,而且不会报错。这是我自己犯过的错,排查了两小时。
  3. 确认波特率。有输出但是乱码,99% 是波特率不对。
  4. 确认 BOOT 引脚。烧录模式和运行模式的引脚状态不同,烧完没切回来就启动不了。有些板子是拨码开关,有些是跳线帽,烧录前看一眼。
  5. 确认烧录地址。分区表里的起始地址和镜像类型必须对应,把文件系统镜像写到引导区,板子当然起不来。
  6. 确认镜像版本匹配。编译用的产品配置和实际硬件不匹配,比如按 A 板编的镜像烧到 B 板上。这类问题串口一般会有硬件初始化失败的日志。
  7. 确认存储介质状态。有些板子的 Flash 被上一次异常烧录写坏了分区表,需要先整片擦除再烧。

提示:排查硬件问题时,一次只改一个变量。同时换线、换板、换镜像,最后即使好了也不知道是哪一项修好的,下次遇到还是不会。

5.4 串口日志的读法

串口日志信息量大,但只看几个关键位置就够了:

  • 最开始几行:引导程序版本、芯片型号、存储容量。这里能立刻发现"镜像和硬件不匹配"。
  • 内核启动阶段:内存初始化、驱动加载。probe failed、timeout这类字样定位驱动问题。
  • 服务启动阶段:哪个服务起不来、报什么错。
  • 崩溃现场:如果出现异常,会打印调用栈,虽然地址是十六进制的,但函数名通常在,能大致定位。

一个实用技巧:把串口日志重定向到文件,方便事后检索。

# 用 screen 的时候开启日志 screen -L /dev/ttyUSB0 115200 # 或者用 minicom 开启捕获 minicom -D /dev/ttyUSB0 -b 115200 -C boot.log

日志文件里搜error、fail、timeout,往往能直接命中问题点。

6. 高频问题速查表与个人避坑心得

6.1 一张表搞定八成报错

把前面散落的经验集中成一张速查表,出问题时先在这里找一遍,能省不少时间。

现象最可能原因快速验证处理
hb: command not foundhb 未安装或 PATH 未生效which hb重新pip install --user build/hb,确认~/.local/bin在 PATH 里
python: not found只装了 python3ls /usr/bin/python*软链python到python3
repo sync卡住不动单个仓库网络不通看卡在哪个仓库路径单独repo sync <路径>
编译到某步突然Killed内存不足被系统杀掉dmesg | tail减少并行数、加交换分区、加大内存
No space left on device磁盘满df -h清理out/、扩磁盘
xxx.h: No such file依赖没声明或路径没加全局搜头文件实际位置补deps或include_dirs
undefined reference实现没进构建或库没链接搜同名.o是否存在补deps,检查sources
改了代码但行为没变文件没进构建图find out/ -name "*.o"检查BUILD.gn引用链
烧录成功但无输出串口接反/波特率/BOOT 引脚换线、试波特率逐项确认硬件连接
hdc list targets为空系统未起/线是充电线/服务卡住看串口日志换数据线,hdc kill后重启
串口全乱码波特率不匹配试 115200 和 1500000改成正确波特率
镜像超分区大小组件过多或分区表太小看镜像实际大小裁剪组件或调整分区表

6.2 几个用时间换来的判断经验

第一条:报错信息里的第一行往往不是原因。编译系统的输出很长,真正的根因经常埋在中段。我的习惯是把输出重定向到文件,然后从后往前找第一个"看起来像是根因"的条目,而不是从第一行红包开始读。

hb build -f 2>&1 | tee build.log # 之后 grep -n -i "error" build.log | head -30

第二条:环境问题优先怀疑自己,不要怀疑源码。我用同一份源码在三台机器上编译,两台成功一台失败,失败的机器是 Ubuntu 版本不同。改源码解决环境问题,是最亏的做法——你现在改的每一处,将来升级源码都会变成冲突。

第三条:每次换环境,先跑一遍最小验证。不要一上手就改驱动。先保证原始源码能编译通过、能烧进去、串口有正常日志。这条基线建立之后,任何改动引起的问题都能通过回滚定位。我见过太多人一上来就在没跑通的环境里改代码,最后连"是环境问题还是代码问题"都分不清。

第四条:给工作区做快照。虚拟机的快照功能是救命稻草。在"源码同步完成""编译首次成功""烧录首次成功"这三个节点各打一个快照,后面任何一步崩了,回滚成本是几分钟而不是几小时。物理机的话,至少把编译成功的out/目录备份一份,或者用 git 把改动管理起来。

第五条:别迷信"最新版本"。源码分支、Ubuntu 版本、Python 版本、Node 版本,都选文档里明确验证过的组合。追新在南向开发里几乎只有坏处,因为你遇到的问题别人还没遇到,搜不到答案。

6.3 关于工具选择的取舍

烧录和调试工具这块,市面上有两类选择:一类是集成度高的图形化工具,把编译、烧录、串口都集成在一个界面里,适合刚上手的人,缺点是出问题时看不到底层命令,排查困难;另一类是纯命令行工具链,灵活、可脚本化、报错信息完整,适合已经熟悉流程的人。

我的建议是先用图形化工具把流程跑通,再切到命令行理解每一步在干什么。图形化工具的价值在于它帮你把参数配对了,你能快速看到"成功的样子";命令行的价值在于你能改参数、能自动化、能在失败时看到完整日志。两者不是替代关系,是学习路径上的两个阶段。

另外,配置文件的管理容易被忽视。产品配置、分区表、烧录参数这些改动,建议全部用 git 管理起来,哪怕只是放在一个私有仓库里。南向开发涉及的文件又多又散,靠记忆记住"上次改了哪几个参数"是不现实的,尤其是隔几周再回来接手的时候。

最后说个我自己的习惯:每解决一个环境问题,就在一个troubleshooting.md里记一行——现象、原因、解法。做了三个月之后,这份笔记基本覆盖了日常遇到的所有报错,新人来了直接给他看,比我口头讲一小时有用得多。环境搭建这件事,本质是把一次性的踩坑成本变成可复用的经验,记下来比记住更重要。