Zephyr学习记录(3):项目结构与配置

成元 2026-05-01 { 教程, 笔记 } [ Zephyr ]

前言

上一篇主要记录了 westmanifest,那一篇更偏“工作空间层面”的理解。

这一篇继续往下走,看看真正开始写 Zephyr 应用时,工程目录通常长什么样,以及几个最常见的配置文件分别负责什么。

刚接触 Zephyr 的时候,这一块很容易看得有点乱:

把这些关系理顺之后,后面看 Kconfig、设备树、模块和自定义板级支持时会顺很多。

Zephyr 为什么强调“应用工程”

官方文档里有一个很重要的说法:Zephyr 的构建系统是 application-centric 的。

我的理解是:
不是你先单独把 Zephyr “编出来”,再去链接自己的业务代码;而是由应用工程发起整个构建过程,然后把应用代码、Zephyr 内核、各类模块一起编成最终镜像。

也就是说,在 Zephyr 的世界里,应用目录不是一个可有可无的例子目录,而是整个构建流程的入口。

所以我们平时真正要关心的,是:

一个最小 Zephyr 应用长什么样

官方文档给出的最小应用结构大致如下:

<app>/
├── CMakeLists.txt
├── app.overlay
├── prj.conf
├── VERSION
└── src/
    └── main.c

如果把它翻译成更容易理解的话,大概就是:

这里最值得先记住的一点是:
prj.conf 管软件功能,app.overlay 管硬件描述。

这两个文件名字经常一起出现,但职责并不一样。

三种常见的应用位置

根据官方文档,Zephyr 应用大致可以分成三类。

1. Zephyr repository application

也就是应用直接放在 zephyr/ 仓库里面。

最典型的例子就是官方自带的 sample:

zephyrproject/
├── .west/
└── zephyr/
    ├── samples/
    │   └── hello_world/
    └── tests/

这种方式的特点是:

2. Zephyr workspace application

也就是应用位于 west 工作空间中,但不在 zephyr/ 仓库内部。

例如:

zephyrproject/
├── .west/
├── zephyr/
├── modules/
├── bootloader/
└── applications/
    └── app/

我觉得这类结构是最常见、也最顺手的一种。

因为它有几个好处:

3. Zephyr freestanding application

也就是应用不在 west workspace 里,而是在别的地方单独放着。

例如:

<home>/
├── zephyrproject/
│   ├── .west/
│   ├── zephyr/
│   └── modules/
└── app/

这种方式也能用,但通常会稍微麻烦一些。

因为你需要额外确保 Zephyr 安装位置能被正确找到,比如依赖 ZEPHYR_BASE 或其它构建配置。

所以如果只是正常学习和做项目,我目前更倾向于:

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)

这几行分别可以这样理解:

其中最关键的还是 find_package(Zephyr)

因为它相当于告诉 CMake:
“我不是一个普通的 CMake 小工程,我要接入 Zephyr 的整套构建逻辑。”

如果只写到入门阶段,我觉得对 CMakeLists.txt 先抓住两件事就够了:

prj.conf:软件功能配置

prj.conf 是应用最常见的 Kconfig 配置片段。

它主要用于打开或关闭 Zephyr 的各种软件功能。

比如:

CONFIG_GPIO=y
CONFIG_SERIAL=y
CONFIG_CONSOLE=y

这些配置的关注点是“我要什么软件能力”,例如:

所以当你遇到“某个 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

这里可以粗略理解成:

这种方式很适合做“同一套代码,不同功能组合”的构建变体。

app.overlay:硬件描述修改

如果说 prj.conf 更偏软件配置,那 app.overlay 就更偏硬件配置。

它用于在板卡原始 devicetree 的基础上做补充或修改。

常见场景例如:

所以当你遇到“引脚怎么配”“这个外设实例在哪开”“板上器件在设备树里怎么描述”这类问题时,通常要看的是 overlay,而不是 prj.conf

这也是很多初学者最容易混淆的地方:

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

可以把它理解成:

这样做的好处是非常明显的:

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_DIRCONF_FILEFILE_SUFFIX 这些变量怎么理解

看到这里时,很多人会开始碰到一些“不是默认文件名”的玩法。

我觉得可以先把它们分成三类。

1. 改配置目录

APPLICATION_CONFIG_DIR 用来指定应用配置目录。

默认情况下,Zephyr 会从应用源码目录里找配置文件;如果你显式指定了 APPLICATION_CONFIG_DIR,那它就会改去你指定的位置找。

这个能力更适合稍复杂一些的工程。
入门阶段先知道“可以改配置目录”就够了。

2. 改配置文件

CONF_FILEEXTRA_CONF_FILEDTC_OVERLAY_FILEEXTRA_DTC_OVERLAY_FILE 这些变量,主要用于指定或追加配置文件。

它们的直觉理解就是:

当你要做不同产品型号、不同调试版本、不同外设组合时,这一套会很好用。

3. 做变体构建

FILE_SUFFIX 用于给配置文件名自动追加后缀。

例如你同时做鼠标版、键盘版两个产品变体时,就可以让同一套工程根据后缀去匹配不同的配置文件,而不是把所有差异都硬塞进一个 prj.conf 里。

这个特性在工程变复杂之后会很有价值,但学习阶段不用一开始就上强度。

一个更贴近实际的推荐结构

如果是我自己在 west workspace 里新开一个小项目,我会更倾向于下面这种组织方式:

zephyrproject/
├── .west/
├── zephyr/
├── modules/
└── applications/
    └── blinky_demo/
        ├── CMakeLists.txt
        ├── prj.conf
        ├── app.overlay
        ├── boards/
        └── src/
            └── main.c

这里的想法很简单:

这样的结构既不乱,也比较符合上一篇里说的 workspace 思路。

从项目结构过渡到构建命令

当应用目录准备好之后,构建其实就自然了。

在应用目录下执行:

west build -b stm32_min_dev@blue

或者显式指定当前目录:

west build -b stm32_min_dev@blue .

这条命令背后做的事情,可以粗略理解成:

所以项目结构并不是“看起来整齐一点”这么简单,
它实际上决定了 Zephyr 在构建时去哪里找源码、找软件配置、找硬件描述。

小结

这一篇如果只抓主线,我觉得至少先记住下面这些点:

把这一层关系搞清楚之后,后面再看 Zephyr 的 Kconfig、设备树和自定义板卡,就不会只是在记零散文件名了。
你会知道每个文件是为了回答哪一类问题而存在的。

参考资料