1. 项目概述与核心思路在嵌入式开发尤其是基于Zephyr RTOS的项目中管理一个项目所依赖的多个代码仓库如Zephyr内核、硬件抽象层、第三方库一直是个麻烦事。传统方法要么是把所有代码都塞进一个仓库导致仓库臃肿、更新困难要么是手动管理多个git submodule操作繁琐且容易出错。Zephyr项目自带的west工具就是为了解决这个痛点而生的多仓库管理工具。它通过一个名为“提货单”的配置文件来定义所有依赖仓库的地址、版本和存放位置实现一键同步。很多开发者初次接触west都是从官方的“Getting Started”指南开始的那里面默认使用的是T1拓扑结构。简单来说就是整个工作空间以Zephyr仓库为中心应用的代码可以放在任何地方。这种方式对于学习和体验Zephyr本身很方便因为它会拉取Zephyr仓库及其west.yml中定义的所有模块。但当你真正开始一个独立的、需要版本控制和团队协作的应用项目时T1结构的弊端就显现出来了你的应用代码散落在工作空间里与Zephyr源码强耦合无法作为一个独立的Git仓库进行管理、分发和持续集成。因此对于绝大多数独立的、产品化的Zephyr应用项目T2拓扑结构是更优的选择。它的核心思路是“以我为主”让你的应用仓库成为整个工作空间的中心。应用的west.yml文件即提货单定义了它需要依赖的Zephyr版本以及具体哪些外部模块。这样你的应用仓库就是一个自包含的、可复现的单元。任何克隆了你应用仓库的人只需要一条west init和west update命令就能获得一个完整的、版本匹配的开发环境。本文将深入探讨这种“单一应用的提货单”使用方法分享从项目结构设计、提货单编写到日常开发工作流的完整实践并附上大量实操中积累的细节和避坑指南。2. 拓扑结构深度解析与选型考量在深入T2结构的具体实现前我们有必要把west支持的三种拓扑结构彻底搞清楚理解它们各自的设计哲学和适用场景这能帮助你在未来面对不同项目时做出最合适的选择。2.1 T1结构以Zephyr为中心的“学习与探索”模式T1结构是west的默认模式也是新手最先接触的模式。它的提货单文件位于zephyr仓库根目录下的west.yml。工作空间布局示例zephyr_workspace/ # 工作空间根目录 ├── zephyr/ # 核心仓库内含 west.yml │ ├── west.yml │ └── ... ├── modules/ # 由zephyr/west.yml定义的所有模块 │ ├── hal/ │ ├── lib/ │ └── ... └── my_app/ # 你的应用可以放在任何位置甚至不在本目录下 ├── src/ └── prj.conf核心特点与适用场景中心化Zephyr仓库是绝对核心它决定了整个生态的版本和模块组成。模块全量下载除非修改zephyr/west.yml否则执行west update会拉取该文件中定义的所有模块无论你的应用是否需要。这对于初次接触Zephyr想了解其全貌的开发者是优点但对于特定应用开发则是负担。应用独立性差你的应用目录my_app通常只是一个普通的文件夹与Zephyr源码树深度绑定。很难将其作为一个独立的Git仓库来管理因为其中不包含构建它所必需的环境定义提货单。最佳场景适用于Zephyr内核本身的开发、调试、贡献或者用于快速原型验证、评估不同模块。对于需要长期维护、有明确版本管理和CI/CD需求的独立产品应用T1结构显得力不从心。2.2 T2结构以应用为中心的“产品开发”模式T2结构将控制权从Zephyr移交到了你的应用手中。提货单文件位于你的应用仓库的根目录通常命名为west.yml或zephyr/west.yml后者是历史遗留现在推荐根目录。工作空间布局示例my_product_firmware/ # 这也是你的Git仓库根目录 ├── CMakeLists.txt ├── prj.conf ├── src/ │ └── main.c ├── west.yml # 核心应用自己的提货单 └── .git/当你在此目录执行west init -l .-l表示local从本地提货单初始化后west会读取./west.yml并创建如下结构my_product_firmware/ ├── .west/ # west的配置目录标识工作空间根 ├── CMakeLists.txt ├── prj.conf ├── src/ ├── west.yml ├── zephyr/ # 由west.yml指定版本拉取 └── modules/ # 仅包含west.yml中明确指定的模块 ├── hal/nxp ├── hal/espressif └── lib/lvgl核心特点与优势应用自治你的应用仓库是一个完整的、自描述的实体。它明确声明了“我需要Zephyr的哪个版本以及哪几个特定模块”。这完美契合软件工程中的“声明式依赖”理念。依赖精准控制只拉取应用真正需要的模块极大节省了初始克隆时间、磁盘空间和后续更新的网络开销。对于一个复杂的Zephyr版本全量模块可能超过1GB而精准控制后可能只需几百MB。完美的版本控制与协作你可以将整个应用仓库包含west.yml推送到Git服务器。任何协作者克隆后都能通过west命令一键复现完全一致的开发环境包括正确的Zephyr和模块版本避免了“在我机器上能编译”的经典问题。独立的生命周期你的应用可以独立于Zephyr的主线开发节奏。你可以锁定一个稳定的Zephyr LTS版本进行产品开发同时根据需要单独升级某个模块而不受Zephyr仓库west.yml变更的影响。适用场景这是绝大多数产品级Zephyr嵌入式项目的推荐结构。无论是智能硬件、物联网设备还是工业控制器只要你的固件是一个需要独立维护、发布和协作的项目T2结构都是首选。2.3 T3结构多应用管理的“平台与生态”模式T3结构更为复杂它引入了一个独立的“提货单仓库”。这个仓库不包含任何Zephyr或应用代码只负责管理多个应用及其共享依赖的版本映射关系。工作空间布局示例firmware_platform/ # 工作空间根 ├── manifest-repo/ # 独立的提货单仓库 │ └── west.yml # 主提货单定义所有项目 ├── zephyr/ # 共享的Zephyr ├── modules/ # 共享的模块 ├── product_a/ # 应用A的独立仓库 ├── product_b/ # 应用B的独立仓库 └── common_driver_lib/ # 公司内部共享库的独立仓库核心特点与适用场景解耦与复用将“依赖关系定义”与“具体项目代码”完全分离。一个平台团队可以维护manifest-repo统一管理所有产品线共享的Zephyr版本和基础模块。各产品团队维护自己的应用仓库。集中式版本管理可以在一处为所有应用升级Zephyr或关键模块的版本确保平台一致性。高复杂性设置和管理成本较高需要清晰的团队协作规范。最佳场景适用于拥有多条产品线、多个团队的大型公司或组织需要在不同产品间共享和统一基础软件栈版本。对于单个或少量应用T3结构显得过于重型。选择决策树你在学习Zephyr或做一次性实验吗- 选T1。你在开发一个需要独立维护、版本控制和团队协作的嵌入式产品吗- 选T2。你们公司有多个团队在开发多个基于Zephyr的产品并且需要统一基础组件版本吗- 选T3。3. T2结构提货单的详细编写指南理解了T2结构的优势后我们来解剖一个实战级的west.yml文件。这是整个模式的核心其编写质量直接决定了项目的可维护性。3.1 基础结构剖析一个典型的T2结构west.yml如下所示我们逐部分解析manifest: # 1. 定义远程仓库服务器 remotes: - name: zephyrproject-rtos url-base: https://github.com/zephyrproject-rtos - name: my-company-gitlab url-base: https://gitlab.mycompany.com # 2. 设置默认值可选但推荐 defaults: remote: my-company-gitlab # 默认从公司私服拉取 revision: main # 默认使用main分支 # 3. 定义项目列表核心 projects: # 3.1 必须的Zephyr项目 - name: zephyr remote: zephyrproject-rtos # 覆盖默认remote从官方拉取 revision: v3.6.0 # 指定明确的版本标签强烈推荐 clone-depth: 1 # 浅克隆节省时间和空间 import: # 关键从zephyr导入部分模块 name-allowlist: # 白名单模式只导入我需要的 - cmsis - hal_nxp - hal_espressif - fatfs - lvgl # 另一种方式是黑名单name-blocklist但不推荐容易遗漏 # 3.2 公司内部的私有库 - name: company-common-drivers path: modules/drivers/company # 可以自定义拉取后的路径 revision: v1.2.0 # 3.3 另一个第三方库例如来自GitHub - name: tinycbor remote: zephyrproject-rtos # 同样从官方仓库拉取 revision: some-sha-or-tag import: false # 这个库在zephyr的west.yml中也存在但我不从那里导入而是单独定义 # 4. 自引用声明本仓库也是west管理的一部分 self: path: app # 本应用代码在app目录下如果west.yml在根目录则通常为. west-commands: scripts/west-commands.yml # 可选自定义west命令3.2 关键配置项深度解读1.revision的学问这是保证构建可复现性的生命线。绝对不要使用浮动的分支名如main、master作为产品项目的revision。最佳实践使用标签。例如revision: v3.6.0。标签是固定的能永久锁定版本。次选使用完整的提交SHA。例如revision: a1b2c3d4e5f6...。同样固定但可读性差。何时用分支仅在项目早期主动开发某个模块且需要频繁同步其最新提交时使用。即使如此也建议在发布或测试时切换到固定标签或SHA。2.import机制的妙用这是T2结构节省空间的精髓。Zephyr的west.yml定义了几十个模块但你的项目可能只用其中几个。import: true导入Zephyr的west.yml中定义的所有项目。这等同于T1模式不推荐在T2中使用。import: name-allowlist: [...]白名单模式。只导入列表中指定的模块。这是最推荐的方式清晰、明确、安全。你需要仔细审查项目的CMakeLists.txt和Kconfig文件找出所有直接和间接依赖的模块。import: name-blocklist: [...]黑名单模式。导入除列表外的所有模块。不推荐因为Zephyr未来新增的模块会被自动引入可能导致意外的构建失败或体积膨胀。如何确定白名单列表一个实用的方法是先使用import: true全量拉取成功构建一次。然后查看build目录下的CMakeCache.txt或运行west list找出实际被引入的模块路径。再将其整理到白名单中最后将west.yml改为白名单模式并删除modules目录重新west update验证。3.clone-depth: 1对于只需要特定版本标签的项目开启浅克隆可以大幅提升初始拉取速度并节省磁盘空间。这对于在CI/CD流水线中频繁创建干净构建环境特别有用。但如果你需要在该仓库中执行git log或基于历史提交的操作则可能需要完整克隆。4.self的作用self部分声明了当前提货单文件所在的仓库本身也是一个被west管理的“项目”。这允许你使用west命令管理应用代码本身如west diff,west status。在west-commands中定义项目特定的扩展命令例如一键烧录、测试等极大提升团队开发效率。3.3 一个完整的、带注释的实战west.yml示例假设我们开发一个基于NXP RT1060和ESP32-C3作为协处理器的智能家居面板需要GUI和文件系统。manifest: remotes: - name: zephyr-org url-base: https://github.com/zephyrproject-rtos - name: company-internal url-base: https://git.company.com # 可以添加多个远程例如用于备份或不同网络的镜像 - name: zephyr-mirror url-base: https://gitee.com/mirrors/zephyr defaults: remote: company-internal # 公司内部项目默认源 revision: release-v1.0 # 公司内部项目默认使用发布分支 projects: # 核心Zephyr RTOS锁定长期支持版本以保证稳定性 - name: zephyr remote: zephyr-org revision: v3.6.0 # 使用LTS版本而非最新的main clone-depth: 1 import: name-allowlist: - cmsis # ARM CMSIS必须 - hal_nxp # NXP芯片HAL库 - hal_espressif # ESP32芯片HAL库 - tinycbor # 用于数据序列化 - lvgl # GUI库 - fatfs # 文件系统 - littlefs # 另一个更适用于Flash的文件系统作为备选 - mbedtls # 安全连接需要 - mcumgr # 设备管理用于OTA # import 部分也可以使用 path-prefix 来按路径前缀过滤 # import: # path-prefix: # - hal # - modules/fs # 公司内部的硬件抽象板级支持包 - name: bsp-smart-panel-rt1060 path: boards/arm/company_panel # 自定义路径与Zephyr官方boards目录区分 revision: hardware-v2.1 # 项目专用的图形资源包 - name: assets-gui path: assets revision: main # 资源文件变动频繁可以用分支跟踪 # 从第三方GitHub仓库引入的一个传感器驱动 - name: sensor-bme680-driver remote: zephyr-org # 假设这个驱动已被收录到Zephyr的模块仓库 revision: abc123def456 # 使用确定的SHA import: false # 独立定义不从zephyr导入 self: path: . # 提货单就在仓库根目录 west-commands: .west-commands.yml # 自定义命令配置文件4. 从零开始的T2项目工作流实操理论说再多不如动手做一遍。下面我们以一个名为smart_thermostat的虚拟项目为例演示从创建到日常开发的完整工作流。4.1 初始化项目仓库首先在代码托管平台GitLab GitHub等创建一个新的空仓库smart_thermostat。# 1. 在本地创建项目目录并初始化Git mkdir smart_thermostat cd smart_thermostat git init git remote add origin https://your-git-server.com/your-team/smart_thermostat.git # 2. 创建最基本的应用代码结构 mkdir src boards drivers touch CMakeLists.txt prj.conf src/main.c # 3. 创建并编写 west.yml (内容参考上一节的精简版) cat west.yml EOF manifest: remotes: - name: zephyrproject-rtos url-base: https://github.com/zephyrproject-rtos projects: - name: zephyr remote: zephyrproject-rtos revision: v3.6.0 clone-depth: 1 import: name-allowlist: - cmsis - hal_nxp - fatfs - lvgl self: path: . EOF # 4. 创建自定义west命令配置文件可选但推荐 cat .west-commands.yml EOF west-commands: - file: scripts/west_commands.py commands: - name: flash help: Flash firmware to target device - name: debug help: Start a debug session EOF # 5. 编写一个简单的main.c和CMakeLists.txt echo #include zephyr/kernel.h\nvoid main(void) { printk(Hello from T2 workspace!\\n); } src/main.c echo cmake_minimum_required(VERSION 3.20.0)\nfind_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})\nproject(smart_thermostat)\ntarget_sources(app PRIVATE src/main.c) CMakeLists.txt # 6. 将初始文件提交到Git git add . git commit -m Initial commit: project structure and west manifest git push -u origin main现在你的应用仓库已经包含了项目的全部定义。4.2 协作者克隆与环境搭建另一位开发者要参与项目他只需要# 1. 克隆应用仓库仅此一个仓库 git clone https://your-git-server.com/your-team/smart_thermostat.git cd smart_thermostat # 2. 使用west初始化工作空间 # -l . 表示使用当前目录下的west.yml文件 west init -l . # 3. 根据west.yml拉取所有依赖Zephyr及白名单模块 west update # 4. 导出Zephyr环境变量通常init会做但显式执行更安全 west zephyr-export # 5. 现在可以构建了假设使用mcuboot和rt1060板 west build -b nxp_rt1060_evk -- -DCONFIG_BOOTLOADER_MCUBOOTy看只需要两步命令west init -l .和west update一个完整的、版本精确匹配的开发环境就准备好了。这比手动管理git submodule或复制整个Zephyr SDK要优雅和可靠得多。4.3 日常开发工作流1. 更新依赖当需要升级Zephyr版本或某个模块时只需修改west.yml中的revision字段然后执行west updatewest会很智能地切换到新的提交或标签。务必在更新后充分测试。2. 添加新依赖假设你需要添加一个libmetal库。首先确认这个库在Zephyr的模块列表中通常在zephyr/west.yml或modules目录下的某个west.yml里能找到。然后在你项目的west.yml的projects列表下仿照zephyr项目添加如果它是Zephyr官方模块通常不需要单独添加只需将其加入import的name-allowlist即可。如果是一个全新的第三方仓库你需要在remotes中添加其远程地址然后在projects中完整定义。3. 提交更改你的west.yml是项目版本锁定的核心文件任何对west.yml的修改都必须提交到Git。这样团队其他成员在拉取代码后运行west update就能同步到相同的依赖状态。4. 使用自定义west命令你可以在.west-commands.yml中定义的命令会像原生west命令一样使用。例如上面定义了flash命令你可以实现一个Python脚本scripts/west_commands.py里面用pyocd或openocd工具实现烧录逻辑然后团队所有人都能用west flash来烧录标准化了操作流程。5. 高级技巧、常见问题与避坑指南在实际项目中踩过不少坑后我总结了一些高级技巧和常见问题的解决方法。5.1 提货单的模块化与继承对于复杂项目一个west.yml文件可能变得很长。你可以利用import功能将其模块化。主west.yml:manifest: remotes: - name: zephyrproject-rtos url-base: https://github.com/zephyrproject-rtos - name: my-company url-base: https://git.company.com projects: - name: zephyr remote: zephyrproject-rtos revision: v3.6.0 import: file: submanifests/zephyr_imports.yml # 将导入列表分离到另一个文件 - name: company-base remote: my-company revision: main import: true # 导入公司基础库自己的提货单 self: path: .submanifests/zephyr_imports.yml:manifest: projects: - name: cmsis - name: hal_nxp - name: lvgl # 注意这个文件中的项目会“继承”主文件中定义的remote和revision吗 # 不会这种import: file:方式导入的是另一个完整的manifest片段 # 其中的项目需要有自己的完整定义或者主文件有defaults设置。 # 更常见的用法是使用 import: true 配合 name-allowlist。5.2 离线开发与镜像仓库配置在公司内网或网络不稳定环境下可以为west配置镜像或本地仓库。方法一修改远程URL直接但硬编码在west.yml中直接使用内网GitLab地址。remotes: - name: zephyr-mirror url-base: http://gitlab.internal.com/mirrors/zephyr方法二使用Git的insteadOf配置更灵活在本地或全局Git配置中设置URL重写规则这样无需修改west.yml。git config --global url.http://gitlab.internal.com/mirrors/.insteadOf https://github.com/zephyrproject-rtos/这样所有指向https://github.com/zephyrproject-rtos/的请求都会被重定向到内网镜像。方法三west的本地缓存west在~/.west/下有缓存。在网络通畅时完成west update后可以将整个工作空间包括.west打包分发给离线环境使用。5.3 常见问题排查表问题现象可能原因解决方案west update失败提示找不到仓库或权限错误1. 远程url-base地址错误或不可达。2. 使用了SSH URL但未配置SSH密钥。3. 公司网络有代理。1. 检查west.yml中的url-base。2. 使用HTTPS URL或配置好SSH Agent。3. 配置Git的HTTP代理git config --global http.proxy ...。构建时提示找不到头文件或CMake包1. 模块未正确拉取不在白名单。2. 模块路径未被正确添加到CMake搜索路径。1. 运行west list检查所有项目状态确认所需模块存在。2. 检查west.yml的import白名单是否遗漏了该模块的父模块或相关模块。3. 确保执行了west zephyr-export。west init -l .提示 “already in a workspace”当前目录或父目录已存在.west目录。如果要重建删除.west目录。如果想在子目录管理需使用--mr指定manifest路径。拉取的模块版本不对1.revision指定了分支名该分支已有新提交。2.west.yml和west.lock文件不一致。1.产品开发中永远使用标签或SHA不要用分支名。2.west update会更新west.lock。确保将west.lock也提交到Git官方推荐以锁定次级依赖的版本。工作空间体积过大1. 使用了import: true全量导入。2. 未使用clone-depth: 1。3. 历史提交过多。1. 改用name-allowlist精准控制。2. 对只读依赖如Zephyr启用浅克隆。3. 定期清理.west目录下的缓存需谨慎。5.4 必须提交到版本控制的文件在T2结构中你的应用仓库里哪些文件应该提交必须提交west.yml依赖声明文件核心中的核心。west.lock由west update生成的锁文件记录了所有项目当前的确切提交SHA。提交它能保证所有协作者、CI服务器获得完全相同的依赖树。这是实现真正可复现构建的关键。你的应用源代码src/,boards/,drivers/等。CMakeLists.txt,prj.conf,Kconfig等构建配置。.west-commands.yml及自定义命令脚本。不应该提交列入.gitignorebuild/目录编译产物。zephyr/目录由west管理的Zephyr源码。modules/目录由west管理的模块源码。.west/目录west工具的工作状态和缓存。5.5 在CI/CD中集成在GitLab CI、GitHub Actions等流水线中使用T2结构异常简单。一个典型的.gitlab-ci.yml片段build_firmware: image: ghcr.io/zephyrproject-rtos/zephyr-build:v0.26.0 # 使用官方Zephyr构建镜像 script: - west init -l . # 初始化工作空间 - west update # 拉取依赖 - west zephyr-export - west build -b $BOARD_TARGET - west sign # 如果需要签名 - cp build/zephyr/zephyr.bin firmware.bin artifacts: paths: - firmware.bin由于依赖被west.yml和west.lock精确锁定你的CI构建每次都是确定性的与本地开发环境完全一致。从最初的T1结构摸索到在多个产品项目中全面转向T2结构最大的体会是它真正将嵌入式固件开发从“环境配置的泥潭”中解放了出来。它带来的最大价值不是节省那一点磁盘空间而是可复现性和团队协作的标准化。新同事入职第一天就能编译出与主分支完全一致的固件CI流水线再也不会因为隐性的依赖变化而失败你可以安心地为一个老产品维护一个基于旧版本Zephyr的分支而不用担心被新版本污染。这一切都始于一个精心编写的west.yml文件。花时间理解并设计好你的项目拓扑结构是迈向高效、稳健的嵌入式软件开发流程的关键一步。