刚接触 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 -DBOARD=xxx那一堆参数。
这就是为什么网上很多教程让你先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 update
west init的作用是创建一个新的 Zephyr workspace 并拉取 manifest 仓库(默认就是 zephyr 主仓库)。最常用的写法是:
west init -m https://github.com/zephyrproject-rtos/zephyr --mr v3.7.0 zephyrproject这里-m指定 manifest 仓库地址,--mr也就是 manifest revision,指定拉取哪个分支或者 tag,zephyrproject是你要创建的目录名。
我自己实际用下来,强烈建议指定--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 或者 jlink,STM32 系列常见的是 openocd,ESP32 则是 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.hex | Intel HEX 格式固件,很多烧录器喜欢用这个 |
| zephyr.map | 链接映射表,查看内存布局、函数地址用 |
| .config | Kconfig 最终配置 |
| 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_XXX=y不一定生效,如果它依赖的某项没打开,编译器会静默忽略。
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指定不同版本,互不干扰,这是我从踩坑里换来的教训。