轻松学习Zephyr BSP: 32-CMake构建系统

摘要:本文是 Zephyr BSP 系列第 33 篇,聚焦 CMake 构建系统。讲解 zephyr_library() 等构建宏、Devicetree/Kconfig/CMake 三角关系、SoC 与 Driver 的 CMake 层级差异,以及"Kconfig 开了但代码没编译"等常见坑的排查思路,并串联第 20~32 篇形成完整 BSP 闭环。

关键词:Zephyr;CMake;BSP;构建系统;Kconfig

CMake / Build System:Zephyr BSP 最后是怎么"编译起来"的

到第 31 篇,你已经把 Company SoC 的 Kconfig 配置体系建立起来了。

现在进入一个非常关键、也很容易混淆的部分:

Devicetree 告诉 Zephyr「硬件是什么」,Kconfig 告诉 Zephyr「启用什么」,CMake 则负责告诉编译系统「到底编译哪些代码、去哪里找这些代码、怎么链接成最终 firmware」。

所以可以把前面的几篇串起来:

                    Zephyr BSP
                       │
        ┌──────────────┼──────────────┐
        │              │              │
   Devicetree        Kconfig        CMake
        │              │              │
  描述硬件结构      决定功能配置      决定构建过程
        │              │              │
        └──────────────┼──────────────┘
                       ↓
                 C / C++ Source
                       ↓
                    Compiler
                       ↓
                    Linker
                       ↓
                zephyr.elf / bin

1. 为什么 BSP 需要 CMake?

假设你的 Company SoC 有:

Company SoC
├── UART
├── GPIO
├── SPI
├── I2C
├── TIMER
├── CLOCK
└── IRQ

对应 Zephyr source:

soc/company/xyz/
├── CMakeLists.txt
├── Kconfig
├── Kconfig.defconfig
├── soc.c
├── clock.c
└── startup.c
drivers/serial/
├── uart_company.c
drivers/gpio/
├── gpio_company.c
drivers/spi/
├── spi_company.c

问题来了:
Zephyr 怎么知道这些 .c 文件应该被编译?

答案就是:
CMake

例如:

zephyr_library()

zephyr_library_sources(
    soc.c
    clock.c
    startup.c
)

这实际上是在告诉 Zephyr:
把这些 source 加入当前 build。

2. Zephyr 的 CMake 不是普通 CMake

这是非常重要的一点。

你当然可以看到:

add_library(...)
add_executable(...)
target_sources(...)
target_include_directories(...)

但 Zephyr BSP 通常大量使用自己的 CMake helper:

zephyr_library()
zephyr_library_sources()
zephyr_library_sources_ifdef()
zephyr_include_directories()

例如:



zephyr_library_sources(
    uart_company.c
    uart_company_dma.c
)

或者:

zephyr_library_sources_ifdef(
	CONFIG_UART_COMPANY_DMA
	uart_company_dma.c
)

后者非常重要。
它把:

Kconfig
↓
CONFIG_UART_COMPANY_DMA
↓
CMake
↓
uart_company_dma.c

连接起来。

3. Kconfig 和 CMake 到底是什么关系?

假设:

Kconfig

定义:

CONFIG_COMPANY_UART
CONFIG_COMPANY_UART_DMA
CONFIG_COMPANY_SPI

用户:

CONFIG_COMPANY_UART=y
CONFIG_COMPANY_UART_DMA=y
CONFIG_COMPANY_SPI=n

那么 CMake 可以:

zephyr_library()

zephyr_library_sources_ifdef(
    CONFIG_COMPANY_UART
    uart_company.c
)

zephyr_library_sources_ifdef(
    CONFIG_COMPANY_UART_DMA
    uart_company_dma.c
)

zephyr_library_sources_ifdef(
    CONFIG_COMPANY_SPI
    spi_company.c
)

最终:

编译
├── uart_company.c
├── uart_company_dma.c
└── spi_company.c ❌

所以:
Kconfig 决定「有没有这个功能」,CMake 决定「这个功能对应哪些 source 被编译」。

4. 最小 Company SoC CMakeLists.txt

假设你的 SoC:

soc/company/mychip/

结构:

soc/company/mychip/
├── CMakeLists.txt
├── Kconfig
├── Kconfig.defconfig
├── soc.c
├── clock.c
└── irq.c

最简单:

zephyr_library()

zephyr_library_sources(
    soc.c
    clock.c
    irq.c
)

意思就是:

创建 Zephyr library
	│
	├── soc.c
	├── clock.c
	└── irq.c

5. 如果代码依赖 CONFIG 呢?

例如:

CONFIG_SOC_COMPANY_MYCHIP=y
CONFIG_COMPANY_CLOCK=y

那么:

zephyr_library()

zephyr_library_sources(
    soc.c
)

zephyr_library_sources_ifdef(
    CONFIG_COMPANY_CLOCK
    clock.c
)

如果:

CONFIG_COMPANY_CLOCK=y

那么:

clock.c

加入 build。
如果:

CONFIG_COMPANY_CLOCK=n

则:

clock.c

不会被编译。

6. Driver 的 CMake 又在哪里?

这里是 BSP 初学者非常容易迷糊的地方。

你可能会认为:

soc/company/mychip/
	CMakeLists.txt

会自动编译:

drivers/serial/uart_company.c

不会。

SoC CMake 和 Driver CMake 是不同层级。

例如:


    └── mychip/
        └── CMakeLists.txt

drivers/
└── serial/
    └── CMakeLists.txt

Driver 自己负责:

zephyr_library()

zephyr_library_sources_ifdef(
    CONFIG_UART_COMPANY
    uart_company.c
)

所以最终:

SoC CMake
    │
    ├── soc.c
    ├── clock.c
    └── irq.c
    │
    ↓
Driver CMake
    │
    ├── uart_company.c
    ├── gpio_company.c
    └── spi_company.c

7. CMake 是怎么被 Zephyr 找到的?

这是理解 Zephyr build system 的关键。

用户执行:

west build -b company_board app

大致发生:

LinkerCompilerNinjaApplication CMakeDriver CMakeSoC CMakeBoard CMakeCMakewest用户LinkerCompilerNinjaApplication CMakeDriver CMakeSoC CMakeBoard CMakeCMakewest用户west build -b company_board app1调用 cmake 生成构建系统2加载 boards/company/myboard/CMakeLists.txt3返回 board 配置4加载 soc/company/mychip/CMakeLists.txt5返回 SoC 源文件列表6加载 drivers/*/CMakeLists.txt7返回 Driver 源文件列表8加载 app/CMakeLists.txt9返回应用源文件列表10解析 Kconfig → 生成 .config11生成 build.ninja12按规则编译 .c → .o13返回 .o 目标文件14链接 .o / .a15生成 zephyr.elf16构建完成17输出 zephyr.elf / zephyr.bin18

这张时序图把 west build 的完整调用链串起来了:west 先调用 CMake,CMake 依次加载 Board → SoC → Driver → Application 各层 CMakeLists.txt,汇总所有源文件并解析 Kconfig 生成 .config;随后 CMake 生成 build.ninja 交给 Ninja,Ninja 驱动 Compiler.c 编译成 .o,最后由 Linker 链接成 zephyr.elf。每个环节的关键产物(.configbuild.ninja.o.elf)都在图中标注出来了。

所以:

west

本身不是编译器。
更准确地说:

west
↓
CMake
↓
Ninja
↓
arm-none-eabi-gcc

例如:

west build -b nucleo_f303re samples/hello_world

背后会产生:

build/
├── CMakeCache.txt
├── build.ninja
├── zephyr/
│   ├── zephyr.elf
│   ├── zephyr.bin
│   └── include/
└── ...

8. build.ninja 是什么?

这是非常值得你打开看的文件。
执行:

west build -b your_board samples/hello_world

之后:

less build/build.ninja

你会发现里面最终出现大量类似:

arm-none-eabi-gcc

以及:

soc.c
clock.c
irq.c
uart_company.c

也就是说:

CMakeLists.txt
	↓
CMake
	↓
build.ninja
	↓
Ninja
	↓
gcc

所以你以后调 BSP 编译问题时:
build.ninja 是一个非常重要的「最终证据」。

9. CMake 如何找到 Board?

假设:

boards/company/myboard/
├── CMakeLists.txt
├── Kconfig.board
├── Kconfig.defconfig
├── myboard.dts
└── myboard.yaml

Board 可以有自己的:

CMakeLists.txt

例如:

board_runner_args(...)

或者添加 board-specific source。
但是很多 board 不需要大量 CMake。
因为 Zephyr 的 board / SoC / architecture 层已经有对应的构建逻辑。

10. Board → SoC → Architecture 的关系

现在把前面 20~32 篇真正串起来:

west build -b company_board
	│
	↓
Board
	│
	↓
SoC
	│
	↓
Architecture
	│
	↓
Zephyr Kernel

同时:

Board
 │
 └── DTS
      │
      ├── UART
      ├── GPIO
      ├── SPI
      └── I2C

以及:

Kconfig
	│
	├── SoC configuration
	├── Driver configuration
	└── Kernel configuration

最终全部汇合:

              Board
                │
        ┌───────┼────────┐
        ↓       ↓        ↓
       DTS    Kconfig   CMake
        │       │        │
        │       │        └── Source files
        │       │
        │       └── CONFIG_*
        │
        └── Hardware description
                │
                ↓
             Compiler
                │
                ↓
             Linker
                │
                ↓
          zephyr.elf

11. zephyr_library() 到底是什么?

这个宏非常值得理解。
例如:

zephyr_library()

你可以把它粗略理解为:
创建一个属于 Zephyr 构建系统管理的 library target。

然后:

zephyr_library_sources(
	soc.c
	clock.c
)

把 source 放进去。

最终这些 .c:

soc.c
clock.c

先被编译成:

soc.c.o
clock.c.o

然后进入 library,例如:

libzephyr.a

或者相关 Zephyr archive。
最后 linker:

ld

把各种 object/library 合并:

libc.a
application objects
startup objects
        │
        ↓
    zephyr.elf

12. 为什么 Zephyr 喜欢 library?

因为 Zephyr 是一个非常模块化的系统。

例如:

Application
	│
	├── Kernel
	├── UART
	├── GPIO
	├── SPI
	├── I2C
	├── Networking
	└── Filesystem

每个 subsystem / driver 可以独立管理自己的 source。
最终 linker 再把它们组合起来。
这使得:

CONFIG_UART=n

时:

UART driver

甚至可以完全不进入最终 firmware。

13. 一个完整 Company UART 的例子

现在把:

28 UART
29 GPIO/SPI/I2C/Timer
30 Board
31 Kconfig
32 CMake

真正串起来。

假设:

drivers/serial/
├── CMakeLists.txt
└── uart_company.c

Kconfig:

config UART_COMPANY
	bool "Company UART driver"
	depends on SERIAL

CMake:

zephyr_library()
	zephyr_library_sources_ifdef(
	CONFIG_UART_COMPANY
	uart_company.c
)

Devicetree:

&uart0 {
	compatible = "company,my-uart";
	status = "okay";
};

Driver:

#define DT_DRV_COMPAT company_my_uart

static int company_uart_init(const struct device *dev)
{
	/* UART hardware initialization */
	return 0;
}

