前言
上一篇主要记录了 west 和 manifest,那一篇更偏“工作空间层面”的理解。
这一篇继续往下走,看看真正开始写 Zephyr 应用时,工程目录通常长什么样,以及几个最常见的配置文件分别负责什么。
刚接触 Zephyr 的时候,这一块很容易看得有点乱:
CMakeLists.txt是干什么的prj.conf和app.overlay有什么区别- 用户工程到底该放在
zephyr/里面,还是放在工作空间外面 - 为什么构建产物又总是在单独的
build/目录里
把这些关系理顺之后,后面看 Kconfig、设备树、模块和自定义板级支持时会顺很多。
Zephyr 为什么强调“应用工程”
官方文档里有一个很重要的说法:Zephyr 的构建系统是 application-centric 的。
我的理解是:
不是你先单独把 Zephyr “编出来”,再去链接自己的业务代码;而是由应用工程发起整个构建过程,然后把应用代码、Zephyr 内核、各类模块一起编成最终镜像。
也就是说,在 Zephyr 的世界里,应用目录不是一个可有可无的例子目录,而是整个构建流程的入口。
所以我们平时真正要关心的,是:
- 我的应用目录里要放哪些文件
- 这些文件怎么告诉 Zephyr “我要开什么软件功能”
- 又怎么告诉 Zephyr “我要怎么改板子的硬件描述”
一个最小 Zephyr 应用长什么样
官方文档给出的最小应用结构大致如下:
<app>/
├── CMakeLists.txt
├── app.overlay
├── prj.conf
├── VERSION
└── src/
└── main.c
如果把它翻译成更容易理解的话,大概就是:
CMakeLists.txt:告诉构建系统“这是一个 Zephyr 应用”,并把源码加进来prj.conf:配置软件功能,也就是 Kconfig 选项app.overlay:配置硬件描述,也就是设备树 overlayVERSION:描述应用版本src/:放应用自己的源代码
这里最值得先记住的一点是:
prj.conf 管软件功能,app.overlay 管硬件描述。
这两个文件名字经常一起出现,但职责并不一样。
三种常见的应用位置
根据官方文档,Zephyr 应用大致可以分成三类。
1. Zephyr repository application
也就是应用直接放在 zephyr/ 仓库里面。
最典型的例子就是官方自带的 sample:
zephyrproject/
├── .west/
└── zephyr/
├── samples/
│ └── hello_world/
└── tests/
这种方式的特点是:
- 离官方代码最近
- 看 sample、做实验时很方便
- 但不太适合作为自己长期维护的业务工程
2. Zephyr workspace application
也就是应用位于 west 工作空间中,但不在 zephyr/ 仓库内部。
例如:
zephyrproject/
├── .west/
├── zephyr/
├── modules/
├── bootloader/
└── applications/
└── app/
我觉得这类结构是最常见、也最顺手的一种。
因为它有几个好处:
- 应用和官方
zephyr/源码分开,不容易把自己的工程和上游源码混在一起 - 仍然处于 workspace 中,
west build、west flash、west debug用起来最自然 - 后面如果要加自己的模块、私有仓库、板级目录,也更容易组织
3. Zephyr freestanding application
也就是应用不在 west workspace 里,而是在别的地方单独放着。
例如:
<home>/
├── zephyrproject/
│ ├── .west/
│ ├── zephyr/
│ └── modules/
└── app/
这种方式也能用,但通常会稍微麻烦一些。
因为你需要额外确保 Zephyr 安装位置能被正确找到,比如依赖 ZEPHYR_BASE 或其它构建配置。
所以如果只是正常学习和做项目,我目前更倾向于:
- 练习 sample:直接看
zephyr/samples/ - 自己做工程:放在 workspace 里,但不要塞进
zephyr/仓库本体
CMakeLists.txt:构建入口
每个 Zephyr 应用都必须有一个 CMakeLists.txt。
最小版本通常长这样:
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr)
project(my_zephyr_app)
target_sources(app PRIVATE src/main.c)
这几行分别可以这样理解:
cmake_minimum_required(...):声明最低 CMake 版本find_package(Zephyr):把 Zephyr 构建系统拉进来project(...):声明工程名target_sources(app PRIVATE ...):把应用源码加入app这个目标
其中最关键的还是 find_package(Zephyr)。
因为它相当于告诉 CMake:
“我不是一个普通的 CMake 小工程,我要接入 Zephyr 的整套构建逻辑。”
如果只写到入门阶段,我觉得对 CMakeLists.txt 先抓住两件事就够了:
- 它是应用构建入口
- 它负责把你的源码挂到 Zephyr 的
app目标上
prj.conf:软件功能配置
prj.conf 是应用最常见的 Kconfig 配置片段。
它主要用于打开或关闭 Zephyr 的各种软件功能。
比如:
CONFIG_GPIO=y
CONFIG_SERIAL=y
CONFIG_CONSOLE=y
这些配置的关注点是“我要什么软件能力”,例如:
- 要不要 GPIO
- 要不要串口
- 要不要 shell
- 要不要日志
- 要不要某个驱动子系统
所以当你遇到“某个 API 能不能用”“某个子系统为什么没打开”这类问题时,通常第一反应就该去看 prj.conf。
prj.conf 的几个实用理解
第一,prj.conf 不是最终配置文件,而是输入片段。
构建时它会和板卡默认配置、SoC 配置以及其它配置来源合并,最后生成真正参与编译的配置结果。
第二,prj.conf 默认会被自动查找。
也就是说,大多数普通应用不需要额外告诉构建系统它在哪,放在应用目录下就行。
第三,Zephyr 也支持额外的配置片段。
例如:
west build -b stm32_min_dev@blue -- -DCONF_FILE=prj.conf
west build -b stm32_min_dev@blue -- -DEXTRA_CONF_FILE=debug.conf
这里可以粗略理解成:
CONF_FILE:指定主要配置文件EXTRA_CONF_FILE:在默认配置基础上,再额外混入一些配置
这种方式很适合做“同一套代码,不同功能组合”的构建变体。
app.overlay:硬件描述修改
如果说 prj.conf 更偏软件配置,那 app.overlay 就更偏硬件配置。
它用于在板卡原始 devicetree 的基础上做补充或修改。
常见场景例如:
- 给某个 GPIO 起别名
- 修改某个外设节点状态
- 指定 SPI/I2C 外设挂了什么器件
- 配置 LED、按键、串口等硬件资源
所以当你遇到“引脚怎么配”“这个外设实例在哪开”“板上器件在设备树里怎么描述”这类问题时,通常要看的是 overlay,而不是 prj.conf。
这也是很多初学者最容易混淆的地方:
CONFIG_...这类东西通常去prj.conf/ { ... }、&uart1 { ... }、&spi1 { ... }这类东西通常去app.overlay
VERSION:版本信息
这个文件在刚入门时不一定每次都用得上,但官方文档把它放在最小应用结构里,说明它并不是摆设。
它主要用于管理应用版本信息。
如果后面工程要做:
- 固件版本管理
- 镜像签名
- 发布流程自动化
那 VERSION 往往就会开始有价值。
如果只是学习阶段,可以先知道它的作用,不一定一开始就重点投入精力。
src/:放应用自己的代码
官方建议把应用源码放在 src/ 子目录下。
这么做的好处很直接:
- 项目文件和源码分开
- 目录更整洁
- 后面扩展多个源文件时更自然
例如:
app/
├── CMakeLists.txt
├── prj.conf
├── app.overlay
└── src/
├── main.c
├── led.c
└── button.c
如果后面工程再复杂一点,也完全可以继续细分:
app/
├── CMakeLists.txt
├── prj.conf
├── app.overlay
├── include/
├── src/
│ ├── main.c
│ ├── drivers/
│ └── modules/
└── boards/
只不过在 Zephyr 的语境下,src/ 仍然是最常见、最推荐的默认入口。
boards/ 目录:按板卡放差异化配置
当一个应用需要支持多个板子时,只靠一个通用的 prj.conf 和一个通用的 app.overlay 往往不够。
这时候通常会用到 boards/ 目录来放板级差异配置。
一个很典型的思路是:
app/
├── CMakeLists.txt
├── prj.conf
├── app.overlay
├── boards/
│ ├── stm32_min_dev_blue.conf
│ ├── stm32_min_dev_blue.overlay
│ ├── native_sim.conf
│ └── native_sim.overlay
└── src/
└── main.c
可以把它理解成:
- 通用配置放在根目录的
prj.conf/app.overlay - 某块板子独有的差异,放进
boards/
这样做的好处是非常明显的:
- 公共逻辑不会被板级细节污染
- 多板支持时更容易维护
- 后面排查“为什么这个板能跑、那个板不行”时也更直观
build/ 目录:构建产物和源码分离
Zephyr 不支持 in-tree build,也就是不会把构建产物直接混写到源码树里。
所以你每次构建时,都会得到一个单独的 build/ 目录。
例如:
west build -b stm32_min_dev@blue
之后应用目录下通常会出现:
app/
├── CMakeLists.txt
├── prj.conf
├── app.overlay
├── src/
└── build/
这样做的好处是:
- 源码目录更干净
- 切换配置时更容易清理和重建
- 不会把中间文件和真正的工程文件混在一起
这也是为什么我们在看 Zephyr 工程时,要有个习惯:
应用目录是“输入”,build/ 目录是“输出”。
APPLICATION_CONFIG_DIR、CONF_FILE、FILE_SUFFIX 这些变量怎么理解
看到这里时,很多人会开始碰到一些“不是默认文件名”的玩法。
我觉得可以先把它们分成三类。
1. 改配置目录
APPLICATION_CONFIG_DIR 用来指定应用配置目录。
默认情况下,Zephyr 会从应用源码目录里找配置文件;如果你显式指定了 APPLICATION_CONFIG_DIR,那它就会改去你指定的位置找。
这个能力更适合稍复杂一些的工程。
入门阶段先知道“可以改配置目录”就够了。
2. 改配置文件
CONF_FILE、EXTRA_CONF_FILE、DTC_OVERLAY_FILE、EXTRA_DTC_OVERLAY_FILE 这些变量,主要用于指定或追加配置文件。
它们的直觉理解就是:
CONF_FILE:主 Kconfig 片段EXTRA_CONF_FILE:额外 Kconfig 片段DTC_OVERLAY_FILE:主设备树 overlayEXTRA_DTC_OVERLAY_FILE:额外 overlay
当你要做不同产品型号、不同调试版本、不同外设组合时,这一套会很好用。
3. 做变体构建
FILE_SUFFIX 用于给配置文件名自动追加后缀。
例如你同时做鼠标版、键盘版两个产品变体时,就可以让同一套工程根据后缀去匹配不同的配置文件,而不是把所有差异都硬塞进一个 prj.conf 里。
这个特性在工程变复杂之后会很有价值,但学习阶段不用一开始就上强度。
一个更贴近实际的推荐结构
如果是我自己在 west workspace 里新开一个小项目,我会更倾向于下面这种组织方式:
zephyrproject/
├── .west/
├── zephyr/
├── modules/
└── applications/
└── blinky_demo/
├── CMakeLists.txt
├── prj.conf
├── app.overlay
├── boards/
└── src/
└── main.c
这里的想法很简单:
zephyr/继续保持官方仓库本体- 自己的应用放到
applications/ - 如果后面板级差异增多,就往
boards/里收 - 如果工程再扩大,再考虑拆
include/、自定义模块或者独立仓库
这样的结构既不乱,也比较符合上一篇里说的 workspace 思路。
从项目结构过渡到构建命令
当应用目录准备好之后,构建其实就自然了。
在应用目录下执行:
west build -b stm32_min_dev@blue
或者显式指定当前目录:
west build -b stm32_min_dev@blue .
这条命令背后做的事情,可以粗略理解成:
- 读取
CMakeLists.txt - 读取
prj.conf等 Kconfig 配置 - 读取
app.overlay等设备树 overlay - 结合板卡定义、Zephyr 内核和模块一起生成构建系统
- 把产物输出到
build/
所以项目结构并不是“看起来整齐一点”这么简单,
它实际上决定了 Zephyr 在构建时去哪里找源码、找软件配置、找硬件描述。
小结
这一篇如果只抓主线,我觉得至少先记住下面这些点:
- Zephyr 的构建是以应用工程为中心发起的
CMakeLists.txt是构建入口prj.conf主要管软件功能,也就是 Kconfigapp.overlay主要管硬件描述,也就是 devicetree overlaysrc/一般放应用源码boards/很适合放不同板卡的差异配置- 构建产物会进入单独的
build/目录,而不是混在源码里 - 自己长期维护的应用,更推荐放在 workspace 中,但不要直接改成
zephyr/仓库的一部分
把这一层关系搞清楚之后,后面再看 Zephyr 的 Kconfig、设备树和自定义板卡,就不会只是在记零散文件名了。
你会知道每个文件是为了回答哪一类问题而存在的。