摘要:本文是 Zephyr BSP 移植系列的第 31 篇,聚焦 Kconfig 在 Company SoC 平台化中的核心作用。文章首先厘清 Devicetree 与 Kconfig 的分工——前者描述硬件资源,后者决定软件编译与功能配置;随后系统讲解 Kconfig 的生成链路(
.config→autoconf.h→ C 编译)、SoC/Board/Driver 三层的 Kconfig 组织方式,以及depends on、select、default等关键机制的区别。通过 Company UART 驱动实例,串联起 Devicetree、Kconfig、CMake 与 Device Model 的完整协作链路,最终建立「Kconfig(软件配置)— Devicetree(硬件描述)— CMake(编译组织)」的三角模型,帮助读者把 Company SoC 从固定代码升级为可配置的硬件软件平台。
Kconfig:把 Company SoC 变成可配置平台
前面我们已经完成了:
20 理解 Zephyr
↓
21 SoC Port Skeleton
↓
22 CPU / Architecture
↓
23 Startup
↓
24 Interrupt Controller
↓
25 Clock / Reset
↓
26 Devicetree
↓
27 Binding
↓
28 UART Driver
↓
29 GPIO / SPI / I2C / Timer
↓
30 Board Support Package
到 31 — Kconfig,我们要解决一个非常关键的问题:
Devicetree 描述「硬件是什么」,Kconfig 决定「软件编译什么、启用什么、配置成什么」。
对于公司自己的 SoC,这一步意味着:
Company SoC
│
├── Devicetree
│ └── 描述硬件资源
│
└── Kconfig
└── 控制软件功能
最终形成:
┌────────────────────────────────────────────┐
│ Company Zephyr BSP │
│ │
│ Devicetree Kconfig │
│ ────────── ─────── │
│ UART0 CONFIG_UART │
│ UART1 CONFIG_GPIO │
│ GPIO CONFIG_SPI │
│ SPI0 CONFIG_I2C │
│ Timer CONFIG_TIMER │
│ IRQ CONFIG_SOC_... │
│ │
└────────────────────────────────────────────┘
一、先搞清楚:Devicetree 和 Kconfig 为什么都需要?
这是 Zephyr BSP 最容易混淆的地方之一。
假设 Company SoC 有:
UART0
UART1
UART2
GPIO
SPI0
SPI1
I2C0
TIMER0
TIMER1
Devicetree 可以描述:
uart0: uart@40000000 {
compatible = "company,uart";
reg = <0x40000000 0x1000>;
interrupts = <5>;
status = "okay";
};
它回答:
UART0 在哪里?
但是它不回答:
UART 驱动要不要编译?
这就是 Kconfig 的职责。
例如:
CONFIG_SERIAL=y
CONFIG_UART_CONSOLE=y
CONFIG_UART_COMPANY=y
于是:
Devicetree
↓
UART0 存在
Kconfig
↓
UART 驱动参与编译
↓
UART Console 开启
两者共同决定最终系统。
二、一个非常重要的理解
可以先记住这个模型:
Devicetree = Hardware Description
Kconfig = Software Configuration
或者更准确一点:
Devicetree
→ 描述具体硬件实例、资源、连接关系
Kconfig
→ 决定功能、驱动、编译选项、系统能力
例如:
Devicetree
uart0 {
reg = <...>;
interrupts = <...>;
clocks = <...>;
};
描述:
UART0
地址
IRQ
Clock
Kconfig
CONFIG_SERIAL=y
CONFIG_UART_INTERRUPT_DRIVEN=y
CONFIG_UART_CONSOLE=y
描述:
编译 UART
支持 IRQ
把 UART 作为 Console
三、Zephyr Kconfig 最终生成什么?
这一点和前面讲 Devicetree 的过程非常类似。
你执行:
west build -b company_board app
Kconfig 会参与:
Kconfig
↓
menuconfig / configuration
↓
.config
↓
autoconf.h
↓
C/C++ 编译
最重要的文件之一是:
build/zephyr/.config
例如:
CONFIG_SOC_SERIES_COMPANY_X=y
CONFIG_SOC_COMPANY_X1=y
CONFIG_SERIAL=y
CONFIG_UART_CONSOLE=y
CONFIG_UART_COMPANY=y
CONFIG_GPIO=y
CONFIG_GPIO_COMPANY=y
然后生成:
build/zephyr/include/generated/zephyr/autoconf.h
里面会出现类似:
# define CONFIG_SERIAL 1
# define CONFIG_UART_CONSOLE 1
# define CONFIG_UART_COMPANY 1
# define CONFIG_GPIO 1
所以最终 C 代码可以:
# ifdef CONFIG_UART_CONSOLE
...
# endif
或者:
# ifdef CONFIG_UART_COMPANY
...
# endif
四、Company SoC 应该在哪里定义 Kconfig?
我们假设公司 SoC:
company_x1
对应:
soc/arm/company_x1/
一个典型结构可以是:
zephyr/
└── soc/
└── arm/
└── company/
└── x1/
├── Kconfig
├── Kconfig.defconfig
├── CMakeLists.txt
├── soc.c
├── soc.h
└── ...
Board:
boards/
└── company/
└── company_board/
├── Kconfig.board
├── Kconfig.defconfig
├── company_board.dts
└── ...
Driver:
drivers/
├── serial/
│ ├── Kconfig
│ └── uart_company.c
│
├── gpio/
│ ├── Kconfig
│ └── gpio_company.c
│
└── ...
注意:
SoC、Board、Driver 的 Kconfig 并不是全部放在一个 Kconfig 文件里。
而是通过 Kconfig 的 source 机制组合起来。
五、Company SoC 的第一个 Kconfig
最简单可以理解成:
menuconfig SOC_SERIES_COMPANY_X
bool "Company X SoC series"
然后:
config SOC_COMPANY_X1
bool "Company X1"
select SOC_SERIES_COMPANY_X
概念上:
CONFIG_SOC_SERIES_COMPANY_X
│
└── CONFIG_SOC_COMPANY_X1
也就是:
Company SoC Series
│
├── X1
├── X2
└── X3
这就是未来公司有多个 SoC 型号时的基础。
六、为什么不能直接写一个 CONFIG_SOC?
例如你当然可以想象:
CONFIG_COMPANY_SOC=y
但实际大型 BSP 不会这么简单。
因为公司通常会有:
Company X1
Company X2
Company X3
Company X5
它们可能:
CPU 相同
Interrupt Controller 相同
UART 相同
GPIO 相同
但是:
X1:
256 KB RAM
1 MB Flash
X2:
512 KB RAM
2 MB Flash
X3:
1 MB RAM
4 MB Flash
所以 Kconfig 需要表达:
Company SoC Family
│
├── X1
├── X2
└── X3
而不是一个简单的:
CONFIG_COMPANY_SOC
七、Kconfig 和 CMake 是什么关系?
这是 BSP 中另外一个很重要的关系。
假设:
CONFIG_UART_COMPANY=y
那么 CMake 可以决定:
zephyr_library()
zephyr_library_sources(uart_company.c)
或者更典型地:
zephyr_library_sources_ifdef(
CONFIG_UART_COMPANY
uart_company.c
)
于是:
Kconfig
│
│ CONFIG_UART_COMPANY=y
↓
CMake
│
│ 加入 uart_company.c
↓
Compiler
│
↓
uart_company.o
所以可以把三者理解成:
Devicetree
│
├── 硬件实例
│
↓
Kconfig
│
├── 软件功能是否启用
│
↓
CMake
│
├── 哪些源文件进入编译
│
↓
Compiler
八、一个真实的 Company UART 例子
假设:
drivers/serial/uart_company.c
我们定义:
CONFIG_UART_COMPANY
Kconfig:
config UART_COMPANY
bool "Company UART driver"
default y
depends on SERIAL
help
Enable Company SoC UART driver.
然后 CMake:
zephyr_library()
zephyr_library_sources_ifdef(
CONFIG_UART_COMPANY
uart_company.c
)
于是:
CONFIG_UART_COMPANY=n
结果:
uart_company.c
↓
不进入最终编译
而:
CONFIG_UART_COMPANY=y
八点五、Company UART 驱动 Kconfig 配置实战
下面把「八、一个真实的 Company UART 例子」展开成一个可以照着抄的完整实战。我们以 drivers/serial/uart_company.c 为例,从 Kconfig 定义、默认配置、CMake 编译到验证与排错,一次走通。
1. drivers/serial/Kconfig 中 UART_COMPANY 的完整定义
在 drivers/serial/Kconfig 中追加:
config UART_COMPANY
bool "Company UART driver"
default y
depends on SERIAL
select UART_INTERRUPT_DRIVEN
help
Enable the Company SoC UART driver.
This driver provides UART support for Company X1 SoC.
几个字段的含义:
bool
→ 这是一个开关,取值 y / n
default y
→ 只要依赖满足,默认打开
depends on SERIAL
→ 必须先有通用串口框架,本驱动才可选
select UART_INTERRUPT_DRIVEN
→ 打开本驱动时,强制打开中断驱动支持
注意:select 在这里是「我打开时,强制别人打开」,和 depends on 的「我需要别人」方向相反。
2. Kconfig.defconfig 中默认配置写法
如果希望 Company Board 默认就启用这个驱动,可以在 Board 的 boards/company/company_board/Kconfig.defconfig 中写:
if BOARD_COMPANY_BOARD
config UART_COMPANY
default y
config UART_CONSOLE
default y
endif
也可以放在 SoC 层的 soc/arm/company/x1/Kconfig.defconfig:
if SOC_COMPANY_X1
config UART_COMPANY
default y
endif
区别在于作用范围:
Board 的 Kconfig.defconfig
→ 只对这块板子生效
SoC 的 Kconfig.defconfig
→ 对该 SoC 下所有板子生效
3. CMakeLists.txt 中 zephyr_library_sources_ifdef 的用法
在 drivers/serial/CMakeLists.txt 中:
zephyr_library()
zephyr_library_sources_ifdef(
CONFIG_UART_COMPANY
uart_company.c
)
含义:
CONFIG_UART_COMPANY=y
→ uart_company.c 进入编译
CONFIG_UART_COMPANY=n
→ uart_company.c 完全不参与编译
如果驱动还依赖某个头文件目录,可以这样写:
zephyr_library_include_dirs_ifdef(
CONFIG_UART_COMPANY
.
)
4. 编译验证步骤
在应用目录执行:
west build -b company_board app
编译完成后,检查 .config 是否包含:
grep UART_COMPANY build/zephyr/.config
预期输出:
CONFIG_UART_COMPANY=y
再检查生成的 autoconf.h:
grep UART_COMPANY build/zephyr/include/generated/zephyr/autoconf.h
预期输出:
#define CONFIG_UART_COMPANY 1
最后确认目标文件确实被编译:
find build -name "uart_company.o"
如果能看到 uart_company.o,说明 Kconfig → CMake → Compiler 整条链路已经打通。
5. 常见错误排查方法
错误一:CONFIG_UART_COMPANY 没有出现在 .config 中
可能原因与排查:
depends on SERIAL 未满足
→ 检查 CONFIG_SERIAL 是否为 y
Kconfig 文件没有被 source
→ 检查 drivers/serial/Kconfig 是否被上层 Kconfig 引入
拼写不一致
→ 确认 Kconfig 里是 UART_COMPANY,CMake 里也是 UART_COMPANY
错误二:CONFIG_UART_COMPANY=y 但 uart_company.c 没编译
CMakeLists.txt 没写 zephyr_library_sources_ifdef
→ 补上对应语句
文件路径不对
→ 确认 uart_company.c 和 CMakeLists.txt 在同一目录
缓存未刷新
→ 执行 west build -t menuconfig 后重新 west build
错误三:menuconfig 里看不到 Company UART driver
Kconfig 文件未被 source
→ 检查 drivers/serial/Kconfig 是否被正确引入
depends on SERIAL 未满足
→ 先打开 Serial Drivers
错误四:改了 Kconfig 但编译结果没变
没有重新生成配置
→ 执行 west build -t menuconfig 修改后保存
使用了旧的 build 目录
→ 必要时 west build -p always -b company_board app 强制重建
6. 验证后的完整链路
drivers/serial/Kconfig
→ CONFIG_UART_COMPANY=y
→ autoconf.h 生成 #define CONFIG_UART_COMPANY 1
→ CMakeLists.txt 的 zephyr_library_sources_ifdef 命中
→ uart_company.c 参与编译
→ uart_company.o 链接进固件
到这里,Company UART 驱动已经从「代码存在」变成了「可配置、可编译、可验证」的完整闭环。
结果:
uart_company.c
↓
编译
↓
uart_company.o
↓
链接
九、Kconfig 的 depends on 是什么?
这是 Kconfig 最核心的机制之一。
例如:
config UART_COMPANY
bool "Company UART driver"
depends on SERIAL
意思是:
UART_COMPANY
│
└── requires SERIAL
如果:
CONFIG_SERIAL=n
那么:
CONFIG_UART_COMPANY
不能正常启用。
可以理解成:
UART_COMPANY
│
▼
SERIAL
这和前面我们学习的 Driver Dependency 是不同层面的概念。
前面:
UART Driver
↓
Clock Driver
↓
Reset Driver
属于:
运行时 / 设备依赖
而:
UART_COMPANY
↓
SERIAL
属于:
编译配置依赖
这两个一定要分开。
十、select 又是什么?
例如:
config SOC_COMPANY_X1
bool "Company X1"
select SOC_SERIES_COMPANY
这里:
X1
↓
select
↓
COMPANY_SOC
意味着:
选择 X1 时,Company SoC Series 自动打开。
所以:
CONFIG_SOC_COMPANY_X1=y
会导致:
CONFIG_SOC_SERIES_COMPANY=y
十一、depends on 和 select 不要混淆
简单记:
depends on
是:
我需要别人。
例如:
UART
depends on
SERIAL
而:
select
是:
我打开时,强制别人打开。
例如:
SOC_X1
select
SOC_SERIES_COMPANY
下面用一张表格把两者的区别一次性说清楚:
| 对比维度 | depends on | select |
|---|---|---|
| 方向 | 我依赖别人(被依赖项在前,我在后) | 我打开时强制打开别人(我在前,被打开项在后) |
| 语义 | 「我需要别人」——依赖项不满足时,本选项不可用 | 「我打开时,强制别人打开」——本选项被选中时,目标项自动置为 y |
| 使用场景 | 驱动依赖框架、功能依赖前置能力,如 UART_COMPANY 依赖 SERIAL | SoC 型号自动打开所属系列、驱动自动打开中断支持,如 SOC_COMPANY_X1 选中 SOC_SERIES_COMPANY |
| 示例代码 | config UART_COMPANY bool "Company UART driver" depends on SERIAL | config SOC_COMPANY_X1 bool "Company X1" select SOC_SERIES_COMPANY |
一句话记忆:
depends on = 我需要别人
select = 我强制别人
可以画成:
depends on:
UART
│
↓
SERIAL
select:
SOC_X1
│
↓
SOC_SERIES_COMPANY
十二、Company Board 的 Kconfig
到了 Board 层:
boards/company/company_board/
可以有:
Kconfig.board
Kconfig.defconfig
例如:
config BOARD_COMPANY_BOARD
bool "Company Board"
depends on SOC_COMPANY_X1
这样形成:
BOARD_COMPANY_BOARD
│
↓
SOC_COMPANY_X1
│
↓
SOC_SERIES_COMPANY
最终:
Board
↓
SoC
↓
SoC Family
十三、为什么 Board 不应该决定所有东西?
例如不要把:
CONFIG_UART_CONSOLE=y
CONFIG_SPI=y
CONFIG_I2C=y
CONFIG_GPIO=y
全部硬编码成:
Company Board = 所有功能全部打开
因为不同应用可能需要不同配置。
例如:
Application A
UART Console
GPIO
Application B
UART
SPI
Application C
I2C
GPIO
Timer
同一个:
Company Board
应该能够服务多个 application。
所以:
Board
↓
提供合理默认配置
Application
↓
决定真正需要什么
这是 Kconfig 非常重要的思想。
十四、Kconfig.defconfig 是干什么的?
它主要用于:
给 Board / SoC 提供默认配置。
例如 Company Board 默认需要 UART Console:
config UART_CONSOLE
default y
但这个默认值只针对:
BOARD_COMPANY_BOARD
那么可以放在 Board 的:
Kconfig.defconfig
概念上:
if BOARD_COMPANY_BOARD
config UART_CONSOLE
default y
endif
于是:
选择 Company Board
↓
UART Console 默认开启
注意:
default y 和 select 是两个不同概念。
十五、一个完整的配置链
现在把:
Board
SoC
Driver
Application
全部串起来。
假设:
west build -b company_board app
首先:
company_board
↓
选择 SoC
↓
company_x1
然后 Kconfig:
SOC_COMPANY_X1=y
↓
SOC_SERIES_COMPANY=y
Board 默认配置:
UART_CONSOLE=y
Application:
CONFIG_SERIAL=y
CONFIG_GPIO=y
最终:
build/zephyr/.config
可能是:
CONFIG_SOC_SERIES_COMPANY=y
CONFIG_SOC_COMPANY_X1=y
CONFIG_SERIAL=y
CONFIG_UART_CONSOLE=y
CONFIG_UART_COMPANY=y
CONFIG_GPIO=y
CONFIG_GPIO_COMPANY=y
十六、然后 C 代码看到什么?
Kconfig 最终生成:
autoconf.h
例如:
# define CONFIG_SOC_SERIES_COMPANY 1
# define CONFIG_SOC_COMPANY_X1 1
# define CONFIG_SERIAL 1
# define CONFIG_UART_CONSOLE 1
# define CONFIG_UART_COMPANY 1
# define CONFIG_GPIO 1
# define CONFIG_GPIO_COMPANY 1
于是:
# ifdef CONFIG_UART_COMPANY
/\* Company UART driver \*/
# endif
成立。
如果:
CONFIG_UART_COMPANY=n
那么对应代码可以完全不参与编译。
十七、这和 Devicetree 的关系非常重要
假设:
uart0: uart@40000000 {
compatible = "company,uart";
reg = <0x40000000 0x1000>;
interrupts = <5>;
status = "okay";
};
这意味着:
硬件存在
但是:
CONFIG_UART_COMPANY
如果没有打开:
驱动可能根本没有编译
所以:
DT status = "okay"
并不等于:
Driver 一定被编译
真正完整的条件更接近:
Devicetree instance
+
Kconfig driver enabled
+
Driver source compiled
↓
struct device
这就是为什么你之前学的:
DT_NODELABEL
DEVICE_DT_DEFINE
struct device
还不是整个故事。
十八、一个完整的 Company UART 链路
最终我们可以把 UART 画成:
Company Board
│
▼
Devicetree
│
uart0 = "okay"
│
│
▼
DT instance
│
│
Kconfig ───────────────┤
CONFIG_SERIAL=y │
CONFIG_UART_COMPANY=y │
▼
uart_company.c
│
▼
DEVICE_DT_DEFINE()
│
▼
struct device
│
▼
printk
这里:
Devicetree
负责:
"UART0 是什么"
Kconfig:
"UART 驱动要不要存在"
CMake:
"uart_company.c 要不要编译"
Driver:
"如何操作 UART0"
Device Model:
"创建 struct device"
十九、Kconfig 不只是「开关」
很多初学者会把 Kconfig 理解成:
CONFIG_xxx=y/n
其实它远不止如此。
Kconfig 可以配置:
Boolean
config UART_CONSOLE
bool
结果:
y / n
Integer
config COMPANY_UART_BAUDRATE
int "UART baudrate"
default 115200
结果:
CONFIG_COMPANY_UART_BAUDRATE=115200
Hex
例如:
config COMPANY_FLASH_BASE
hex "Flash base address"
default 0x08000000
String
config COMPANY_NAME
string "Company name"
因此 Kconfig 不仅决定:
有没有功能
还可以决定:
功能怎么配置
二十、一个 Company SoC 的典型 Kconfig 树
最终我们希望得到类似:
Company SoC
│
├── SoC Family
│ └── COMPANY_X
│
├── CPU
│ └── Cortex-M4
│
├── Drivers
│ ├── UART
│ │ ├── UART0
│ │ └── UART1
│ │
│ ├── GPIO
│ ├── SPI
│ ├── I2C
│ └── TIMER
│
└── Features
├── Console
├── Shell
├── Logging
├── PM
└── DMA
用户可以通过:
west build -t menuconfig
看到配置界面。
例如:
[*] Serial Drivers
[*] UART Console
[*] Company UART driver
(115200) Default baudrate
[*] GPIO Drivers
[*] SPI Drivers
[ ] I2C Drivers
[*] Logging
[ ] Power Management
这时候:
你的 Company BSP 已经从「固定的一套代码」变成了「可以配置的硬件软件平台」。
二十一、west build -t menuconfig
这是今天最值得亲手做的实验。
在应用目录:
west build -t menuconfig
你会进入:
┌─────────────────────────────────────┐
│ Zephyr Kernel Configuration │
│ │
│ General Kernel Options │
│ Device Drivers │
│ Serial Drivers │
│ GPIO Drivers │
│ SPI Drivers │
│ I2C Drivers │
│ Logging │
│ Power Management │
│ │
└─────────────────────────────────────┘
修改配置以后:
.config
会改变。
然后重新:
west build
Kconfig:
.config
↓
autoconf.h
↓
C compilation
二十二、真正做 Company BSP 时建议这样组织
如果你未来公司的 SoC BSP 有:
Company X1
Company X2
Company X3
推荐形成:
soc/
└── arm/
└── company/
├── Kconfig
│
├── x1/
│ ├── Kconfig
│ ├── Kconfig.defconfig
│ └── ...
│
├── x2/
│ ├── Kconfig
│ └── ...
│
└── x3/
├── Kconfig
└── ...
然后:
drivers/
├── serial/
│ └── Kconfig
├── gpio/
│ └── Kconfig
├── spi/
│ └── Kconfig
└── i2c/
└── Kconfig
Board:
boards/
└── company/
├── board_a/
│ ├── Kconfig.board
│ └── Kconfig.defconfig
│
└── board_b/
├── Kconfig.board
└── Kconfig.defconfig
这样:
Company SoC
│
├── X1
├── X2
└── X3
│
├── Board A
├── Board B
└── Board C
可以形成一个真正可维护的 BSP。
二十三、到这里,你应该建立一个非常重要的「三角模型」
以后看到 Zephyr BSP,不要只想到:
Devicetree
而应该想到:
Kconfig
软件配置/功能
▲
│
│
│
Devicetree ◄──────┼──────► CMake
硬件描述 │ 编译组织
│
▼
Driver / SoC
更完整一点:
┌──────────────┐
│ Kconfig │
│ 软件配置 │
└──────┬───────┘
│
▼
CONFIG_xxx
│
▼
┌──────────────┐
│ CMake │
│ 编译哪些代码 │
└──────┬───────┘
│
▼
┌──────────────┐
│ Driver │
└──────┬───────┘
▲
│
┌──────┴───────┐
│ Devicetree │
│ 硬件是什么 │
└──────────────┘
二十四、从 20 → 31,你现在真正学会了什么?
现在整个 Company SoC BSP 已经可以串起来:
20 理解 Zephyr
│
21 SoC Port Skeleton
│
22 CPU / Architecture
│
23 Startup
│
24 Interrupt Controller
│
25 Clock / Reset
│
26 Devicetree
│
27 Binding
│
28 UART Driver
│
29 GPIO / SPI / I2C / Timer
│
30 Board Support Package
│
31 Kconfig
│
▼
可配置 Company BSP
也就是说,我们已经从:
「让 Zephyr 跑起来」
进入:
「让 Zephyr 支持公司的 SoC 产品线」

108

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