最终:

DTS
↓
uart0 enabled

Kconfig
↓
CONFIG_UART_COMPANY=y

CMake
↓
uart_company.c included

Compiler
↓
uart_company.c.o

Linker
↓
zephyr.elf

这就是一个完整 Zephyr driver 从描述到 firmware 的生命周期。

14. 最容易踩的坑:Kconfig 开了,但代码没编译

例如:

CONFIG_UART_COMPANY=y

你以为:

uart_company.c

肯定会编译。
其实不一定。
如果 CMake 没有:

zephyr_library_sources_ifdef(
	CONFIG_UART_COMPANY
	uart_company.c
)

那么:

Kconfig
↓
CONFIG_UART_COMPANY=y

虽然存在,
但:

uart_company.c

可能根本没有进入 build。
结果可能出现:

undefined reference

例如:

undefined reference to `__device_dts_ord_...`

15. 反过来也一样

CMake 可以加入:

zephyr_library_sources(
	uart_company.c
)

但是 Kconfig 没有正确配置:

CONFIG_UART_COMPANY

那么 source 虽然被编译:

uart_company.c.o

但是里面可能:

# if defined(CONFIG_UART_COMPANY)
...
# endif

没有有效代码。
所以:
Kconfig 和 CMake 必须配套。

16. 如何检查一个 .c 到底有没有被编译?

这是 BSP 调试中非常实用的技能。
先:

west build -t pristine
west build -b your_board your_app

然后:

grep -R "uart_company.c" build/

或者:

grep -R "uart_company.c.o" build/

如果出现:

build.ninja

中的编译规则,说明 CMake 已经把它加入 build。
也可以:

ninja -C build -v

看完整编译命令。

17. 一个非常实用的调试思路

以后看到:

undefined reference

不要马上修改 C 代码。
先沿着这条链检查:

1. Kconfig
      ↓
2. CONFIG_xxx=y ?
      ↓
3. CMake
      ↓
4. source 是否加入 build?
      ↓
5. .o 是否生成?
      ↓
6. symbol 是否存在?
      ↓
7. linker 是否把它链接进去?

例如:

grep CONFIG_UART build/zephyr/.config

然后:

grep uart_company build/build.ninja

然后:

find build -name '\*uart_company\*'

最后:

arm-none-eabi-nm build/zephyr/zephyr.elf | grep company_uart

17.1 实战排查:CONFIG_UART_COMPANY=y 但 undefined reference

下面用一个完整例子,把第 17 节的思路走一遍。

场景:你在 prj.conf 里设置了:

CONFIG_UART_COMPANY=y

编译时却报:

undefined reference to `company_uart_init`

或:

undefined reference to `__device_dts_ord_...`

第一步:确认 Kconfig 真的生效

grep CONFIG_UART_COMPANY build/zephyr/.config

输出:

CONFIG_UART_COMPANY=y

说明 Kconfig 配置没问题,CONFIG_UART_COMPANY 确实被打开了。

第二步:搜索 build.ninja,确认 CMake 是否把源文件加入构建

grep uart_company build/build.ninja

如果输出为空,说明 CMake 没有把 uart_company.c 加入构建——问题出在 CMakeLists.txt。

如果输出类似:

build zephyr/drivers/serial/CMakeFiles/drivers__serial.dir/uart_company.c.obj: C_COMPILER__drivers__serial_ ...

说明 CMake 已经把它加入构建,问题可能出在编译或链接阶段。

第三步:确认 .o 文件是否生成

find build -name '*uart_company*'

输出:

build/zephyr/drivers/serial/CMakeFiles/drivers__serial.dir/uart_company.c.obj

如果这个文件存在,说明编译阶段没问题。

第四步:用 nm 检查符号是否存在于最终 ELF

arm-none-eabi-nm build/zephyr/zephyr.elf | grep company_uart

如果输出为空,说明 链接阶段没有把该符号链接进去

第五步:定位根因——检查 CMakeLists.txt

打开:

drivers/serial/CMakeLists.txt

发现内容:

zephyr_library()

zephyr_library_sources(
    uart_company.c
)

这里没有用 zephyr_library_sources_ifdef(),而是无条件编译。但更常见的问题是:

zephyr_library()

zephyr_library_sources_ifdef(
    CONFIG_UART_COMPANY
    uart_company.c
)

先核对 Kconfig 里定义的符号名:

config UART_COMPANY

而 CMake 里引用的是:

CONFIG_UART_COMPANY

注意:Zephyr 的 Kconfig 符号会自动加上 CONFIG_ 前缀,所以 config UART_COMPANY 对应的宏正是 CONFIG_UART_COMPANY,这一处写法是对的。

真正的问题往往出在符号名本身不一致:例如 Kconfig 里定义的是 COMPANY_UART,而 CMake 里却写成了 UART_COMPANY

第六步:修复 CMakeLists.txt

假设 Kconfig 里定义的是:

config COMPANY_UART
	bool "Company UART driver"
	depends on SERIAL

那么 CMakeLists.txt 应该改为:

zephyr_library()

zephyr_library_sources_ifdef(
    CONFIG_COMPANY_UART
    uart_company.c
)

完整 diff

--- a/drivers/serial/CMakeLists.txt
+++ b/drivers/serial/CMakeLists.txt
@@ -1,5 +1,5 @@
 zephyr_library()
 
 zephyr_library_sources_ifdef(
-    CONFIG_UART_COMPANY
+    CONFIG_COMPANY_UART
     uart_company.c
 )

第七步:重新编译验证

west build -t pristine
west build -b your_board your_app

再次检查:

grep CONFIG_COMPANY_UART build/zephyr/.config

输出:

CONFIG_COMPANY_UART=y

然后:

grep uart_company build/build.ninja

确认源文件已加入构建。

最后:

arm-none-eabi-nm build/zephyr/zephyr.elf | grep company_uart

这次能看到符号了:

00000000 T company_uart_init

17.2 实战排查:Kconfig 已开启但 CMake 未加入源文件

下面再走一个更常见的场景:Kconfig 配置完全正确,但 CMakeLists.txt 里漏写了 zephyr_library_sources_ifdef() 条目,导致源文件根本没进构建。

场景:你在 prj.conf 里设置了:

CONFIG_UART_COMPANY=y

编译时却报:

undefined reference to `company_uart_init`

第一步:确认 Kconfig 真的生效

grep CONFIG_UART_COMPANY build/zephyr/.config

输出:

CONFIG_UART_COMPANY=y

Kconfig 没问题,功能确实被启用了。

第二步:搜索 build.ninja,确认 CMake 是否把源文件加入构建

grep uart_company build/build.ninja

输出为空。

这说明 uart_company.c 根本没有被 CMake 加入构建。问题很可能出在 drivers/serial/CMakeLists.txt

第三步:确认 .o 文件是否存在

find build -name '*uart_company*'

输出为空,进一步证实:这个源文件从未被编译。

第四步:用 nm 检查最终 ELF 里的符号

arm-none-eabi-nm build/zephyr/zephyr.elf | grep company_uart

输出为空,说明链接器里根本没有这个符号,所以才会报 undefined reference

第五步:定位根因——检查 CMakeLists.txt

打开:

drivers/serial/CMakeLists.txt

发现内容:

zephyr_library()

zephyr_library_sources(
    uart_company_common.c
)

问题就在这里:CMakeLists.txt 里只有无条件编译的 uart_company_common.c,缺少 zephyr_library_sources_ifdef(CONFIG_UART_COMPANY uart_company.c) 这条条件编译条目。所以即使 Kconfig 里 CONFIG_UART_COMPANY=yuart_company.c 也不会被加入构建。

第六步:修复 CMakeLists.txt

修复前:

zephyr_library()

zephyr_library_sources(
    uart_company_common.c
)

修复后:

zephyr_library()

zephyr_library_sources(
    uart_company_common.c
)

zephyr_library_sources_ifdef(
    CONFIG_UART_COMPANY
    uart_company.c
)

完整 diff

--- a/drivers/serial/CMakeLists.txt
+++ b/drivers/serial/CMakeLists.txt
@@ -1,5 +1,9 @@
 zephyr_library()
 
 zephyr_library_sources(
     uart_company_common.c
 )
+
+zephyr_library_sources_ifdef(
+    CONFIG_UART_COMPANY
+    uart_company.c
+)

第七步:重新编译验证

west build -t pristine
west build -b your_board your_app

再次检查 build.ninja:

grep uart_company build/build.ninja

这次能看到编译规则了:

build zephyr/drivers/serial/CMakeFiles/drivers__serial.dir/uart_company.c.obj: C_COMPILER ...

再确认 .o 文件:

find build -name '*uart_company*'

输出:

build/zephyr/drivers/serial/CMakeFiles/drivers__serial.dir/uart_company.c.obj

最后用 nm 确认符号:

arm-none-eabi-nm build/zephyr/zephyr.elf | grep company_uart

输出:

00000000 T company_uart_init

undefined reference 消失,链接成功。

17.3 实战排查:Devicetree 节点已启用但驱动未初始化

前面两个例子都聚焦在「Kconfig / CMake」这条链上。但还有一种非常隐蔽的情况:Kconfig 开了、CMake 也把源文件编译进去了、链接也成功了,可驱动就是没有被初始化——设备树节点明明 status = "okay"init 函数却从未被调用。

这类问题要沿着 DTS 节点状态 → binding 匹配 → driver 注册宏 → init 函数调用 四步排查。

场景:你在 myboard.dts 里启用了 UART 节点:

&uart0 {
	compatible = "company,my-uart";
	status = "okay";
};

编译、链接都成功,但运行时 company_uart_init 从未被调用,串口完全没反应。

第一步:确认 dts 节点状态

先确认设备树节点确实被启用,且 status 没有被覆盖:

grep -A3 "uart0" build/zephyr/zephyr.dts

预期输出:

uart0: serial@40001000 {
	compatible = "company,my-uart";
	status = "okay";
	reg = <0x40001000 0x400>;
};

如果这里 status = "disabled" 或节点根本没出现,说明问题出在设备树,而不是驱动代码。

第二步:确认 binding 匹配

Zephyr 通过 compatible 字符串查找 binding 文件。检查 binding 是否存在:

find dts/bindings -name "*company*my-uart*"

预期输出:

dts/bindings/serial/company,my-uart.yaml

再确认 binding 里的 compatible 与 dts 节点一致:

grep compatible dts/bindings/serial/company,my-uart.yaml

预期输出:

