Zephyr Kconfig 配置系统保姆级详解:语法 + menuconfig 实战 + 固件裁剪 + 排错(含完整可运行工程)

本文是「Zephyr 内核从入门到精通」系列第 05 篇。上一篇讲透了设备树,本篇讲它的黄金搭档 Kconfig——它和设备树什么关系、prj.conf 写在哪、menuconfig 怎么一步步操作、开关一个功能固件体积怎么变、配置不生效怎么排查。

全文保姆级:每一步都标清「在哪做、怎么做、应该看到什么」,配一个完整可编译的小工程,照着抄就能跑。建议先点赞收藏,照着做一遍。

目录

  • 一、Kconfig vs 设备树:一句话分清
  • 二、Kconfig 基本语法(符号类型 & 依赖 & select)
  • 三、配置从哪来、谁说了算(优先级)
  • 四、固件裁剪的原理:配置怎么变成代码
  • 五、完整实战工程:开关 LOG / SHELL,看固件体积怎么变
  • 六、menuconfig / guiconfig 一步步操作(保姆级图解)
  • 七、多配置文件 & 代码内条件编译
  • 八、强化版高频报错排查表(13 条)
  • 九、总结

一、Kconfig vs 设备树:一句话分清

很多新手把 Kconfig 和设备树搞混,因为两者都在「配置」。但分工非常明确:

设备树回答「硬件长什么样」,Kconfig 回答「这次启用哪些功能、给多少资源」。

> 【插入图 1:01_Kconfig与设备树分工】

用装修类比:设备树是装修图纸(房子结构,固定不变),Kconfig 是功能清单(装不装空调、要不要地暖,按预算勾选)。

问题归属
LED 接在哪个引脚设备树
I2C 总线上挂了哪个传感器、地址多少设备树
是否启用蓝牙 / 日志 / ShellKconfig
主线程栈大小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"

四种符号类型记住即可:

类型取值例子
booly / nCONFIG_LOG=y
int整数CONFIG_MAIN_STACK_SIZE=2048
hex0x 开头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:强制选中(反向开依赖)

selectdepends on 的反方向——开 A 时自动把 B 也打开:

config BT_PERIPHERAL
    bool "Peripheral Role support"
    select BT_BROADCASTER   # 开我,自动也开 BT_BROADCASTER

小心:select强制打开被选项,哪怕被选项自己的依赖没满足,可能导致诡异冲突。新手日常基本不用手写 select,知道它存在、能看懂报错即可。


三、配置从哪来、谁说了算(优先级)

板子有出厂默认配置,你又写了 prj.conf,还可能用 menuconfig 临时改,到底谁说了算?

> 【插入图 2:02_Kconfig配置优先级】

优先级从低到高(越靠近「本次构建」的越大):

模块默认值(default)  <  板级 <board>_defconfig  <  prj.conf  <  额外 conf(EXTRA_CONF_FILE)  <  menuconfig 临时改动
   最低                                                                                          最高
  1. 模块默认值:各子系统 Kconfig 文件里的 default,最底层兜底。
  2. 板级 defconfigboards/.../<board>_defconfig,这块板子的出厂默认(比如默认开了哪个串口)。
  3. prj.conf:你的工程配置,日常主战场
  4. 额外 conf:通过 -DEXTRA_CONF_FILE=xxx.conf 叠加的文件,会覆盖 prj.conf。
  5. menuconfig:构建时手动改的临时值,优先级最高,但只存在于构建目录,pristine 重建就丢

记忆口诀:离「本次构建」越近,优先级越高。 不管来源多复杂,所有配置最终合并成一份文件:build/zephyr/.config。排错只认它。


四、固件裁剪的原理:配置怎么变成代码

这是 Kconfig 的核心价值,也是 Zephyr 能同时覆盖几十 KB 小 MCU 和高端芯片的根本原因。

> 【插入图 3:03_Kconfig裁剪固件】

完整链条:

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_LOGCONFIG_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_FILEdebug.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 体积对比(亲眼看到裁剪效果)

配置FLASHRAM
精简版(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 干净重建
3menuconfig 改完,重建就丢只改了临时 .config,没写回 prj.confCONFIG_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,设备树节点没 okayKconfig 与设备树两边都要配(如 I2C:CONFIG_I2C=y + 节点 status="okay"
10EXTRA_CONF_FILE 多文件不生效分隔符写错或没整体加引号用分号分隔并整体引号:-DEXTRA_CONF_FILE="a.conf;b.conf"
11menuconfig 里某项是灰色改不动它依赖的上层符号没开先按 ? 看它依赖谁,去把依赖打开,它才可选
12改了 int 配置(如栈大小)没变仍是缓存 / 改错了文件-p always 重建,并在 .config 里确认 CONFIG_X=新值
13两个配置互相冲突报 warning一个 select 强行打开了另一个看构建警告里的符号名,调整 prj.conf,避免硬冲突

核心排错习惯:配置出问题,第一时间打开 build/zephyr/.config——它是所有来源合并后的「真相之源」,比盯着 prj.conf 猜靠谱一百倍。


九、总结

  1. 分工:设备树管「硬件长什么样」,Kconfig 管「启用哪些功能、给多少资源」,启用驱动通常两边配合。
  2. 语法:日常写 prj.conf(工程根目录),四种类型 bool/int/hex/string,重点理解 depends onselect
  3. 优先级:模块默认 < 板级 defconfig < prj.conf < 额外 conf < menuconfig,越靠近本次构建越大,最终都汇进 build/zephyr/.config
  4. 原理prj.conf → .config → autoconf.h → #if defined(CONFIG_xxx) 条件编译,编译期生效,关闭功能零开销
  5. 实战:用 -DEXTRA_CONF_FILE 分离调试/发布配置;menuconfig 当探索工具但记得把改动抄回 prj.conf;-p always 干净重建;体积看 build 终端的 Memory region 表,最终值看 .config

下一篇《基础篇总结》:把架构、设备树、Kconfig 串成完整主线,用一个例子打通「设备树 + Kconfig + 代码」三件套,建立 Zephyr 开发心智模型,随后正式进入内核篇。

如果帮到你,点赞 + 收藏 + 关注三连。配置相关报错欢迎贴评论区(最好附上 .config 里相关几行),我帮你看。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值