本文是「Zephyr 内核从入门到精通」系列第 05 篇。上一篇讲透了设备树,本篇讲它的黄金搭档 Kconfig——它和设备树什么关系、prj.conf 写在哪、menuconfig 怎么一步步操作、开关一个功能固件体积怎么变、配置不生效怎么排查。
全文保姆级:每一步都标清「在哪做、怎么做、应该看到什么」,配一个完整可编译的小工程,照着抄就能跑。建议先点赞收藏,照着做一遍。
目录
- 一、Kconfig vs 设备树:一句话分清
- 二、Kconfig 基本语法(符号类型 & 依赖 & select)
- 三、配置从哪来、谁说了算(优先级)
- 四、固件裁剪的原理:配置怎么变成代码
- 五、完整实战工程:开关 LOG / SHELL,看固件体积怎么变
- 六、menuconfig / guiconfig 一步步操作(保姆级图解)
- 七、多配置文件 & 代码内条件编译
- 八、强化版高频报错排查表(13 条)
- 九、总结
一、Kconfig vs 设备树:一句话分清
很多新手把 Kconfig 和设备树搞混,因为两者都在「配置」。但分工非常明确:
设备树回答「硬件长什么样」,Kconfig 回答「这次启用哪些功能、给多少资源」。

用装修类比:设备树是装修图纸(房子结构,固定不变),Kconfig 是功能清单(装不装空调、要不要地暖,按预算勾选)。
| 问题 | 归属 |
|---|---|
| LED 接在哪个引脚 | 设备树 |
| I2C 总线上挂了哪个传感器、地址多少 | 设备树 |
| 是否启用蓝牙 / 日志 / Shell | Kconfig |
| 主线程栈大小 | Kconfig |
| 日志默认等级 | Kconfig |
判断窍门:问「硬件本来长什么样」找设备树;问「这次要不要、给多少」找 Kconfig。
注意:启用一个驱动通常需要两边配合——设备树里有对应节点且
status = "okay",Kconfig 里开对应子系统(如CONFIG_I2C=y)。缺一个都跑不起来,这是新手最容易踩的坑。
二、Kconfig 基本语法
2.1 你每天写的 prj.conf 长这样
日常 99% 的配置都写在工程目录下的 prj.conf:
# ====== bool:布尔开关,y 开 / n 关 ======
CONFIG_GPIO=y
CONFIG_BT=n
# ====== int:整数 ======
CONFIG_MAIN_STACK_SIZE=2048
CONFIG_LOG_DEFAULT_LEVEL=3
# ====== hex:十六进制 ======
CONFIG_SRAM_BASE_ADDRESS=0x20000000
# ====== string:字符串(带引号)======
CONFIG_BT_DEVICE_NAME="MyDevice"
四种符号类型记住即可:
| 类型 | 取值 | 例子 |
|---|---|---|
bool | y / n | CONFIG_LOG=y |
int | 整数 | CONFIG_MAIN_STACK_SIZE=2048 |
hex | 0x 开头 | CONFIG_SRAM_BASE_ADDRESS=0x20000000 |
string | 双引号包裹 | CONFIG_BT_DEVICE_NAME="MyDev" |
每个
CONFIG_xxx都是某个Kconfig文件里config xxx定义出来的(注意定义里没有 CONFIG_ 前缀,引用时才加)。定义里包含:类型、默认值、依赖、帮助文本。
2.2 depends on:依赖(新手必懂,排错第一坑)
Kconfig 符号之间有依赖关系。看一个 Zephyr 源码里的真实例子:
config BT_PERIPHERAL
bool "Peripheral Role support"
depends on BT # 依赖 BT,必须先 BT=y
如果你没开 CONFIG_BT,却写了 CONFIG_BT_PERIPHERAL=y,结果是这一行无效——不是「写错了」,而是「依赖未满足,系统直接忽略你的选择」。
这种情况在 menuconfig 里表现为该项灰色、无法用空格勾选。这是「我明明 =y 了却没生效」最常见的原因,没有之一。
2.3 select:强制选中(反向开依赖)
select 是 depends on 的反方向——开 A 时自动把 B 也打开:
config BT_PERIPHERAL
bool "Peripheral Role support"
select BT_BROADCASTER # 开我,自动也开 BT_BROADCASTER
小心:
select会强制打开被选项,哪怕被选项自己的依赖没满足,可能导致诡异冲突。新手日常基本不用手写select,知道它存在、能看懂报错即可。
三、配置从哪来、谁说了算(优先级)
板子有出厂默认配置,你又写了 prj.conf,还可能用 menuconfig 临时改,到底谁说了算?

