Zephyr源码目录结构全解析:从根目录到subsys的嵌入式导航地图
如果你是刚接触 Zephyr 的嵌入式开发者第一次打开 zephyr 仓库根目录多半会被铺满整个屏幕的文件夹吓一跳。它和常见的 FreeRTOS、RT-Thread 这类 RTOS 源码包完全不同——内核、板级支持、驱动、子系统、构建脚本、测试框架全部堆在一个仓库里顶层目录一眼望过去有二三十个。这篇博文就来说清楚这套目录结构的组织逻辑从根目录一路拆到 subsys结合我实际拿 Zephyr 做项目的经验讲一讲每个目录什么时候会用到、哪些地方容易踩坑。内容主要面向从其他 RTOS 转过来的开发者以及在 Zephyr 里经常“找不到文件”的人。1. 先理解 Zephyr 的“多仓库 全家桶”世界观1.1 Zephyr 源码目录不是普通嵌入式工程大多数嵌入式工程师对 RTOS 源码的预期是一个内核小目录加几个 demo加一个移植层。FreeRTOS 就是这样kernel 目录一放portable 目录一放剩下的事就是往自己工程里复制粘贴。Zephyr 完全不是这个思路它更像是 Linux kernel 的文档树和构建系统一起打包进了 RTOS 仓库。Zephyr 的核心仓库里kernel/只是其中一个目录旁边还躺着arch/、boards/、soc/、drivers/、subsys/、dts/、cmake/、scripts/这一大堆顶层目录。它把一块嵌入式产品从芯片启动到应用运行的每一层代码都收纳进来了。这种结构的好处是你不需要到处寻找“当前板子的 BSP 在哪”所有板级相关的东西都有固定位置问就是按目录找。坏处也很明显新手第一次进来很容易懵为什么arch/里也有 armsoc/里也有 armboards/里还有 arm这三个目录到底谁管谁理解了它们的分工就等于理解了 Zephyr 目录结构的一半。1.2 west 把目录变成可组装的地图Zephyr 的源码组织还有一个外层概念west 工具。west.yml这种 manifest 文件描述了当前项目依赖哪些仓库比如 zephyr、hal 仓库、mcuboot 等等。默认的 Zephyr 项目结构通常是~/zephyrproject/ ├── zephyr/ # 就是本文说的这个仓库 ├── bootloader/ # mcuboot 等 ├── modules/ # 外部模块仓库比如 hal 系列 └── tools/ # 辅助工具所以在讲目录结构之前必须先说清楚本文讨论的是zephyr/这个仓库的内部结构而不是整个 west 工作区的结构。west 负责把多个仓库组装起来Zephyr 仓库内部目录则负责把单仓库的内容组织好。这两层地图缺一不可否则你会在“为什么我改了modules/下的代码编译却找不到符号”这种问题上浪费时间。2. 根目录速查一份顶层目录/文件功能对照2.1 顶层目录与文件总览Zephyr 仓库根目录下的内容可以先对照这张表快速建立一个全局印象目录 / 文件作用我什么时候会进去arch/与 CPU 架构强相关的代码调试启动流程、异常处理或者移植新架构boards/具体开发板的板级定义给某块板子加外设、调 pinctrl、看默认配置soc/芯片厂商与系列级别的支持新芯片 Bring-up、修改芯片默认时钟drivers/各类外设驱动写新驱动或者排查某个外设行为异常dts/设备树绑定 YAML 与 SoC 级 dtsi改设备树、加新的设备节点、加新外设绑定include/对外暴露的公共头文件查内核 API、驱动 API、系统调用声明kernel/内核核心实现研究调度器、信号量、定时器实现lib/库实现libc、os 工具库、posix 支持换 libc、查printk/ring_buffer实现modules/外部模块的接线点给某个 west 模块写 CMake/Kconfig 桥接samples/官方示例工程抄作业、快速验证硬件scripts/构建、格式检查、测试相关脚本跑 twister、跑 checkpatchsubsys/各种子系统协议栈、框架找蓝牙、网络、日志、shell、文件系统tests/内核与子系统的测试用例回归验证、看某个功能的预期行为toolchain/工具链变体定义排查交叉编译工具链异常cmake/构建系统模块与辅助脚本改编译选项、追查链接错误Kconfig.zephyrKconfig 配置系统入口看全局配置怎么被 source 进来CMakeLists.txt顶层构建入口全局编译设置west.yml仓库自身的 manifest管理依赖仓库版本VERSION版本号文件确认当前版本这几个目录里cmake/、scripts/、toolchain/属于“构建基础设施”平时应用开发很少直接翻。doc/我会直接忽略需要查资料去官网看在线文档更实时。剩下那些直接参与固件产出的目录才是真正要花时间理解的。2.2 最容易混淆的三兄弟arch、boards、socarch/、boards/、soc/这三个目录放在一起很容易让人以为它们职责重叠。它们实际上处于三个不同的抽象层次。arch/管的是 CPU 架构本身和具体芯片型号无关。比如arch/arm/下面是 Cortex-M 通用的启动代码、异常向量、SysTick 处理arch/riscv/下面是 RISC-V 的 trap 处理和中断上下文切换。你在arch/里一般不会看到某个具体芯片的名字看到的都是架构级的通用逻辑。soc/管的是“芯片系列”级支持处于架构之下、板卡之上。比如soc/arm/nordic_nrf/下面管的是 Nordic nRF 系列共用的时钟配置、电源管理、Flash 分区默认值soc/arm/st_stm32/下面则按系列再细分。这里的代码往往和厂商 HAL 库配合通过 Kconfig 把具体型号的默认配置编译进固件。boards/则是具体板卡级。一块开发板在 Zephyr 中的目录通常长这样boards/arm/nrf52840dk_nrf52840/里面有板级设备树nrf52840dk_nrf52840.dts、板级默认配置nrf52840dk_nrf52840_defconfig、板卡头文件board.h、pinctrl 定义文件。同一个 SoC 可以衍生出几十块不同板子它们共享soc/下的芯片级代码各自只维护自己的板级差异。2.3 顶层 Kconfig.zephyr、CMakeLists.txt 与 west.yml 的定位很多人扫根目录时会忽略这三个特殊文件但其实整个构建流程的入口都在这里。Kconfig.zephyr是整个 Zephyr 配置系统的总入口它负责用source语句把所有子目录的 Kconfig 文件串起来。可以把它理解成配置系统的“总目录索引”。你在应用工程里设置CONFIG_LOGyKconfig 系统会顺着这条 source 链找到subsys/logging/Kconfig中定义的LOG选项。CMakeLists.txt是构建系统的顶层入口。它负责包含cmake/modules/下的各种脚本定义zephyr这个超级库并决定哪些子目录会被加入构建。最新代码里你会看到它把arch、kernel、lib、drivers、subsys等等都纳入到一个统一的构建流程中。west.yml这个文件值得特别说明它本身描述的是这个仓库依赖哪些外部仓库。如果你用west init初始化的项目以zephyr仓库作为 manifest 仓库那么west.yml就是整个项目依赖关系的源头。如果你用的是自定义 manifest 仓库那这个文件就成了“参考实现”。3. subsys 子系统全景从蓝牙到日志再到文件系统3.1 subsys 目录为什么这么大它和 drivers 的分界线subsys/是 Zephyr 目录结构里最能体现“RTOS 全家桶”思路的地方。它包含了一堆“不是驱动但又是系统重要组成部分”的代码。这里有一个刚接触 Zephyr 的人最容易问的问题为什么蓝牙协议栈放在subsys/bluetooth/而不是drivers/里我的判断标准很简单如果一段代码主要是在“操作硬件寄存器”它是驱动如果一段代码主要是在“实现一种通信协议、一套算法或者一个应用框架”它是子系统。蓝牙里当然有操作射频硬件的部分那部分在蓝牙协议栈内部以 Controller 和 HCI 传输层的形式存在但 GATT、GAP、L2CAP、Mesh 这些协议逻辑显然不是“驱动一个外设”能概括的。网络协议栈同理subsys/net/里实现的 TCP/IP 协议栈、CoAP、MQTT 这些都是与具体硬件无关的协议逻辑。另一个判断方式是驱动通常面向“单一外设”子系统面向“一类业务”。比如一个温湿度传感器驱动放在drivers/sensor/但把所有传感器数据统一管理、提供 sensor 抽象层的框架逻辑可能在subsys/sensing/一个 UART 驱动放在drivers/serial/但把 UART 抽象成系统日志输出通道的框架逻辑在subsys/logging/的 backend 里。3.2 按功能拆解 subsys 内部结构不同版本的 Zephyr 中subsys/的子目录会略有差异我以当前较新的版本为例按功能分类来看网络与连接类子目录内容subsys/net/网络协议栈、L2 链路层、IP/TCP/UDP、socket 实现subsys/net/lib/CoAP、MQTT、LwM2M、DNS、DHCP、WebSocket 等应用层协议subsys/bluetooth/蓝牙 Host 协议栈、Controller、Mesh、服务实现subsys/canbus/CAN 总线协议包括 CANopensubsys/usb/USB 设备协议栈subsys/modbus/Modbus 协议实现subsys/ppp/点对点协议实现基础软件服务类子目录内容subsys/logging/日志框架包括后端、格式控制subsys/shell/Shell 子系统命令注册与后端subsys/settings/键值对设置持久化框架subsys/fs/文件系统抽象接 littlefs、FatFS 等subsys/storage/Flash 分区、Stream Flash 等存储服务subsys/console/历史遗留的控制台输入输出逻辑subsys/dfu/固件升级相关工具类模块subsys/mgmt/设备管理协议比如 MCUmgrsubsys/task_wdt/任务看门狗框架subsys/timers/软件定时器框架系统框架与底层辅助类子目录内容subsys/pm/电源管理状态机subsys/testsuite/Zephyr 自带的测试框架ztest就在这subsys/tracing/系统跟踪工具支持subsys/profiling/性能分析接口subsys/stats/统计信息收集框架subsys/ipc/核间通信服务比如 RPMsg 封装subsys/portability/可移植层比如 CMSIS 相关封装subsys/random/随机数熵源上层封装subsys/sd/SD 卡子系统subsys/dsp/数字信号处理库封装这个表格不必背但你一定要记住一件事当你在 Zephyr 中要找某个功能时如果它不属于“某个外设的驱动”先去subsys/对应子目录翻一圈。很多从厂商 SDK 转过来的开发者习惯去drivers/里找一切结果在 Zephyr 里找半天也找不到蓝牙协议栈。3.3 一个典型应用会用到 subsys 里的哪些目录拿一个实际项目来举例。比如做一个 BLE 温湿度采集设备带屏幕显示、通过 USB 或者 UART 连 Shell 调试配置参数需要掉电保存。这个项目在 Zephyr 中会用到哪些目录蓝牙功能subsys/bluetooth/用 GATT 服务上报温湿度数据。日志输出subsys/logging/配合drivers/serial/的 UART backend。Shell 调试subsys/shell/注册自定义命令比如手动校准传感器。参数持久化subsys/settings/把校准值、设备地址存到 Flash。文件系统subsys/fs/subsys/storage/如果要把历史数据记到 littlefs或者通过 Flash Map 管理分区。电源管理subsys/pm/设备电池供电需要低功耗状态机。这些子系统之间不是孤立的。比如日志框架要从settings/里读取当前日志级别配置Shell 的一条命令可能最终触发一个pm/的状态切换。目录结构在物理上把它们分开了但 Kconfig 和 CMake 会在构建时把它们按需组合起来。4. 构建系统怎么把目录“编”进固件4.1 Kconfig 的 source 链与目录层级Zephyr 的目录结构不是给人看的摆设构建系统真的会顺着目录一层层找过去。Kconfig 系统从Kconfig.zephyr开始用source语句把各顶层目录的 Kconfig 引入。比如subsys/Kconfig里面会继续source各个子系统的 Kconfigsubsys/net/Kconfig又会继续source网络栈下面各个功能的 Kconfig。这意味着每个模块的配置项定义位置和它的源码目录位置是对应的。想查某个配置项在哪定义先猜它在哪个目录然后去那个目录的 Kconfig 或者 Kconfig.* 系列文件里 grep通常几秒就能找到。有一个非常实用的技巧构建完成后在 build 目录下会生成一个扁平的 Kconfig 配置输出你可以直接看到最终生效的配置项。如果怀疑某个CONFIG_XXX没有生效先grep CONFIG_XXX build/Kconfig看看它到底有没有被选中以及是被哪个依赖条件卡住了。4.2 CMake 扫描目录的机制自动还是手动Kconfig 决定了“要不要编译某段代码”CMake 决定了“怎么把目录里的源文件编进来”。Zephyr 的 CMake 体系基于一个重要的概念zephyr库。所有需要编进固件的源文件最终都要通过zephyr_library系列函数挂到这个库上。各目录的CMakeLists.txt就是这个挂载过程。比如drivers/serial/CMakeLists.txt里会有类似zephyr_library_sources(uart_xyz.c)的语句把某个 UART 驱动源文件加入构建。zephyr_library_sources_ifdef(CONFIG_XYZ ...)这类变体则会根据 Kconfig 条件决定是否添加。这里有一个常见的误解以为 Zephyr 会自动编译目录下所有.c文件。不是的。目录只是组织代码的容器Kconfig 才是编译的开关CMakeLists.txt 才是加入构建的动作。如果你往一个既有驱动目录里丢了一个新.c文件但没改CMakeLists.txt它不会被编译而且不会有任何报错——就是安静地不编译。这种“文件在符号却找不到”的问题排查起来相当费时间。4.3 include/zephyr 前缀变化头文件目录的演进include/目录近几个版本经历了一次大调整这也是很容易引发编译错误的地方。早期 Zephyr 把公共头文件直接放在include/根目录下应用代码习惯写#include kernel.h、#include device.h。从 3.2 左右开始社区把所有公开头文件逐步迁入include/zephyr/子目录于是标准写法变成了#include zephyr/kernel.h、#include zephyr/device.h。迁移过程中为了兼容旧路径还保留了一段时间的转发头文件所以很多老代码看起来还能编译。但到了较新的版本部分旧路径的转发头文件已经被移除。我见过不少人在升级 Zephyr 版本后编译报出一堆fatal error: kernel.h: No such file or directory一查全是老写法。你现在新写的代码无论当前版本是否还兼容旧路径一律按新路径写。#include zephyr/kernel.h多敲几个字母换来的是升级无痛。5. 实际开发中如何利用目录结构5.1 新增板卡动 boards、soc、dts 哪几个目录Zephyr 的目录结构直接决定了 BSP 移植的工作流。想把一块新板卡跑起来大概分两种情况。如果芯片已经有 Zephyr 支持只是换了个板子那主要工作在boards/目录。选一块最接近的参考板复制目录然后修改板级设备树.dts、默认配置_defconfig、板级头文件board.h、pinctrl 配置文件。设备树里要确认主控型号、Flash/RAM 大小、时钟配置和外设引脚。如果芯片本身不在 Zephyr 支持列表里工作量就要延伸到soc/和dts/。需要在soc/arch/vendor/下建立芯片系列目录编写Kconfig.soc、Kconfig.defconfig、SoC 级设备树dtsi还要处理中断控制器、时钟、Flash 控制器等基础外设的绑定关系。这个工作量相当大如果不是公司项目层面的需要通常不建议自己从零做优先考虑找一个功能相近的芯片系列做修改。不管哪种情况都要注意boards/Kconfig、soc/Kconfig这些汇总文件中是否已经包含了新目录的source语句。很多第一次移植的人改了三天代码结果编译时板子根本找不到原因就是忘了在上级 Kconfig 里加一行 source。5.2 新驱动与新子系统应该放哪里自己做项目时经常会有一个功能模块不知道该塞到哪个目录。我建议用下面这个判断流程它是不是在操作某个具体外设的寄存器如果是放drivers/外设类别/比如传感器放drivers/sensor/、按键输入相关放drivers/input/同时补 Kconfig 和 CMakeLists.txt。它是不是在实现某种协议或框架不依赖具体硬件如果是放subsys/功能名/并按 Zephyr 惯例建Kconfig、CMakeLists.txt、可能的include/子目录。它是不是一个可以独立复用、无 RTOS 依赖的算法库可以考虑lib/或作为独立 west 模块放到modules/下。它是不是一个独立第三方组件有自己的版本管理需求那应该做成独立仓库通过west.yml引入然后在modules/里写接口文件。这个分类不是死的但核心原则是不要自己另搞一套目录结构。Zephyr 的构建工具和 Kconfig 扫描体系都默认按照既有目录规则工作你另起炉灶会让后续维护变得非常痛苦。5.3 日常定位代码的几个快捷方式目录结构知道了还要知道怎么快速找到目标代码。有几个方法我在实际开发中天天用找板子ls boards/arm/或直接ls boards/ | grep 厂商确认官方支持的板卡名。找 SoC 设备树定义find dts/ -name *nrf52*芯片的 dtsi 文件一般按厂商和系列组织。找符号定义grep -rn 函数名 --include*.c --include*.h 目录先限定在subsys/或drivers/范围里搜比全仓库搜快很多。找 Kconfig 选项grep -r config LOG_MODE_IMMEDIATE subsys/logging/Kconfig*定位配置项的位置。查看最终配置grep CONFIG_LOG build/Kconfig确认实际编译时某个配置是否生效。如果用的是 VS Code 或者 Clion直接建立 compile_commands 索引然后鼠标点跳转更省力。Zephyr 构建默认可以生成compile_commands.json配置好之后查找定义就是一次 Ctrl点击的事。6. 目录结构引发的三个经典坑6.1 头文件前缀踩坑kernel.h 与 zephyr/kernel.h前面提过的头文件迁移问题值得单独拿出来讲因为它在社区里出现的频率太高了。现象是从老版本升级工程后编译报错找不到kernel.h、device.h、init.h等常用头文件。根因就是这些头文件从include/根目录迁移到了include/zephyr/子目录写不写zephyr/前缀决定了编译器能不能找到。处理方式很直接全局替换#include xxx.h为#include zephyr/xxx.h并把#include xxx.h的本地引用也检查一遍。同时不要去动include/zephyr/下的头文件结构也不要用-I强行把旧路径加回编译参数那只会掩盖问题让后续升级继续踩坑。6.2 SOC 目录迁移带来的旧教程失效Zephyr 在 3.5 前后做过一次大规模目录调整把arch/arch/soc/整体迁移到了顶层soc/目录。你现在看很多老教程和网上代码片段里面写的是arch/arm/soc/nordic_nrf/之类路径但实际新版本里这些文件已经搬到了soc/arm/nordic_nrf/。这个坑非常隐蔽。因为你ls去看arch/arm/时能看到soc目录吗不一定。如果是迁移不干净的版本可能只留了一个空壳真正的 Kconfig 文件已经不在那了。你在老路径下改文件改的可能是并不参与构建的残留副本编译结果当然不会有变化。解决方案也很简单遇到和 SOC 相关的路径先在soc/顶层目录下确认一遍。如果发现自己的操作路径和实际仓库结构对不上大概率是版本差异导致的路径迁移。不要硬套老教程去查当前版本的文档或者直接find定位。6.3 自定义目录不被 CMake 扫描的陷阱最后一个坑是关于“我自己建的目录”。Zephyr 不会因为你建了一个新目录就自动编译它。有次我在项目里想加一个本地算法库放在 zephyr 仓库同级目录下想着反正链接时能把.o文件加进去。结果构建了二十分钟链接时一堆未定义符号。排查了半天才发现算法库的源文件压根没有被编译——CMake 根本没扫描那个目录。Zephyr 里的源文件加入构建有三种典型方式放到既有目录里比如drivers/下对应的子目录并在该目录的CMakeLists.txt里用zephyr_library_sources()显式添加。通过 CMake 的add_subdirectory()手动添加你的自定义目录并在里面用 zephyr_library 系列函数挂载源文件。通过 Kconfig 条件配合zephyr_library_sources_ifdef()让某个配置开关控制是否编译。自定义目录最常用的是第二种方式。注意要放在应用工程的 CMakeLists 里处理而不是直接改 Zephyr 仓库的顶层 CMakeLists否则每次 west update 都可能被覆盖。最后分享一个我现在的习惯拿到一个新版本的 Zephyr先不看文档而是先跑一遍find和ls把目录结构在脑子里重新过一遍。Zephyr 的目录本身就是它最好的注释很多功能你甚至不用看源码光看目录名、Kconfig 文件名和 CMakeLists 里的挂载关系就能猜出七八成。这个项目真正难的不是写代码而是先学会“按地图找东西”目录结构就是那张地图值得花一下午好好摸一遍。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →