ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Zephyr RTOS构建工具链West全解析:从init到flash的完整编译流程

Zephyr RTOS构建工具链West全解析:从init到flash的完整编译流程 刚接触 Zephyr RTOS 的开发者相信我第一道门槛往往不是 C 语言而是 West 这套构建工具链。很多朋友按网上的教程一路 pip install west然后 git clone 了源码接着一条west build -b qemu_x86 samples/hello_world丢下去满心以为会直接跑出 Hello World结果不是报“Zephyr base not found”就是“No board named ...”再要么就是 CMake 版本不对卡得人头皮发麻。这篇文章不打算给你抄一段命令就跑而是把 Zephyr RTOS 里 West 这条命令工具链和它背后的编译过程掰开揉碎。我会从“为什么 Zephyr 非要搞个 West 出来”开始讲把west init、west update、west build、west flash这些高频命令逐个拆开再用一个完整的 hello_world 编译流程让你看清 CMake、Ninja、Kconfig、设备树和编译脚本到底是怎么串在一起的。这篇内容适合两种人一种是刚上手 Zephyr 的嵌入式新手看完能知道自己敲的每条命令在执行什么另一种是被工程构建问题折磨过、想搞清楚“改 prj.conf 为什么不生效”“这是哪个目录生成的 .bin”这些细节的开发者。源码拉到本地能编译只是开始真正搞明白每一步在干什么后面做项目、加新板子、排查构建问题才会顺手得多。1. 为什么 Zephyr 要用 West 而不是直接 make很多人第一次接触 Zephyr 都会困惑这年头嵌入式项目不都是 Makefile 或者 CMake 一把梭吗Zephyr 本来也用 CMake 做底层构建但它在 CMake 之上又套了一层 West这就让刚入门的人看构建系统时总觉得隔了一层纱。1.1 Zephyr 不是单仓库West 是分层的元工具Zephyr 这个项目从诞生起就不是单个 Git 仓库而是一个集合体。核心的 zephyr 仓库负责内核和主构建逻辑此外还有一堆 HAL硬件抽象层、外设模块、第三方库比如modules/hal/st、modules/hal/nordic、modules/trusted-firmware-m等等。这些仓库由同一个 manifest 文件统一管理而 West 就是那个负责把它们按指定版本拉下来、组织在一起的管家。你可以把 West 理解成“嵌入式界的包管理器 构建入口”。就像前端用 npm 管理依赖、用 webpack 或者 vite 打包一样Zephyr 用 West 管理多仓库版本再调用 CMake 和 Ninja 完成编译。West 不是一个取代 CMake 的新构建系统而是把 CMake 的各种细节封装成更简单的命令让开发者不用手动去管cmake -B build -DBOARDxxx那一堆参数。这就是为什么网上很多教程让你先pip install west但从来没人让你手动 clone 所有仓库。单纯把 zephyr 主仓库 clone 下来是编不了工程的你还会缺一堆 HAL 和工具链相关的依赖。1.2 West build 到底在执行什么west build这条命令看着简单实际它替你做了两件事第一件事是 CMake 配置阶段它会把 board 型号、应用源码路径、Kconfig 配置、设备树文件全部组织好生成构建系统文件第二件事才是真正调用 Ninja 或者 Make 去编译和链接产出最终固件。而且 West 在配置阶段还会自动检查环境比如当前 CMake 版本够不够、有没有装 Zephyr SDK、Python 依赖是否齐全。有一步不满足它就直接报错停下来这也是为什么新手经常在west build这一步碰壁。1.3 一个典型的 Zephyr 工程结构Zephyr 的 workspace 和我们平时见到的 Git 工程不太一样。一个标准的 Zephyr workspace 长这样zephyrproject/ ├── .west/ │ └── config ├── zephyr/ │ ├── west.yml │ ├── CMakeLists.txt │ ├── boards/ │ ├── samples/ │ ├── subsys/ │ └── ... ├── bootloader/ ├── modules/ │ └── hal/ └── tools/.west目录存的是 west 的本地配置相当于一个工作区索引。west.yml在 zephyr 仓库里它定义了所有子项目仓库的地址、分支和 commit 号是 West 拉取代码的“地图”。west init只是初始化工作区真正把东西全拉下来的是west update这个后面细讲。2. West 核心命令逐个过West 的命令设计得很直观经常用到的其实就五六个。我挨个讲每个都会说清楚它在干什么以及有什么容易踩的坑。2.1 west init 与 west updatewest init的作用是创建一个新的 Zephyr workspace 并拉取 manifest 仓库默认就是 zephyr 主仓库。最常用的写法是west init -m https://github.com/zephyrproject-rtos/zephyr --mr v3.7.0 zephyrproject这里-m指定 manifest 仓库地址--mr也就是 manifest revision指定拉取哪个分支或者 tagzephyrproject是你要创建的目录名。我自己实际用下来强烈建议指定--mr。如果不加默认拉取 main 分支也就是开发版代码。开发版不是说不能用而是可能刚更新完还没有被广泛测试某个模块和文档里的样例对不上是常有的事。要稳定做项目就固定在一个 release 版本上比如 v3.7.0 或者 v4.0.0。初始化完成之后工作区里其实只有 zephyr 这一个仓库其他仓库都还没拉。这时候必须执行cd zephyrproject west updatewest update会读取 zephyr/west.yml 里面定义的所有 project把 HAL、模块、工具链相关的仓库全部拉取到 manifest 里指定的 commit。这一步的坑在于仓库比较多总数据量几个 GB网络差的情况下很容易抓到一半失败。遇到失败不用慌重新执行一次west update会断点续传只要网络稳定多跑几次总能补全。2.2 west build 常用参数与 pristine 构建west build是使用频率最高的命令基本格式是west build -b board_name app_directory比如编译自带示例cd zephyrproject/zephyr west build -b qemu_x86 samples/hello_world这里-b指定目标板子编译输出默认放在build目录。实际项目里我通常会给-d参数指定构建目录把不同板子的构建产物分开。比如west build -b nrf52840dk_nrf52840 -d build/nrf52840 samples/hello_world这样同一个应用源码想同时编译多个板子时不会互相覆盖缓存。还有一个非常重要的参数是-p也就是 pristine。west build -p always -b qemu_x86 samples/hello_world表示强制全量重新构建会清空之前的构建缓存再重新配置编译。-p auto默认值则是自动判断如果 board、应用目录或者关键配置变了就会自动 pristdine否则用增量构建。我建议在开发调试阶段用默认的 auto 就行编译速度会快不少。但如果你发现改了代码编译出来的固件行为没变或者menuconfig修改的配置没生效大概率是缓存陈旧这时候用-p always强制重来一次就解决了。2.3 west flash / west debug / west build -t编译出来的是 elf、bin 这类固件文件但要让代码跑在开发板上还得烧录。west flash就是干这个的west build -b nrf52840dk_nrf52840 samples/hello_world west flash它会根据当前板子的定义自动选择烧录器 runner。nRF 系列通常用 nrfjprog 或者 jlinkSTM32 系列常见的是 openocdESP32 则是 esptool。也可以手动指定 runner比如west flash --runner jlink当你用了不常见的开发板或者接了好几台烧录器时指定 runner 会更可靠。调试相关的命令是west debug会启动 GDB 连接调试器。习惯命令行的嵌入式老手一般直接从这儿进 GDB 打断点效率很高。west build -t是经常会看到一个用法它的意思是列出当前构建系统支持的所有目标相当于查看 Ninja 里有哪些 task。执行west build -t menuconfig会弹出 Kconfig 图形化配置界面和 Linux kernel 的 menuconfig 一样能可视化地打开各类配置项。这个在调驱动、调功耗的时候特别好用。除了 menuconfig还常用west build -t run可以在 QEMU 上直接运行程序而不用另外输命令。2.4 west list / west topdir / west boards 其他高频命令有几个查信息的命令也比较实用。west list可以列出 workspace 里所有被 west 管理的仓库及其版本号适合排查依赖拉没拉全。west topdir直接打印 workspace 根目录路径写脚本的时候经常用得到。west boards会列出当前 Zephyr 支持的所有板子数量非常庞大。找板子的时候可以配合 grep比如west boards | grep nrf52840这样能快速确认一个板型名是否存在免得-b参数写错了报一堆错。3. 从零编译 Hello World 的完整拆解前面命令讲了半天我们来走一遍完整流程边跑边看每一步到底发生了什么。这一步走下来你对 Zephyr 编译过程的理解会比看十篇文档都深。3.1 环境准备依赖安装与环境变量在拉代码之前先把系统依赖装齐。Zephyr 官方支持的 Linux 发行版上需要的核心工具是sudo apt install --no-install-recommends git cmake ninja-build gperf \ ccache dfu-util device-tree-compiler wget \ python3-dev python3-pip python3-setuptools python3-tk python3-wheel xz-utils file \ make gcc gcc-multilib g-multilib libsdl2-dev libmagic1注意 CMake 版本Zephyr 比较新的大版本要求 CMake 3.20 以上最好用 apt 装完再确认一下cmake --version如果版本太低后面west build直接会报错。Python 方面推荐 3.10 以上版本。这里有个老生常谈的坑别用系统自带的 Python建议用 venv 创建独立环境避免改装系统包搞乱依赖。3.2 拉取 Zephyr 源码并初始化 workspace准备好之后开始拉代码pip install west west init -m https://github.com/zephyrproject-rtos/zephyr --mr v3.7.0 zephyrproject cd zephyrproject west updatewest update执行完你可以用west list看一下拉下来的仓库数量。看到 zephyr、hal 相关的仓库都在就算成功。这一步网络不好的时候很痛苦因为仓库多、体积大。我有几次 update 到一半中断重新执行west update它会把已经下载好的跳过继续补剩下的所以看到失败别急着重新 init直接再跑 update 就行。3.3 编译 hello_world 并运行接下来我推荐先用 QEMU 跑一次因为不需要真实硬件排查问题最简单cd zephyrproject/zephyr west build -b qemu_x86 samples/hello_world west build -t runwest build -t run会自动启动 QEMU你会在终端里看到类似输出Hello World! qemu_x86整个编译过程第一次会比较慢因为要配置工具链和编译大量内核文件后续增量编译就快得多。编译完成后build 目录下会生成最终固件。3.4 编译过程四步走CMake 配置、Kconfig、设备树、Ninja 编译west build执行过程中实际干了四件比较核心的事我拆开说。第一步CMake配置阶段。West 调用 CMake并传入 board 型号、应用源码目录、workspace 路径、工具链路径等参数。CMake 会根据zephyr/CMakeLists.txt和boards/arch/board/board.yaml、board.defconfig这些文件来确定整个工程的构建规则。你在工程的CMakeLists.txt里写的find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})也是在这一阶段被执行。第二步配置 Kconfig。Zephyr 像 Linux 一样用 Kconfig 管理内核配置项。每个应用目录下的prj.conf会覆盖默认配置board目录下的board_defconfig提供板级默认值。这一阶段会生成 build 目录下的.config文件里面就是最终的配置结果。我调试的时候经常直接打开这个文件看某些配置到底有没有打开。第三步处理设备树。Zephyr 的设备模型基于设备树Device Tree简称 DT类似 Linux 的设备树但语法更精简。board 定义目录里有board.dts和board.dtsi描述硬件上有哪些外设、中断、引脚。编译过程会把这套设备树源码通过解析脚本生成 C 头文件也就是 build/zephyr/include/generated/devicetree_generated.h。你在代码里用的DT_NODELABEL(...)这类宏就是从这儿来的。第四步Ninja 编译与链接。配置完成之后所有源码文件、生成的头文件、Kconfig 生成的 autoconf.h 都已经就绪West 调用 Ninja 并行编译所有 C 文件最后链接成固件。之所以用 Ninja 而不是 make是因为 Ninja 在并行编译和增量编译速度上更有优势。这一步完成之后build/zephyr 文件夹下生成的产物至关重要文件作用zephyr.elf带符号和调试信息的可执行文件调试时靠它zephyr.bin纯二进制固件烧录到 Flash 用zephyr.hexIntel HEX 格式固件很多烧录器喜欢用这个zephyr.map链接映射表查看内存布局、函数地址用.configKconfig 最终配置include/generated/自动生成的头文件包括设备树和配置相关的看这些文件就能定位问题。比如怀疑哪个模块没编译进去直接去zephyr.map里搜符号怀疑配置没生效打开.config看具体项是 y 还是 n。3.5 一套真实工程里我们怎么组织构建目录说个我在实际项目里常用的做法始终保持源码干净构建目录放 workspace 外面或者放在独立目录里。west build -b nrf52840dk_nrf52840 -d ../build/hello_nrf applications/hello_app好处是以后升级 Zephyr 版本可以不用删构建缓存直接在旧缓存上重新配置。另外如果同一个应用要同时维护两个硬件版本比如用同一个代码库兼容nrf52840dk_nrf52840和nrf5340dk_nrf5340用-d区分构建目录就不会互相污染。4. 实际踩过的坑与排查技巧构建系统越复杂踩坑概率越高。这里把我自己和身边同事遇到过的高频问题整理成速查表按经验从高到低排序。4.1 构建缓存导致配置不生效我见过最多的求助帖就是“我改了 prj.conf 打开某个配置重新编译运行怎么感觉没生效”绝大多数原因是使用了旧的构建缓存。Kconfig 配置嵌在 CMake 配置产物里如果源码发生了变化但 West 没有触发 pristine 构建旧的.config可能被保留下来。稳妥做法是west build -p always -b board app_dir还有更精细的做法按需单独重新配置 Kconfig 依赖而不全量重编rm -rf build west build -b board app_dir如果连 main.c 的修改都觉得没生效先检查 build 是不是指向了别的目录。有时候你-d指定了构建目录却忘了改代码后重新 build纯属乌龙。4.2 CMake 版本与工具链不匹配Zephyr 对构建工具版本要求比较敏感。系统里 CMake 版本太老比如 3.16执行west build会直接提示 minimum required 版本。升级 CMake 别用 apt因为 apt 源里版本往往偏老建议直接用 pip 安装pip install cmake装完确认一下cmake --version注意新版 CMake 会挂在 python venv 环境里所以一定要在激活的 venv 里操作。还有 Ninja 版本太老的 Ninja 也可能导致并行编译失败一样可以用 pip 升级pip install ninja4.3 找不到 board / 不知道支持哪些开发板west build -b nrf52840dk_nrf52840 samples/hello_world如果报“No board named xxx”要么是板名打错了要么是当前 Zephyr 版本还不支持这块板。第一时间跑west boards | grep nrf52840确认正确的板名。注意板子名和开发板商品名不一样比如 nRF52840 DK 对应的板名是nrf52840dk_nrf52840多一个架构后缀少一个字母都对不上。另外如果是在自己的项目里新增了板级支持得保证 boards 目录路径正确。Zephyr 会从 workspace 下所有 repo 里搜索 boards 目录如果你新建的板子放在自定义仓库里manifest 里要把这个仓库加进去不然 West 根本不知道它的存在。4.4 west update 拉取失败与仓库体积问题west update拉到一半报错是网络问题常见症状因为 Zephyr 仓库集合体积确实大。解决办法是先把 Git 的压缩缓存调大一点git config --global http.postBuffer 524288000再执行west update。如果某个仓库一直拉不动也可以暂时注释掉 west.yml 里对应的那个 project先把核心的 zephyr 仓库拉完后面再恢复。千万不要直接把整个 workspace 删了重新 init太浪费时间。只要 west region 没坏重复 update 就好。4.5 使用 west build -t menuconfig 排查 Kconfig 问题如果你发现某个配置项定义了模块但编译出来的固件没有对应功能用 menuconfig 图形化查看最直观west build -t menuconfig对着界面搜索配置项看它是被谁依赖、被谁反向依赖而被关闭了。这比反复改 prj.conf 然后盲猜效率高得多。很多配置项之间有依赖关系光在 prj.conf 里写CONFIG_XXXy不一定生效如果它依赖的某项没打开编译器会静默忽略。4.6 环境变量相关坑老版本 Zephyr 教程里会让你执行source zephyr-env.sh新版本用 West 之后环境变量大部分自动管理了。但如果你在脚本里手动了 ZEPHYR_BASE 或者 ZEPHYR_TOOLCHAIN_VARIANT可能会影响构建。我推荐的干净做法是不用手动 source 各种脚本直接把 Zephyr SDK 路径放在系统环境变量里然后建一个 venv 装 west每次进项目目录用python -m west build这种形式来执行能规避掉很多莫名其妙找不到 west 命令的问题。另外 Zephyr SDK 下载解压之后记得把路径加进环境变量或者用west config设置 SDK 路径west config zephyr.sdk_path ~/zephyr-sdk-0.16.5基本上能想到的问题就是这些。我实际开发过程中最花费时间的往往不是业务代码而是构建系统的隐性知识。West 这套东西第一次接触确实让人想骂人但习惯了以后就会发现它把多仓库、多板型、多配置这些现代嵌入式工程里的复杂问题都统一收敛了比当年各种芯片厂商各自的 IDE 和命令行工具要高效太多。最后再分享一个小技巧如果你同时维护多个 Zephyr 版本的项目别在一个 workspace 里切换分支那很容易把 build 缓存搞得一塌糊涂。老老实实建两个独立 workspace各自用west init指定不同版本互不干扰这是我从踩坑里换来的教训。
返回列表