优先级从低到高(越靠近「本次构建」的越大):
模块默认值(default) < 板级 <board>_defconfig < prj.conf < 额外 conf(EXTRA_CONF_FILE) < menuconfig 临时改动
最低 最高
- 模块默认值:各子系统
Kconfig文件里的default,最底层兜底。 - 板级 defconfig:
boards/.../<board>_defconfig,这块板子的出厂默认(比如默认开了哪个串口)。 - prj.conf:你的工程配置,日常主战场。
- 额外 conf:通过
-DEXTRA_CONF_FILE=xxx.conf叠加的文件,会覆盖 prj.conf。 - menuconfig:构建时手动改的临时值,优先级最高,但只存在于构建目录,pristine 重建就丢。
记忆口诀:离「本次构建」越近,优先级越高。 不管来源多复杂,所有配置最终合并成一份文件:
build/zephyr/.config。排错只认它。
四、固件裁剪的原理:配置怎么变成代码
这是 Kconfig 的核心价值,也是 Zephyr 能同时覆盖几十 KB 小 MCU 和高端芯片的根本原因。

完整链条:
prj.conf / defconfig / menuconfig
│ Kconfig 系统:求解依赖、合并所有来源
▼
build/zephyr/.config ← 「真相之源」,合并后的最终配置
│ 生成
▼
build/zephyr/include/generated/.../autoconf.h ← #define CONFIG_xxx 1
│ 编译期被代码包含
▼
你的代码:#if defined(CONFIG_xxx) 条件编译
关键:配置在编译期生效。 当 CONFIG_BT=n 时,蓝牙相关代码根本不会被编译进固件:
关闭的功能 = 0 行代码、0 字节 RAM、0 字节 Flash 开销。
所以「裁剪固件」不是运行时省内存,而是编译时就不把不需要的东西放进去。下面的实战工程会让你亲眼看到固件体积随配置变化。
五、完整实战工程:开关 LOG / SHELL,看固件体积怎么变
下面这个小工程完整可编译,目标:开/关 CONFIG_LOG 和 CONFIG_SHELL,对比固件 FLASH/RAM 用量,亲手感受裁剪效果。
本文以
nrf52840dk/nrf52840为例(无板子也能 build 看体积)。换成你自己的板子,把-b后面替换即可。
5.1 工程目录结构(prj.conf 到底放哪?)
在任意目录新建工程文件夹,结构如下。prj.conf 就放在工程根目录,和 CMakeLists.txt 平级:
kconfig_demo/ ← 工程根目录
├── CMakeLists.txt ← 构建脚本
├── prj.conf ← 主配置(必须在这里,名字固定)
├── debug.conf ← 额外调试配置(可选叠加)
└── src/
└── main.c ← 应用代码
5.2 CMakeLists.txt
# CMakeLists.txt
cmake_minimum_required(VERSION 3.20.0)
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})
project(kconfig_demo)
target_sources(app PRIVATE src/main.c)
5.3 prj.conf(默认:精简版,LOG/SHELL 都关)
# prj.conf —— 工程根目录,名字固定不能改
CONFIG_GPIO=y
# 先全部关掉,作为「精简基线」
CONFIG_LOG=n
CONFIG_SHELL=n
5.4 debug.conf(额外叠加:打开 LOG 和 SHELL)
# debug.conf —— 开发调试时叠加,开日志 + Shell
CONFIG_LOG=y
CONFIG_LOG_DEFAULT_LEVEL=4
CONFIG_SHELL=y
CONFIG_THREAD_NAME=y
5.5 src/main.c(同一份代码,跟着配置自动裁剪)
/* src/main.c */
#include <zephyr/kernel.h>
/* 只有 CONFIG_LOG=y 时,这段日志代码才会被编译进固件 */
#if defined(CONFIG_LOG)
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(demo, LOG_LEVEL_INF);
#endif
int main(void)
{
#if defined(CONFIG_LOG)
/* CONFIG_LOG=n 时,这两行连同 LOG 子系统都不会进固件 */
LOG_INF("Hello from demo, LOG is ON");
LOG_INF("main stack size = %d bytes", CONFIG_MAIN_STACK_SIZE);
#else
/* CONFIG_LOG=n 时走这里,用最朴素的 printk */
printk("LOG is OFF, this is printk\n");
#endif
while (1) {
k_msleep(1000);
}
return 0;
}
注意两个用法:
#if defined(CONFIG_LOG):判断某个 bool 配置是否为 y,做条件编译。CONFIG_MAIN_STACK_SIZE:直接当普通宏(整数)用在代码里。
5.6 第一次构建:精简版(LOG/SHELL = 关)
在工程根目录打开终端,执行(-p always 表示干净重建,避免缓存干扰):
cd kconfig_demo
west build -p always -b nrf52840dk/nrf52840 .
构建结束,终端最后会打印 Memory region 用量表(这就是固件体积),类似:
Memory region Used Size Region Size %age Used
FLASH: 21456 B 1 MB 2.05%
RAM: 6240 B 256 KB 2.38%
记下这两个数字(FLASH ≈ 21 KB,RAM ≈ 6 KB,数值随版本/板子不同,以你本地为准)。
5.7 第二次构建:叠加 debug.conf(开 LOG + SHELL)
同样在工程根目录执行,用 -DEXTRA_CONF_FILE 把 debug.conf 叠加上去:
west build -p always -b nrf52840dk/nrf52840 . -- -DEXTRA_CONF_FILE=debug.conf
多个额外配置文件用分号分隔:
-DEXTRA_CONF_FILE="debug.conf;extra.conf"(Windows PowerShell 下整体加引号)。
构建完看体积,会明显变大,类似:
Memory region Used Size Region Size %age Used
FLASH: 58320 B 1 MB 5.56%
RAM: 12480 B 256 KB 4.76%
【📷 截图位:west build 终端体积表(叠加 debug.conf 后,明显变大)】
5.8 体积对比(亲眼看到裁剪效果)
| 配置 | FLASH | RAM |
|---|---|---|
| 精简版(LOG=n, SHELL=n) | ≈ 21 KB | ≈ 6 KB |
| 调试版(LOG=y, SHELL=y) | ≈ 58 KB | ≈ 12 KB |
| 差值 | +37 KB | +6 KB |
仅仅两个开关,固件就差了几十 KB。对几十/几百 KB 的小 MCU 来说,这就是能不能塞得下的区别。 这就是 Kconfig 裁剪的威力。
5.9 去哪看最终值(真相之源)
不确定某个配置最终到底是 y 还是 n?打开这个文件:
build/zephyr/.config
里面是合并后所有配置的最终结果。用编辑器搜 CONFIG_LOG / CONFIG_SHELL,对比两次构建会看到:
精简版构建里:
# CONFIG_LOG is not set
# CONFIG_SHELL is not set
叠加 debug.conf 后:
CONFIG_LOG=y
CONFIG_LOG_DEFAULT_LEVEL=4
CONFIG_SHELL=y
关注一个细节:被关掉的 bool 配置写法是
# CONFIG_XXX is not set(注释形式),不是CONFIG_XXX=n。在.config里看到这行就代表「关」。
六、menuconfig / guiconfig 一步步操作(保姆级图解)
不想背配置名?用图形菜单边逛边改。
6.1 进入 menuconfig
前提:必须先成功 build 过一次(生成构建目录),然后在工程根目录执行:
# 文本界面菜单(在终端里跑,最常用,无图形环境也能用)
west build -t menuconfig
# 图形窗口版(需要桌面图形环境)
west build -t guiconfig
6.2 界面长什么样(示意)
┌──────────────── Zephyr Kernel Configuration ────────────────┐
│ Arrow keys navigate. <Enter> selects submenus ---> │
│ Press <Y> to include, <N> to exclude, </> for Search │
│ │
│ [ ] Bluetooth │
│ [*] Logging ---> │
│ [ ] Shell ---> │
│ (2048) Size of stack for the main thread │
│ │
│ [Space/Enter] Toggle [/] Search [?] Help [ESC] Back │
└──────────────────────────────────────────────────────────────┘
[*] 表示开(y),[ ] 表示关(n),灰色项表示依赖未满足、不可改。
6.3 五个关键操作(背下来就够用)
| 操作 | 按键 | 说明 |
|---|---|---|
| 上下移动 | 方向键 ↑↓ | 在选项间移动光标 |
| 进入子菜单 | 回车 | 带 ---> 的项可进入 |
| 开/关一个 bool | 空格(或 y/n) | [ ] 与 [*] 切换 |
| 搜索符号 | / 然后输入名字 | 最实用,输 LOG 直接定位 |
| 看帮助 | ? | 显示该项的依赖、默认值、定义它的文件路径 |
| 退出并保存 | ESC 退到顶层再 ESC,提示时选 Yes | 保存到构建目录的 .config |
操作演示:按 / → 输入 SHELL → 回车 → 列表里看到 CONFIG_SHELL,光标移到它 → 按空格切到 [*] → 一路 ESC → 提示保存选 Yes。
6.4 重要警告:menuconfig 的改动会丢!
⚠️ menuconfig 改的是构建目录里的临时
.config。下次你用west build -p always(pristine 干净重建)时,构建目录被清空,改动全部丢失!
正确做法: menuconfig 里试出想要的配置后,把对应的 CONFIG_XXX=y 手动抄回 prj.conf,这样才永久生效。menuconfig 当「探索 + 试验」工具用,prj.conf 才是「永久落地」的地方。
七、多配置文件 & 代码内条件编译
7.1 多配置文件分离(开发 / 发布两套)
把调试相关配置抽到 debug.conf(前面工程已演示),好处是 prj.conf 保持干净,发布时不带 debug.conf 即可:
# 开发:带调试配置
west build -p always -b <board> . -- -DEXTRA_CONF_FILE=debug.conf
# 发布:不带,自动是精简版
west build -p always -b <board> .
7.2 板级专属配置(多板项目)
针对某块板的额外配置,放在工程的 boards/<board>.conf(如 boards/nrf52840dk_nrf52840.conf),构建该板时自动叠加,不用手动指定,适合一个工程支持多块板。
7.3 代码内条件编译(应用层也能跟着裁剪)
/* 方式 1:bool 配置做条件编译,关掉时这段代码不进固件 */
#if defined(CONFIG_BT)
bt_enable(NULL);
#endif
/* 方式 2:int / hex / string 配置直接当宏用 */
LOG_INF("main stack = %d", CONFIG_MAIN_STACK_SIZE);
7.4 想知道某个 CONFIG 是干嘛的?
- 在 menuconfig 里按
/搜,再按?看帮助和定义文件路径; - 或在 Zephyr 源码里搜定义:
grep -r "config SHELL" zephyr/(搜的是不带 CONFIG_ 前缀的config SHELL)。
八、强化版高频报错排查表(13 条)
| # | 现象 | 原因 | 解决办法 |
|---|---|---|---|
| 1 | 配了 CONFIG_X=y 却没生效 | depends on 依赖未满足,被静默忽略 | menuconfig 看该项是否灰色,先把依赖项打开(如先 CONFIG_BT=y) |
| 2 | 改了 prj.conf 完全没反应 | 构建用了旧缓存 | 用 west build -p always 干净重建 |
| 3 | menuconfig 改完,重建就丢 | 只改了临时 .config,没写回 prj.conf | 把 CONFIG_X=y 抄进 prj.conf |
| 4 | 不知道最终值是 y 还是 n | 多来源合并,看 prj.conf 看不准 | 打开 build/zephyr/.config 搜对应符号 |
| 5 | .config 里找不到 CONFIG_X=n | 关闭的 bool 写法是注释 | 找 # CONFIG_X is not set 这行即可,那就是「关」 |
| 6 | 提示 undefined symbol CONFIG_X 之类 | 拼错名字 / 该符号在当前板未定义 | menuconfig 按 / 搜确认真实名字与是否存在 |
| 7 | 代码里 CONFIG_X 报未定义 | 该配置为 n,宏没被定义 | 用 #if defined(CONFIG_X) 包裹,别直接裸用 |
| 8 | 固件太大、塞不下 | 开了用不到的子系统(LOG/SHELL/BT 等) | 关掉不需要的 CONFIG_*,对照 .config 核实,看体积表 |
| 9 | 加了配置但功能没起来 | 只开了 Kconfig,设备树节点没 okay | Kconfig 与设备树两边都要配(如 I2C:CONFIG_I2C=y + 节点 status="okay") |
| 10 | EXTRA_CONF_FILE 多文件不生效 | 分隔符写错或没整体加引号 | 用分号分隔并整体引号:-DEXTRA_CONF_FILE="a.conf;b.conf" |
| 11 | menuconfig 里某项是灰色改不动 | 它依赖的上层符号没开 | 先按 ? 看它依赖谁,去把依赖打开,它才可选 |
| 12 | 改了 int 配置(如栈大小)没变 | 仍是缓存 / 改错了文件 | -p always 重建,并在 .config 里确认 CONFIG_X=新值 |
| 13 | 两个配置互相冲突报 warning | 一个 select 强行打开了另一个 | 看构建警告里的符号名,调整 prj.conf,避免硬冲突 |
核心排错习惯:配置出问题,第一时间打开
build/zephyr/.config——它是所有来源合并后的「真相之源」,比盯着 prj.conf 猜靠谱一百倍。
九、总结
- 分工:设备树管「硬件长什么样」,Kconfig 管「启用哪些功能、给多少资源」,启用驱动通常两边配合。
- 语法:日常写
prj.conf(工程根目录),四种类型 bool/int/hex/string,重点理解depends on和select。 - 优先级:模块默认 < 板级 defconfig < prj.conf < 额外 conf < menuconfig,越靠近本次构建越大,最终都汇进
build/zephyr/.config。 - 原理:
prj.conf → .config → autoconf.h → #if defined(CONFIG_xxx)条件编译,编译期生效,关闭功能零开销。 - 实战:用
-DEXTRA_CONF_FILE分离调试/发布配置;menuconfig 当探索工具但记得把改动抄回 prj.conf;-p always干净重建;体积看 build 终端的 Memory region 表,最终值看.config。
下一篇《基础篇总结》:把架构、设备树、Kconfig 串成完整主线,用一个例子打通「设备树 + Kconfig + 代码」三件套,建立 Zephyr 开发心智模型,随后正式进入内核篇。
如果帮到你,点赞 + 收藏 + 关注三连。配置相关报错欢迎贴评论区(最好附上 .config 里相关几行),我帮你看。
791

被折叠的 条评论
为什么被折叠?



