摘要:本文是 Zephyr BSP 系列的开篇,目标是在不编写任何 UART/GPIO/Clock Driver 的前提下,搭建一个"示例公司 SoC"(ACME123)的最小 SoC Port 骨架。文章依次讲解 SoC Port 与 Board Port 的本质区别,并逐一拆解六个核心文件:
soc.yml(SoC 身份)、soc.h(C 语言定义)、Kconfig.soc(SoC 选择)、Kconfig(硬件能力)、Kconfig.defconfig(默认值)与CMakeLists.txt(构建入口)。最后强调 SoC Port 只建立身份与构建骨架,真正的硬件描述需进入dtsi,并梳理了从 SoC Port 到完整 BSP 的演进路径与 ARM 架构边界。
Zephyr SoC Port 的最小骨架
前面 16~20 篇,我们已经把:
Devicetree → dependency ordinal → struct device → Driver → SoC/Board这一整条链路拆开了。
从这一篇开始,真正进入你的目标:
把公司的 SoC 做成 Zephyr BSP。
Zephyr 当前的 SoC Porting Guide 明确规定了 SoC 支持的基本目录和文件;其中 soc.yml、soc.h、Kconfig.soc、CMakeLists.txt 是核心骨架。
这一篇先不写 UART Driver、不写 GPIO Driver、不写 Clock Driver。
我们只做一件事:
让一个"示例公司 SoC"能够被 Zephyr 识别、配置并参与编译。
1. 先明确:SoC Port 到底在做什么?
你最终想达到的是:
Zephyr │ ▼ ┌────────────┐ │ Your SoC │ │ MYSoC1234 │ └────────────┘ │ ┌────────────┼────────────┐ ▼ ▼ ▼ Cortex-M4 UART GPIO │ │ │ └────────────┼────────────┘ ▼ Your Board │ ▼ hello_world注意一个非常重要的概念:
SoC Port ≠ Board Port
SoC 描述的是:
芯片本身有什么。
Board 描述的是:
这块开发板上把这些东西怎么接出来。
例如你的公司芯片:
MYSoC1234 CPU ├── Cortex-M4 ├── NVIC └── SysTick Memory ├── Flash └── SRAM Peripherals ├── UART0 ├── UART1 ├── GPIO0 ├── TIMER0 └── SPI0而开发板:
MY-BOARD-A MYSoC1234 │ ├── UART0 → USB-UART ├── GPIO0 → LED ├── GPIO1 → Button └── SPI0 → Sensor因此:
SoC Port ↓ 让 Zephyr 认识 MYSoC1234 Board Port ↓ 告诉 Zephyr MY-BOARD-A 怎么使用 MYSoC1234这也是为什么官方 Board Porting Guide 要求 Board 建立在已经支持的 SoC 之上。
2. 今天我们先造一个示例 SoC
为了以后你的公司 SoC 可以直接套这个结构,我们假设:
Vendor: acme SoC: acme123 Architecture: ARM CPU: Cortex-M4最终希望形成:
zephyr/ └── soc/ └── acme/ └── acme123/ ├── soc.yml ├── soc.h ├── CMakeLists.txt ├── Kconfig ├── Kconfig.soc └── Kconfig.defconfig这正是当前 Zephyr SoC Porting Guide 给出的基本 SoC 骨架。
3. 第一个文件:soc.yml
这是一个非常容易被误解的文件。
它不是 Devicetree。
它也不是:
SoC 有哪些 UART? SoC 有哪些 GPIO? UART 地址是多少?这些信息不应该放在这里。
soc.yml 描述的是:
SoC 在 Zephyr Hardware Model 中的身份。
最简单:
socs: - name: acme123也就是说:
Zephyr Hardware Model Vendor │ └── ACME │ └── acme123如果你的公司以后有:
ACME123 ACME125 ACME128甚至:
ACME SoC Family │ ├── ACME100 Series │ ├── ACME101 │ └── ACME102 │ └── ACME200 Series ├── ACME201 └── ACME202那么 soc.yml 就可以描述 family / series / SoC 的层级。
例如:
family: - name: acme_m series: - name: acme1 socs: - name: acme123 - name: acme125官方文档也是通过 family → series → socs 来描述这种层级关系。
4. 第二个文件:soc.h
这个文件看起来很普通:
#ifndefZEPHYR_SOC_ACME_ACME123_SOC_H_#defineZEPHYR_SOC_ACME_ACME123_SOC_H_#defineACME123_SOC_ID0x1234#endif它的意义是:
给 Zephyr 的 C/C++ 世界提供 SoC-specific 定义。
例如以后可能出现:
#defineACME123_SOC_ID0x1234#defineACME123_FLASH_SIZE0x00080000#defineACME123_SRAM_SIZE0x00010000或者:
#defineACME123_HAS_FPU#defineACME123_HAS_CACHE但要注意:
不要把整个 SoC 硬件描述都塞进 soc.h。
例如:
#defineUART0_BASE0x40000000#defineGPIO0_BASE0x40010000#defineTIMER0_BASE0x40020000这种内容未来通常应该通过:
Devicetree来描述。
5. 第三个文件:Kconfig.soc
这是整个 SoC Port 最重要的文件之一。
假设:
soc.yml acme123那么我们需要告诉 Kconfig:
SOC=acme123最小版本可以是:
config SOC_ACME123 bool config SOC default"acme123"ifSOC_ACME123于是:
Kconfig │ ▼SOC_ACME123=y │ ▼SOC="acme123"6. 为什么需要两个东西?
你可能会问:
为什么不是直接:
config SOC default"acme123"而要:
SOC_ACME123 │ ▼SOC="acme123"因为 Zephyr 的 Hardware Model 需要一个可选择的 SoC symbol。
也就是说:
SOC_ACME123表示:
当前选择了哪个 SoC。
而:
SOC="acme123"表示:
当前 SoC 的名字是什么。
官方当前的 Kconfig.soc 示例也是通过 SOC_<SOC_NAME> 最终给全局 SOC 字符串提供 default。
7. 如果有 Series / Family
假设你的公司 SoC 以后是:
ACME Family │ └── ACME1 Series │ ├── ACME123 ├── ACME125 └── ACME128那么:
config SOC_FAMILY_ACME bool config SOC_SERIES_ACME1 boolselectSOC_FAMILY_ACME config SOC_ACME123 boolselectSOC_SERIES_ACME1 config SOC_FAMILY default"acme"ifSOC_FAMILY_ACME config SOC_SERIES default"acme1"ifSOC_SERIES_ACME1 config SOC default"acme123"ifSOC_ACME123于是:
SOC_FAMILY │ ▼ acme │ ▼ SOC_SERIES │ ▼ acme1 │ ▼ SOC │ ▼ acme123这就是以后公司真正做产品线时非常重要的结构。
8. 第四个文件:Kconfig
这个文件和 Kconfig.soc 不一样。
可以把它理解成:
Kconfig.soc ↓"我是谁?"Kconfig ↓"我有什么硬件能力?"例如:
ifSOC_ACME123 config ACME123_HAS_UART bool default y config ACME123_HAS_GPIO bool default y endif或者 CPU 相关:
config SOC_ACME123selectARMselectCPU_HAS_FPU官方文档特别强调:
Kconfig.soc 主要用于 SoC tree 本身的选择;需要选择一般 Zephyr Kconfig 能力时,应使用 Kconfig。
这个区别以后会非常重要。
9. 第五个文件:Kconfig.defconfig
这个文件主要用来提供:
SoC-specific 默认值。
例如:
ifSOC_ACME123 config NUM_IRQS default64endif意思:
如果选择 ACME123 │ ▼ NUM_IRQS 默认=64而不是强制用户:
CONFIG_NUM_IRQS=64官方要求 Kconfig.defconfig 中的内容通常放在:
ifSOC_ACME123... endif之内。
10. 第六个文件:CMakeLists.txt
这是 SoC Port 进入 Build System 的入口。
最小可以:
zephyr_include_directories(.)以后慢慢增加:
zephyr_sources(soc.c clock.c)或者:
zephyr_include_directories(.include)再以后可能变成:
CMakeLists.txt │ ├── startup ├── clock ├── pinctrl ├── interrupt └── system官方说明 CMakeLists.txt 可以定义 SoC 所需要的 source files、include paths,并指定基础 linker script。
11. 到这里,我们已经有了一个"最小 SoC 骨架"
完整结构:
soc/ └── acme/ └── acme123/ │ ├── soc.yml │ ├── soc.h │ ├── CMakeLists.txt │ ├── Kconfig.soc │ ├── Kconfig │ └── Kconfig.defconfig可以把它记成:
ACME123 SoC │ ┌──────────┼──────────┐ │ │ │ ▼ ▼ ▼ soc.yml Kconfig CMake │ │ │ │ │ └── 编译什么 │ │ │ └───────────── SoC 配置 │ └──────────────────────── SoC 身份12. 但是这里有一个关键问题
到目前为止:
ACME123虽然已经存在于 Zephyr 的 SoC tree 中,
但是 Zephyr 还不知道 ACME123 的硬件长什么样。
我们现在没有:
CPU RAM Flash UART GPIO Timer Interrupt Clock所以还不能真正运行:
device_is_ready(...)因为:
SoC Port只是建立了SoC 身份和构建骨架。
真正的硬件描述下一步要进入:
dts/<ARCH>/<VENDOR>/<soc>.dtsi12.1 最小实战:acme123.dtsi
既然真正的硬件描述要进入dtsi,那我们就直接动手写一个最小可用的acme123.dtsi。它位于:
dts/arm/acme/acme123.dtsi最小版本只需要三个节点:cpus、sram0、flash0。
/* * dts/arm/acme/acme123.dtsi * ACME123 SoC 最小 Devicetree 描述 */ /dts-v1/; #include <arm/armv7-m.dtsi> / { soc { sram0: memory@20000000 { compatible = "mmio-sram"; reg = <0x20000000 0x00010000>; }; flash0: flash@08000000 { compatible = "soc-nv-flash"; reg = <0x08000000 0x00080000>; }; }; }; &cpus { cpu0: cpu@0 { compatible = "arm,cortex-m4"; }; };每个节点与 soc.yml / Kconfig 的对应关系
很多初学者会困惑:dtsi里的节点和前面写的soc.yml、Kconfig到底是什么关系?其实它们各司其职:
soc.yml │ └── 告诉 Zephyr Hardware Model:ACME123 是谁 │ Kconfig.soc / Kconfig │ └── 告诉 Build System:ACME123 有哪些可配置能力 │ acme123.dtsi │ └── 告诉 Devicetree:ACME123 的硬件长什么样具体到每个节点:
cpus │ ├── 对应 soc.yml 中的 CPU 描述(Cortex-M4) ├── 对应 Kconfig 中的selectARM /selectCPU_HAS_FPU └── 告诉 Zephyr:这个 SoC 用哪个 CPU 核sram0 │ ├── 对应 soc.h 中的 ACME123_SRAM_SIZE(0x00010000) ├── 对应 Kconfig.defconfig 中的 SRAM 相关默认值 └── 告诉 Zephyr:SRAM 的起始地址和大小flash0 │ ├── 对应 soc.h 中的 ACME123_FLASH_SIZE(0x00080000) ├── 对应 Kconfig.defconfig 中的 Flash 相关默认值 └── 告诉 Zephyr:Flash 的起始地址和大小Board DTS 如何 include 这个 dtsi
acme123.dtsi描述的是芯片本身,而 Board DTS 描述的是开发板怎么用这颗芯片。所以 Board DTS 的第一件事就是把这个 SoC 的 dtsi 包含进来:
/* * boards/acme/acme_devkit/acme_devkit.dts */ /dts-v1/; #include <acme/acme123.dtsi> / { model = "ACME ACME123 DevKit"; compatible = "acme,acme_devkit"; chosen { zephyr,console = &uart0; zephyr,sram = &sram0; zephyr,flash = &flash0; }; };注意这里:
#include <acme/acme123.dtsi>这个 include 路径是相对于 Zephyr 的dts/目录的。也就是说,Zephyr 会自动在:
zephyr/dts/下面搜索acme/acme123.dtsi。这也是为什么官方要求 SoC Devicetree 文件必须放在dts/<ARCH>/<VENDOR>/目录下的原因——Board DTS 才能用统一的 include 路径找到它。
一旦 Board DTS include 了acme123.dtsi,sram0和flash0就自动成为 Board 的一部分,chosen节点再把它们指定为 Zephyr 的默认 SRAM 和 Flash,这样hello_world才能知道把代码和数据放在哪里。
官方 SoC Porting Guide 规定 SoC Devicetree 文件位于架构/厂商对应目录,并且每个使用该 SoC 的 Board DTS 都需要包含它。
13. 所以整个 SoC Port 可以分成几个层次
这点你以后做公司 BSP 时一定要记住:
SoC Port │ ┌──────────────┼──────────────┐ ▼ ▼ ▼ Hardware Model Build Model Hardware Description │ │ │ soc.yml Kconfig DTSI │ │ │ │ │ ├── CPU │ │ ├── RAM │ │ ├── UART │ │ ├── GPIO │ │ └── Timer │ │ │ ├── SOC │ ├── CPU │ └── features │ └── Family/Series/SoC这就是你从"会写 Zephyr Driver"进入"做 Zephyr BSP"之后需要建立的思维方式。
14. 最小骨架和完整 BSP 的区别
今天的最小骨架
soc/acme/acme123/ ├── soc.yml ├── soc.h ├── Kconfig ├── Kconfig.soc ├── Kconfig.defconfig └── CMakeLists.txt目标:
Zephyr │ ▼ 知道 ACME123 是一个合法 SoC后面的完整 SoC Port
最终会逐渐变成:
soc/ └── acme/ └── acme1/ │ ├── acme123/ │ │ ├── soc.yml │ ├── soc.h │ ├── CMakeLists.txt │ ├── Kconfig │ ├── Kconfig.soc │ ├── Kconfig.defconfig │ │ │ ├── startup.c │ ├── system.c │ ├── clock.c │ └──... │ └──...同时:
dts/ └── arm/ └── acme/ ├── acme123.dtsi └── acme125.dtsi然后:
drivers/ ├── serial/ │ └──... ├── gpio/ │ └──... ├── clock_control/ │ └──... └── pinctrl/ └──...14.1 从最小骨架到完整 BSP 的演进路线图
把上面「今天的最小骨架」和「后面的完整 SoC Port」串起来,整个演进过程可以拆成 5 个阶段。每个阶段都有明确的目标、新增文件、验证方法和预计工作量,方便你规划公司 BSP 的落地节奏。
| 阶段 | 目标 | 新增文件 | 验证方法 | 预计工作量 |
|---|---|---|---|---|
| 1. 最小骨架 | 让 Zephyr 识别 ACME123 是一个合法 SoC,能参与编译 | soc.yml、soc.h、Kconfig.soc、Kconfig、Kconfig.defconfig、CMakeLists.txt | west build能通过,CONFIG_SOC="acme123"生效 | 0.5~1 天 |
| 2. DTSI 硬件描述 | 让 Devicetree 知道 ACME123 的 CPU、SRAM、Flash 长什么样 | dts/arm/acme/acme123.dtsi | west build -t dtc无报错,chosen能指向sram0/flash0 | 1~2 天 |
| 3. 启动代码 | 让 CPU 能完成复位、时钟、内存初始化,跑通hello_world | startup.c、system.c、clock.c、linker.ld | hello_world在板子上打印输出 | 3~5 天 |
| 4. 驱动移植 | 让 UART/GPIO/Clock 等外设能被device_is_ready()识别并驱动 | drivers/serial/、drivers/gpio/、drivers/clock_control/、drivers/pinctrl/ | 对应samples跑通,外设读写正常 | 5~10 天 |
| 5. Board 集成 | 把 SoC 接到具体开发板上,形成完整 BSP | boards/acme/acme_devkit/(board.c、board.dts、board_defconfig、Kconfig.board) | 官方samples全量跑通,twister测试通过 | 3~5 天 |
几点说明:
- 阶段 1 和 2 可以并行:
soc.yml建立身份,acme123.dtsi建立硬件描述,两者互不依赖,可以同时开工。 - 阶段 3 是分水岭:从「能编译」到「能运行」,需要真正理解 ARM Cortex-M 的启动流程和链接脚本,工作量会明显上升。
- 阶段 4 是长期投入:每个外设驱动都是独立的一块,建议按「先 UART(能打印调试)→ 再 GPIO(能点灯)→ 最后 Clock/Pinctrl」的顺序推进。
- 阶段 5 是收尾:Board 集成本身不难,但要做
twister回归测试,确保整个 BSP 在 CI 里稳定。
最后才有:
boards/ └── acme/ └── acme_devkit/15. 最重要的一张图
把你前面 01~20 篇和现在的 BSP 学习串起来:
Zephyr BSP │ ┌────────────┴────────────┐ │ │ SoC Board │ │ ┌──────┼──────┐ │ │ │ │ │ ▼ ▼ ▼ ▼ soc.yml Kconfig DTSI board DTS │ │ │ │ │ │ ├── CPU │ │ │ ├── RAM │ │ │ ├── UART │ │ │ └── GPIO │ │ │ │ │ └──────────┐ │ │ │ │ └─────────────────┼──────────────┘ ▼ Zephyr Build │ ▼ Devicetree │ ▼ dependency ordinal │ ▼ DEVICE_DT_DEFINE │ ▼ struct device │ ▼ Driver │ ▼ Application这张图其实就是你这个专栏从:
01 →20最终要汇聚到:
公司 SoC BSP的主线。
16. 还有一个非常重要的边界
如果你的公司 SoC 是:
ARM Cortex-M4那么:
你不需要重新做 Architecture Port。
因为 ARM Cortex-M 架构已经是 Zephyr 支持的架构。
你的工作是:
Architecture │ │ 已经存在 ▼ ARM Cortex-M │ ▼ SoC Port │ ▼ ACME123 │ ▼ Board Port │ ▼ ACME123-EVK只有当你们公司的 SoC 使用一个 Zephyr 尚未支持的 ISA/ABI,例如一个全新的 CPU 架构时,才会上升到 Architecture Port。Zephyr 官方也明确把 Architecture Port 定义为支持新的 ISA/ABI,并涉及启动、异常/中断、线程上下文切换等核心机制。
17. 因此,"SoC Port 最小骨架"的真正目标
不是:
“创建几个文件。”
而是建立下面这个认识:
公司 SoC │ ┌────────────┼────────────┐ │ │ │ ▼ ▼ ▼ Hardware Software Build identity config integration │ │ │ soc.yml Kconfig CMake │ │ │ └────────────┼────────────┘ │ ▼ Zephyr recognizes your SoC然后下一层才是:
ACME123 │ ▼ acme123.dtsi │ ┌────────────┼────────────┐ ▼ ▼ ▼ CPU RAM UART0 │ ▼ UART Driver │ ▼ struct device