compatible: "company,my-uart"

如果 binding 文件不存在,或 compatible 拼写不一致,驱动就无法被实例化。

第三步:确认 driver 注册宏

打开驱动源文件,检查 DT_DRV_COMPAT 是否与 binding 的 compatible 对应:

grep -n "DT_DRV_COMPAT" drivers/serial/uart_company.c

预期输出:

#define DT_DRV_COMPAT company_my_uart

注意:DT_DRV_COMPAT 里的写法是把 compatible 字符串 "company,my-uart" 中的逗号和连字符替换为下划线,即 company_my_uart

再确认注册宏存在:

grep -nE "DEVICE_DT_INST_DEFINE|DEVICE_DT_INST_INIT" drivers/serial/uart_company.c

预期输出:

DEVICE_DT_INST_DEFINE(0, company_uart_init, NULL, NULL, NULL, POST_KERNEL,
		      CONFIG_UART_COMPANY_INIT_PRIORITY, &company_uart_driver_api);

如果 DT_DRV_COMPAT 写错,或缺少 DEVICE_DT_INST_DEFINE,驱动就不会被注册。

第四步:确认 init 函数是否被调用

nm 在最终 ELF 里查找设备实例符号:

arm-none-eabi-nm build/zephyr/zephyr.elf | grep -E "device_state|__device_dts_ord"

预期输出(能看到 uart0 对应的设备实例):

00000000 D __device_dts_ord_0
00000000 T company_uart_init

如果 company_uart_init 存在但 __device_dts_ord_0 不存在,说明驱动代码编译进去了,但没有对应的设备树实例——问题出在 DTS 节点或 binding 匹配。

第五步:定位根因——检查 binding 的 compatible 拼写

打开 binding 文件:

cat dts/bindings/serial/company,my-uart.yaml

发现内容:

compatible: "company,my-uart"

而 DTS 节点里写的是:

compatible = "company,my-uart";

看起来一致?再仔细看,DTS 节点里实际写的是:

compatible = "company,my_uart";

注意:binding 里是 company,my-uart(连字符),DTS 里却写成了 company,my_uart(下划线)。Zephyr 的 binding 匹配是严格字符串匹配,一个字符不一致就匹配不上,驱动自然不会被实例化。

第六步:修复 dts 节点

修复前:

&uart0 {
	compatible = "company,my_uart";
	status = "okay";
};

修复后:

&uart0 {
	compatible = "company,my-uart";
	status = "okay";
};

完整 diff

--- a/boards/company/myboard/myboard.dts
+++ b/boards/company/myboard/myboard.dts
@@ -1,5 +1,5 @@
 &uart0 {
-	compatible = "company,my_uart";
+	compatible = "company,my-uart";
 	status = "okay";
 };

第七步:重新编译验证

west build -t pristine
west build -b myboard your_app

再次确认设备树节点:

grep -A3 "uart0" build/zephyr/zephyr.dts

这次能看到:

uart0: serial@40001000 {
	compatible = "company,my-uart";
	status = "okay";
	reg = <0x40001000 0x400>;
};

再检查设备实例符号:

arm-none-eabi-nm build/zephyr/zephyr.elf | grep "__device_dts_ord_0"

这次能看到:

00000000 D __device_dts_ord_0

company_uart_init 会被 Zephyr 内核在启动阶段自动调用,串口恢复正常。

总结:这个例子的根因是 DTS 节点里的 compatible 与 binding 文件不一致(下划线 vs 连字符),导致驱动无法匹配到设备树实例。排查时按「DTS 节点状态 → binding 匹配 → driver 注册宏 → init 函数调用」四步走,就能快速定位是设备树、binding、注册宏还是初始化顺序的问题。

总结:这个例子的根因是 CMakeLists.txt 中缺少 zephyr_library_sources_ifdef() 条目。Kconfig 配置正确,但 CMake 没有把对应的 .c 文件加入构建。排查时按「.config → build.ninja → .o → nm」这条链逐步确认,就能快速定位是 Kconfig、CMake、编译还是链接的问题。

总结:这个例子的根因是 Kconfig 符号名与 CMake 里引用的符号名不一致。排查时按「.config → build.ninja → .o → nm」这条链逐步确认,就能快速定位是 Kconfig、CMake、编译还是链接的问题。

这样就能定位:

Kconfig 问题
    or
CMake 问题
    or
Compile 问题
    or
Link 问题

18. CMake + Kconfig + Devicetree 三角关系

这是本篇最重要的模型:

                    ┌─────────────┐
                    │ Devicetree  │
                    │ “硬件是什么” │
                    └──────┬──────┘
                           │
                           ↓
                    device instance
                           │
                           │
┌──────────────┐           │           ┌──────────────┐
│   Kconfig    │───────────┼──────────→│    CMake     │
│ “启用什么”    │           │           │ “编译什么”    │
└──────────────┘           │           └──────┬───────┘
                           │                  │
                           ↓                  ↓
                       Driver code      Source files
                           │                  │
                           └────────┬─────────┘
                                    ↓
                                 Compiler
                                    ↓
                                  Linker
                                    ↓
                              zephyr.elf

你可以记成一句话:
DTS 决定「实例」,Kconfig 决定「功能」,CMake 决定「代码」,Linker 决定「最终映像」。

19. Company BSP 推荐目录结构

到这里,你的 Company SoC BSP 可以逐渐形成:

zephyr/
│
├── arch/
│   └── ...
│
├── soc/
│   └── company/
│       └── mychip/
│           ├── CMakeLists.txt
│           ├── Kconfig
│           ├── Kconfig.defconfig
│           ├── soc.c
│           ├── clock.c
│           ├── reset.c
│           └── irq.c
│
├── boards/
│   └── company/
│       └── myboard/
│           ├── CMakeLists.txt
│           ├── Kconfig.board
│           ├── Kconfig.defconfig
│           ├── myboard.dts
│           └── myboard.yaml
│
├── dts/
│   └── bindings/
│       └── serial/
│           └── company,my-uart.yaml
│
├── drivers/
│   ├── serial/
│   │   ├── CMakeLists.txt
│   │   ├── Kconfig
│   │   └── uart_company.c
│   │
│   ├── gpio/
│   │   ├── CMakeLists.txt
│   │   └── gpio_company.c
│   │
│   └── spi/
│       ├── CMakeLists.txt
│       └── spi_company.c
│
└── ...

20. 最终完整构建链

现在把 20~32 篇压缩成一张图:

west build -b company_board app

Board 层

SoC 层

Architecture 层

Devicetree
描述硬件

Kconfig
CONFIG_COMPANY_*

DTS nodes

CONFIG 配置

CMake
决定编译哪些源码

SoC .c

Drivers .c

App .c

Compiler

*.o / *.a

Linker

zephyr.elf

zephyr.bin

zephyr.hex

这张图把整条构建链串起来了:west build 命令先经过 Board → SoC → Architecture 三层,确定目标平台;随后 Devicetree 提供硬件描述、Kconfig 提供功能配置,两者共同汇入 CMake,由 CMake 决定到底编译哪些 .c 源文件。源文件经 Compiler 编译成 .o 目标文件与 .a 静态库,再由 Linker 链接成最终的 zephyr.elf,并进一步生成 zephyr.binzephyr.hex 烧录镜像。整条链路中,CMake 是承上启下的枢纽——它把硬件描述、功能配置和源码选择三者统一起来。

21. 这一篇你真正需要掌握的 6 个东西

① CMakeLists.txt

负责描述 build。

② zephyr_library()

创建 Zephyr 管理的 library。

③ zephyr_library_sources()

加入 source:

zephyr_library_sources(foo.c)

④ zephyr_library_sources_ifdef()

让 Kconfig 控制 source:

zephyr_library_sources_ifdef(
	CONFIG_FOO
	foo.c
)

⑤ build.ninja

CMake 最终生成的实际构建规则。

⑥ zephyr.elf

所有代码经过:

CMake
↓
Compiler
↓
Linker

之后得到的最终 ELF。

22. 你现在已经到了一个很重要的阶段

前面的:

20 SoC BSP
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
31 Kconfig
32 CMake

实际上已经组成一个完整的 Company SoC BSP 闭环

              ┌──────────────┐
              │ Company SoC  │
              └──────┬───────┘
                     │
        ┌────────────┼────────────┐
        ↓            ↓            ↓
   Architecture     SoC         Board
        │            │            │
        ↓            ↓            ↓
      Startup      Clock       Devicetree
      IRQ          Reset       Binding
        │            │            │
        └────────────┼────────────┘
                     ↓
                  Drivers
                     ↓
                  Kconfig
                     ↓
                   CMake
                     ↓
                 Compiler
                     ↓
                  Linker
                     ↓
                Firmware
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值