摘要:本文围绕 Zephyr BSP 中 Multi-Board / Multi-Chip 的核心问题展开:哪些能力放在 SoC 层、哪些放在 Board 层、哪些通过 Devicetree/Kconfig 表达。文章从「SoC 描述芯片有什么,Board 描述板子实际用了什么」这一第一原则出发,依次讲解 Family → Variant 的 SoC 组织方式、Board 层与 Devicetree 的职责划分、Board 如何选择 SoC、Kconfig 与 Devicetree 的分工、Pinmux 与 Package Variant 的处理,
目录
- 引言
- Multi-Board / Multi-Chip:一个 SoC,如何同时支持多个 Board?
- 一、先建立一个非常重要的层次
- 二、举一个 Company SoC Family
- 三、正确思路:Family → Variant
- 四、Board 层又是什么?
- 五、Devicetree 在这里就发挥作用了
- 六、Multi-Board 的典型结构
- 六点五、同一 SoC 下不同封装 Board 的目录组织
- 七、那么 Board 如何选择 SoC?
- 七点五、实战示例:cs32a1_evb 的完整 SoC 选择链路
- 七点五·补、实战示例:cs32a2_evb 的 SoC 选择差异
- 七点七、实战示例:Pinmux 与 Package Variant 处理
引言
本文面向正在或即将从事 Zephyr BSP 开发的工程师。阅读本文前,建议你已熟悉 Devicetree 的基本语法(节点、属性、覆盖)以及 Kconfig 的配置机制(symbol、select、source),这样能更顺畅地跟上后面的实战示例。读完本文,你将掌握 Multi-Board / Multi-Chip 的层次划分原则——清楚哪些能力放在 SoC 层、哪些放在 Board 层、哪些通过 Devicetree/Kconfig 表达,并能独立设计出可维护的公司级 BSP 目录结构,让「一个 SoC 家族同时支持多个 Board」变得优雅而可控。
最后给出公司级 BSP 的目录设计与「Board 是产品、SoC 是平台、Driver 是能力、Devicetree 是连接三者的描述语言」的脑内模型。
Multi-Board / Multi-Chip:一个 SoC,如何同时支持多个 Board?
前面我们已经把一条完整的 Company SoC → Zephyr BSP 链路走完了:
Zephyr
│
┌──────┴──────┐
│ Application │
└──────┬──────┘
│
Zephyr Driver
│
Devicetree
│
Board / SoC
│
Company HAL
│
Company SoC
但真实公司的 BSP 通常不会只有:
company_soc_x
└── board_x
而是更像:
Company SoC Family
│
├── SoC-A
│ ├── Board-A1
│ └── Board-A2
│
├── SoC-B
│ ├── Board-B1
│ └── Board-B2
│
└── SoC-C
└── Board-C1
甚至:
SoC-A
│
├── 128-pin package
├── 64-pin package
└── 48-pin package
这时候最重要的问题就来了:
哪些东西应该放在 SoC 层?哪些放在 Board 层?哪些应该通过 Devicetree/Kconfig 选择?
这就是今天这一篇的核心。
一、先建立一个非常重要的层次
建议先记住:
Board
│
┌───────┴────────┐
│ │
SoC/Chip Board HW
│ │
┌────┴────┐ ┌────┴────┐
│ │ │ │
CPU Peripherals LED Button
│ │
└────┬────┘
│
Vendor HAL
换句话说:
SoC 描述「芯片有什么」。 Board 描述「这块板子实际上用了什么」。
这是 Multi-Board / Multi-Chip 的第一原则。
二、举一个 Company SoC Family
假设你的公司有两个芯片:
Company SoC Family
│
├── CS32A1
│
└── CS32A2
它们非常接近:
| Cortex-M4 | ✓ | ✓ |
| UART | 4 | 6 |
| SPI | 2 | 4 |
| I2C | 2 | 3 |
| GPIO | 32 | 64 |
| Timer | 4 | 8 |
| ADC | 1 | 2 |
那么 Zephyr 不应该复制两套完整 BSP。
错误:
soc/
├── cs32a1/
│ ├── uart.c
│ ├── spi.c
│ ├── clock.c
│ ├── timer.c
│ └── ...
│
└── cs32a2/
├── uart.c
├── spi.c
├── clock.c
├── timer.c
└── ...
这样很快会出现:
uart.c
uart.c
uart.c
uart.c
大量代码重复。
三、正确思路:Family → Variant
更合理的是:
soc/
└── company/
└── cs32/
├── common/
│ ├── clock.c
│ ├── reset.c
│ ├── uart.c
│ └── ...
│
├── cs32a1/
│ └── soc.c
│
└── cs32a2/
└── soc.c
也就是:
CS32 Family
│
┌──────────┴──────────┐
│ │
Common HAL Variant
│ │
┌─────┼─────┐ ┌────┴────┐
│ │ │ │ │
UART SPI I2C A1 A2
核心思想: 共享能力共享代码,芯片差异单独处理。
四、Board 层又是什么?
假设:
CS32A1
有两块开发板:
CS32A1
│
├── cs32a1_evb
│
└── cs32a1_devkit
两个板子芯片完全一样。 但是:
EVB:
LED → GPIOA.5
Button → GPIOC.2
UART → UART0
而:
DevKit:
LED → GPIOB.3
Button → GPIOA.7
UART → UART2
那么:
SoC
│
├── UART0
├── UART1
├── UART2
├── GPIOA
├── GPIOB
└── GPIOC
是 SoC 的能力。 而:
LED → GPIOA.5
是 Board 的连接关系。 所以不能把:
LED_GPIO = GPIOA_5;
硬编码进 SoC driver。
五、Devicetree 在这里就发挥作用了
例如:
/ {
aliases {
led0 = &user_led;
};
leds {
compatible = "gpio-leds";
user_led: led_0 {
gpios = <&gpioa 5 GPIO_ACTIVE_HIGH>;
};
};
};
另一个 Board:
/ {
aliases {
led0 = &user_led;
};
leds {
compatible = "gpio-leds";
user_led: led_0 {
gpios = <&gpiob 3 GPIO_ACTIVE_HIGH>;
};
};
};
Application 完全不需要知道:
GPIOA.5
还是:
GPIOB.3
Application 只知道:
GPIO_DT_SPEC_GET(DT_ALIAS(led0), gpios)
这就是: Board variation 被 Devicetree 吸收了。
六、Multi-Board 的典型结构
最终可能变成:
boards/
└── company/
└── cs32a1/
├── cs32a1_evb/
│ ├── cs32a1_evb.dts
│ ├── cs32a1_evb.yaml
│ └── CMakeLists.txt
│
└── cs32a1_devkit/
├── cs32a1_devkit.dts
├── cs32a1_devkit.yaml
└── CMakeLists.txt
六点五、同一 SoC 下不同封装 Board 的目录组织
上一节我们看到了 cs32a1_evb 和 cs32a1_devkit 两块板子共用同一颗 CS32A1 的目录结构。这一节我们把视角再往下推一层:同一颗 SoC,还有不同封装——cs32a1 有 64-pin 和 48-pin 两种封装,对应两块板子 cs32a1_64evb 和 cs32a1_48evb。它们用的是同一颗 SoC、同一个 dtsi、同一套 driver,唯一的差异就是封装引出的引脚数量不同。
完整目录树:
boards/
└── company/
└── cs32/
├── cs32a1_64evb/ # ★ 64-pin 封装的 EVB 板
│ ├── cs32a1_64evb.dts # 板级 Devicetree:使能外设 + 选择引脚
│ ├── cs32a1_64evb.yaml # 声明 soc: cs32a1 + 板卡信息
│ ├── Kconfig.defconfig # 选择 SOC_CS32A1_PACKAGE_64
│ └── CMakeLists.txt
│
└── cs32a1_48evb/ # ★ 48-pin 封装的 EVB 板
├── cs32a1_48evb.dts # 板级 Devicetree:使能外设 + 选择引脚
├── cs32a1_48evb.yaml # 声明 soc: cs32a1 + 板卡信息
├── Kconfig.defconfig # 选择 SOC_CS32A1_PACKAGE_48
└── CMakeLists.txt
对应的 SoC 层(同一颗 SoC,不复制):
soc/
└── company/
└── cs32/
├── CMakeLists.txt
├── Kconfig
│
├── cs32a1/ # ★ 唯一的 SoC 目录,被两块板子共用
│ ├── soc.c
│ └── Kconfig # 内含 Package Variant choice
│
└── cs32a2/
├── soc.c
└── Kconfig
dts 层(同一份 dtsi,被两块板子共用):
dts/
└── arm/
└── company/
└── cs32/
└── cs32a1.dtsi # ★ 唯一的 SoC dtsi,含 #if 条件包含
关键点:
这就是「同一 SoC、不同封装」的目录组织方式——SoC 层只维护一份,封装差异通过 Kconfig + 条件包含表达,Board 层各自独立。这正是 Multi-Board 模型在 Package Variant 层面的落地。
而 SoC:
soc/
└── company/
└── cs32/
├── CMakeLists.txt
├── Kconfig
│
├── cs32a1/
│ ├── soc.c
│ └── Kconfig
│
└── cs32a2/
├── soc.c
└── Kconfig
七、那么 Board 如何选择 SoC?
这是整个 Multi-Chip BSP 最关键的一层。
Board:
cs32a1_evb
需要告诉 Zephyr:
我的 SoC 是 CS32A1
概念上:
BOARD
│
▼
SOC
│
▼
CPU / HAL / Drivers
也就是:
cs32a1_evb
│
▼
CS32A1
│
▼
Cortex-M4
七点五、实战示例:cs32a1_evb 的完整 SoC 选择链路
下面以 cs32a1_evb 为例,把「Board 如何选择 SoC」这条链路完整走一遍。整个过程分三步:board.yaml 声明 SoC → Kconfig 使能 SoC → Devicetree 使能外设。
第一步:board.yaml —— 声明「这块板子用哪个 SoC」
# boards/company/cs32/cs32a1_evb/cs32a1_evb.yaml
identifier: cs32a1_evb # Board 的唯一标识,构建时用 -b cs32a1_evb 指定
name: CS32A1 Evaluation Board # 人类可读的板卡名称
type: board # 类型固定为 board
arch: arm # 架构:ARM Cortex-M4
toolchain:
– zephyr # 使用 Zephyr SDK 工具链
ram: 64 # 板载 RAM 大小(KB),供链接脚本使用
flash: 256 # 板载 Flash 大小(KB)
soc: cs32a1 # ★ 关键:声明本 Board 使用的 SoC 是 cs32a1
# Zephyr 据此找到 soc/company/cs32/cs32a1/ 目录
作用:soc: cs32a1 是 Board 与 SoC 之间的「契约」。Zephyr 构建系统读取这一行后,会自动把 cs32a1 对应的 SoC 目录加入编译,并触发后续 Kconfig 的默认选择。
第二步:Kconfig —— 让 CONFIG_SOC_CS32A1 生效
Board 声明了 soc: cs32a1 后,Zephyr 会去 SoC 的 Kconfig 里找到对应的 symbol:
# soc/company/cs32/cs32a1/Kconfig
config SOC_CS32A1
bool "CS32A1 SoC"
select CPU_CORTEX_M4 # 自动选中 Cortex-M4 内核
select HAS_FLASH_LOAD_OFFSET # 声明支持 Flash 加载偏移
help
Company CS32A1 SoC, Cortex-M4 based.
# soc/company/cs32/Kconfig —— 家族入口,把各 Variant 挂进来
config SOC_SERIES_CS32
bool "CS32 Series"
select SOC_FAMILY_COMPANY # 选中公司 SoC Family
source "soc/company/cs32/cs32a1/Kconfig"
source "soc/company/cs32/cs32a2/Kconfig"
构建时,Zephyr 根据 board.yaml 的 soc: cs32a1 自动生成默认配置:
# build/zephyr/.config 中自动生成的关键项
CONFIG_SOC_CS32A1=y # ★ 由 board.yaml 的 soc 字段自动推导
CONFIG_SOC_CS32A2=n # 另一个 Variant 未被选中
CONFIG_SOC_SERIES_CS32=y # 家族被选中
CONFIG_CPU_CORTEX_M4=y # 由 select CPU_CORTEX_M4 自动带上
作用:CONFIG_SOC_CS32A1=y 让 Zephyr 知道「当前编译的是 CS32A1」,从而把 soc/company/cs32/cs32a1/soc.c 编进来,并让 soc/company/cs32/common/ 里的共享代码(uart.c、clock.c 等)以 CS32A1 的寄存器定义编译。
第三步:Devicetree —— 使能 Board 实际用到的外设
SoC 的 .dtsi 里,UART0 默认是 disabled(芯片有,但板子不一定用):
/* dts/arm/company/cs32/cs32a1.dtsi —— SoC 层:描述芯片有什么 */
/ {
soc {
uart0: uart@40000000 {
compatible = "company,cs32-uart";
reg = <0x40000000 0x1000>;
interrupts = <10>;
status = "disabled"; /* 默认关闭,等 Board 决定 */
};
uart1: uart@40001000 {
compatible = "company,cs32-uart";
reg = <0x40001000 0x1000>;
interrupts = <11>;
status = "disabled";
};
};
};
Board 的 .dts 里,只把实际用到的 UART0 打开:
/* boards/company/cs32/cs32a1_evb/cs32a1_evb.dts —— Board 层:板子实际用了什么 */
#include <company/cs32/cs32a1.dtsi> /* 先引入 SoC 的完整描述 */
/ {
model = "CS32A1 Evaluation Board";
compatible = "company,cs32a1-evb", "company,cs32a1";
chosen {
zephyr,console = &uart0; /* 控制台输出到 UART0 */
zephyr,shell-uart = &uart0;
};
};
/* ★ 关键:Board 覆盖 SoC 的默认配置,使能 UART0 */
&uart0 {
status = "okay"; /* 打开 UART0 */
current-speed = <115200>; /* 波特率 115200 */
pinctrl-0 = <&uart0_tx_pa2 &uart0_rx_pa3>; /* 引脚复用:TX→PA2, RX→PA3 */
pinctrl-names = "default";
};
/* 未使用的 UART1 保持 disabled,不产生设备实例 */
作用:&uart0 { status = "okay"; } 让 Zephyr 为 UART0 生成设备实例,并绑定 company,cs32-uart 驱动。Application 通过 DEVICE_DT_GET(DT_NODELABEL(uart0)) 就能拿到这个设备,完全不需要关心 SoC 层和 Board 层的细节。
七点五·补、实战示例:cs32a2_evb 的 SoC 选择差异
上一节我们用 cs32a1_evb 走通了「Board 如何选择 SoC」的完整链路。这一节我们换一块芯片 cs32a2_evb,看看当 SoC 换成 CS32A2 时,这条链路有哪些相同、哪些不同。整个过程同样分三步:board.yaml 声明 SoC → Kconfig 使能 SoC → Devicetree 使能外设,但每一步的「内容」都体现了 CS32A2 与 CS32A1 的差异。
第一步:board.yaml —— 声明「这块板子用 cs32a2」
# boards/company/cs32/cs32a2_evb/cs32a2_evb.yaml
identifier: cs32a2_evb # Board 的唯一标识,构建时用 -b cs32a2_evb 指定
name: CS32A2 Evaluation Board # 人类可读的板卡名称
type: board # 类型固定为 board
arch: arm # 架构:ARM Cortex-M4
toolchain:
– zephyr # 使用 Zephyr SDK 工具链
ram: 128 # 板载 RAM 大小(KB),CS32A2 更大
flash: 512 # 板载 Flash 大小(KB),CS32A2 更大
soc: cs32a2 # ★ 关键:声明本 Board 使用的 SoC 是 cs32a2
# Zephyr 据此找到 soc/company/cs32/cs32a2/ 目录
作用:soc: cs32a2 是 Board 与 SoC 之间的「契约」。与 cs32a1_evb 相比,唯一的结构性差异就是这一行——Zephyr 构建系统读取后,会自动把 cs32a2 对应的 SoC 目录加入编译,并触发 CS32A2 的 Kconfig 默认选择。这就是「Board 是产品,SoC 是平台」的体现:换一颗 SoC,Board 只需改一行声明。
第二步:Kconfig —— 让 CONFIG_SOC_CS32A2 生效
Board 声明了 soc: cs32a2 后,Zephyr 会去 SoC 的 Kconfig 里找到对应的 symbol:
# soc/company/cs32/cs32a2/Kconfig
config SOC_CS32A2
bool "CS32A2 SoC"
select CPU_CORTEX_M4 # 自动选中 Cortex-M4 内核
select HAS_FLASH_LOAD_OFFSET # 声明支持 Flash 加载偏移
help
Company CS32A2 SoC, Cortex-M4 based.
# soc/company/cs32/Kconfig —— 家族入口,把各 Variant 挂进来
config SOC_SERIES_CS32
bool "CS32 Series"
select SOC_FAMILY_COMPANY # 选中公司 SoC Family
source "soc/company/cs32/cs32a1/Kconfig"
source "soc/company/cs32/cs32a2/Kconfig"
构建时,Zephyr 根据 board.yaml 的 soc: cs32a2 自动生成默认配置:
# build/zephyr/.config 中自动生成的关键项
CONFIG_SOC_CS32A2=y # ★ 由 board.yaml 的 soc 字段自动推导
CONFIG_SOC_CS32A1=n # 另一个 Variant 未被选中
CONFIG_SOC_SERIES_CS32=y # 家族被选中
CONFIG_CPU_CORTEX_M4=y # 由 select CPU_CORTEX_M4 自动带上
作用:注意对比——cs32a1_evb 构建时是 CONFIG_SOC_CS32A1=y、CONFIG_SOC_CS32A2=n;而 cs32a2_evb 恰好反过来,是 CONFIG_SOC_CS32A2=y、CONFIG_SOC_CS32A1=n。同一个家族 Kconfig 文件,通过 board.yaml 的 soc 字段自动切换选中哪个 Variant,这就是 Family → Variant 组织方式在构建层面的落地。
第三步:Devicetree —— 使能 CS32A2 多出的 uart4/uart5
CS32A2 比 CS32A1 多了两个 UART(见第二节的对比表:CS32A1 有 4 个 UART,CS32A2 有 6 个)。SoC 的 .dtsi 里,CS32A2 完整描述这 6 个 UART,且默认都是 disabled:
/* dts/arm/company/cs32/cs32a2.dtsi —— SoC 层:描述芯片有什么 */
/ {
soc {
uart0: uart@40000000 {
compatible = "company,cs32-uart";
reg = <0x40000000 0x1000>;
interrupts = <10>;
status = "disabled"; /* 默认关闭,等 Board 决定 */
};
uart1: uart@40001000 {
compatible = "company,cs32-uart";
reg = <0x40001000 0x1000>;
interrupts = <11>;
status = "disabled";
};
uart2: uart@40002000 {
compatible = "company,cs32-uart";
reg = <0x40002000 0x1000>;
interrupts = <12>;
status = "disabled";
};
uart3: uart@40003000 {
compatible = "company,cs32-uart";
reg = <0x40003000 0x1000>;
interrupts = <13>;
status = "disabled";
};
/* ★ CS32A2 独有:比 CS32A1 多出的两个 UART */
uart4: uart@40004000 {
compatible = "company,cs32-uart";
reg = <0x40004000 0x1000>;
interrupts = <14>;
status = "disabled"; /* 芯片有,但板子不一定用 */
};
uart5: uart@40005000 {
compatible = "company,cs32-uart";
reg = <0x40005000 0x1000>;
interrupts = <15>;
status = "disabled";
};
};
};
Board 的 .dts 里,cs32a2_evb 不仅使能了 UART0 作为控制台,还额外使能了 CS32A2 独有的 uart4/uart5:
/* boards/company/cs32/cs32a2_evb/cs32a2_evb.dts —— Board 层:板子实际用了什么 */
#include <company/cs32/cs32a2.dtsi> /* 先引入 CS32A2 的完整描述 */
/ {
model = "CS32A2 Evaluation Board";
compatible = "company,cs32a2-evb", "company,cs32a2";
chosen {
zephyr,console = &uart0; /* 控制台输出到 UART0 */
zephyr,shell-uart = &uart0;
};
};
/* ★ 关键:Board 覆盖 SoC 的默认配置,使能 UART0 */
&uart0 {
status = "okay"; /* 打开 UART0 */
current-speed = <115200>; /* 波特率 115200 */
pinctrl-0 = <&uart0_tx_pa2 &uart0_rx_pa3>; /* 引脚复用:TX→PA2, RX→PA3 */
pinctrl-names = "default";
};
/* ★ CS32A2 独有:Board 使能多出的 uart4/uart5 */
&uart4 {
status = "okay"; /* 打开 UART4 */
current-speed = <115200>;
pinctrl-0 = <&uart4_tx_pc0 &uart4_rx_pc1>; /* 引脚复用:TX→PC0, RX→PC1 */
pinctrl-names = "default";
};
&uart5 {
status = "okay"; /* 打开 UART5 */
current-speed = <9600>;
pinctrl-0 = <&uart5_tx_pc2 &uart5_rx_pc3>; /* 引脚复用:TX→PC2, RX→PC3 */
pinctrl-names = "default";
};
/* 未使用的 uart1/uart2/uart3 保持 disabled,不产生设备实例 */
作用:这里体现了「SoC 描述芯片有什么,Board 描述板子实际用了什么」的完整闭环——CS32A2 的 dtsi 里有 uart4/uart5(芯片能力),但默认 disabled;cs32a2_evb 的 dts 里使能了它们(板级决定)。而 cs32a1_evb 的 dtsi 里根本没有 uart4/uart5 节点,自然也无法使能。外设数量的差异由 SoC 层 dtsi 表达,是否使用由 Board 层 dts 决定。
整条链路串起来看:
cs32a2_evb.yaml
│ soc: cs32a2
▼
Kconfig → CONFIG_SOC_CS32A2=y(CS32A1=n)
│ 使能 CS32A2 及共享 HAL
▼
cs32a2.dtsi → uart0..uart5 存在但 disabled(芯片能力,含多出的 uart4/uart5)
│
▼
cs32a2_evb.dts → &uart0 + &uart4 + &uart5 { status = "okay" }(板级使能)
│
▼
Zephyr 生成 uart0/uart4/uart5 设备实例 → Application 直接使用
两块板子的 SoC 选择链路对比:
| board.yaml 的 soc 字段 | soc: cs32a1 | soc: cs32a2 |
| 生成的 Kconfig symbol | CONFIG_SOC_CS32A1=y | CONFIG_SOC_CS32A2=y |
| 未选中的 Variant | CONFIG_SOC_CS32A2=n | CONFIG_SOC_CS32A1=n |
| 引入的 SoC dtsi | cs32a1.dtsi | cs32a2.dtsi |
| SoC 层 UART 数量 | 4 个(uart0~uart3) | 6 个(uart0~uart5) |
| Board 使能的外设 | uart0 | uart0 + uart4 + uart5 |
CS32A1 vs CS32A2 外设能力对比:
| UART | 4(uart0~uart3) | 6(uart0~uart5) | CS32A2 独有 uart4/uart5 |
| SPI | 2 | 4 | CS32A2 独有 2 个 |
| I2C | 2 | 3 | CS32A2 独有 1 个 |
| GPIO | 32 | 64 | CS32A2 独有 32 个 |
| Timer | 4 | 8 | CS32A2 独有 4 个 |
| ADC | 1 | 2 | CS32A2 独有 1 个 |
| Cortex-M4 | ✓ | ✓ | 共享(同一内核) |
如何读这张表:共享的是「两个 Variant 都有的能力」——它们由 soc/company/cs32/common/ 里的同一份 HAL 代码实现,例如 UART0UART3、SPI0SPI1、I2C0~I2C1 等;CS32A2 独有的是「多出来的能力」——它们只在 cs32a2.dtsi 里描述(如 uart4/uart5),cs32a1.dtsi 里根本没有对应节点。这就是 Family → Variant 的差异表达:共享能力共享代码,芯片差异单独处理。Board 层只需在 dts 里使能自己实际用到的外设,其余保持 disabled 即可。
| 共享 HAL(uart.c 等) | 同一份 soc/company/cs32/common/ | 同一份 soc/company/cs32/common/ |
七点七、实战示例:Pinmux 与 Package Variant 处理
前面我们分别讲了 Pinmux(第十四节)和 Package Variant(第十五节)的概念。这一节我们把两者结合起来,用 cs32a1 的 64-pin 与 48-pin 两种封装为例,完整走一遍:SoC 层 dtsi 定义引脚复用选项 → Board 层 dts 通过 pinctrl-0 选择具体引脚 → Package Variant 通过 Kconfig / dtsi 条件包含区分可用引脚。整个过程分三步。
第一步:SoC 层 dtsi —— 定义 pinctrl 节点与全部引脚复用选项
CS32A1 内部是同一个 silicon,但不同封装暴露的引脚数量不同。SoC 层 dtsi 里,我们把所有封装都可能用到的复用选项都定义出来——注意:这里只描述「芯片支持哪些复用」,不决定「哪个封装用哪一组」:
/* dts/arm/company/cs32/cs32a1.dtsi —— SoC 层:描述芯片有什么 */
/ {
soc {
/* pinctrl 控制器:负责管理所有引脚的复用功能 */
pinctrl: pin-controller@40028000 {
compatible = "company,cs32-pinctrl";
reg = <0x40028000 0x400>;
status = "okay";
/* ★ 引脚复用选项:UART0 的 TX 可以映射到 PA2 或 PB6 */
uart0_tx_pa2: uart0_tx_pa2 {
pinmux = <0x02 0>; /* PA2 复用为 UART0_TX */
};
uart0_tx_pb6: uart0_tx_pb6 {
pinmux = <0x16 0>; /* PB6 复用为 UART0_TX */
};
/* ★ 引脚复用选项:UART0 的 RX 可以映射到 PA3 或 PB7 */
uart0_rx_pa3: uart0_rx_pa3 {
pinmux = <0x03 0>; /* PA3 复用为 UART0_RX */
};
uart0_rx_pb7: uart0_rx_pb7 {
pinmux = <0x17 0>; /* PB7 复用为 UART0_RX */
};
/* ★ 引脚复用选项:SPI0 的 SCK 可以映射到 PA5 或 PB13 */
spi0_sck_pa5: spi0_sck_pa5 {
pinmux = <0x05 1>; /* PA5 复用为 SPI0_SCK */
};
spi0_sck_pb13: spi0_sck_pb13 {
pinmux = <0x2d 1>; /* PB13 复用为 SPI0_SCK */
};
};
uart0: uart@40000000 {
compatible = "company,cs32-uart";
reg = <0x40000000 0x1000>;
interrupts = <10>;
status = "disabled"; /* 默认关闭,等 Board 决定 */
};
spi0: spi@40010000 {
compatible = "company,cs32-spi";
reg = <0x40010000 0x1000>;
interrupts = <30>;
status = "disabled"; /* 默认关闭,等 Board 决定 */
};
};
};
作用:SoC 层把「芯片支持的所有复用组合」都列出来——uart0_tx_pa2、uart0_tx_pb6、spi0_sck_pa5、spi0_sck_pb13 等。具体某个封装能用哪些引脚,由 Package Variant 决定。这就是「SoC 描述芯片有什么」的体现。
第二步:Package Variant —— 通过 Kconfig / dtsi 条件包含区分可用引脚
CS32A1 有 64-pin 和 48-pin 两种封装。48-pin 封装没有引出 PB13(它被 Package 裁掉了),所以 spi0_sck_pb13 这个复用选项在 48-pin 封装上不可用。
方式一:通过 Kconfig 区分(推荐)
# soc/company/cs32/cs32a1/Kconfig
config SOC_CS32A1
bool "CS32A1 SoC"
select CPU_CORTEX_M4 # 自动选中 Cortex-M4 内核
select HAS_FLASH_LOAD_OFFSET # 声明支持 Flash 加载偏移
help
Company CS32A1 SoC, Cortex-M4 based.
# ★ Package Variant 选择:由 Board 的 board.yaml 或 defconfig 决定
choice
prompt "CS32A1 Package Variant"
default SOC_CS32A1_PACKAGE_64
config SOC_CS32A1_PACKAGE_64
bool "64-pin package"
select SOC_CS32A1_HAS_PB13 # ★ 64-pin 封装有 PB13
config SOC_CS32A1_PACKAGE_48
bool "48-pin package"
# 不 select SOC_CS32A1_HAS_PB13 —— 48-pin 封装没有 PB13
endchoice
# ★ 供 dtsi 条件包含使用的 symbol
config SOC_CS32A1_HAS_PB13
bool
方式二:通过 dtsi 条件包含区分
/* dts/arm/company/cs32/cs32a1.dtsi —— SoC 层:描述芯片有什么 */
/ {
soc {
pinctrl: pin-controller@40028000 {
compatible = "company,cs32-pinctrl";
reg = <0x40028000 0x400>;
status = "okay";
uart0_tx_pa2: uart0_tx_pa2 {
pinmux = <0x02 0>;
};
uart0_tx_pb6: uart0_tx_pb6 {
pinmux = <0x16 0>;
};
uart0_rx_pa3: uart0_rx_pa3 {
pinmux = <0x03 0>;
};
uart0_rx_pb7: uart0_rx_pb7 {
pinmux = <0x17 0>;
};
spi0_sck_pa5: spi0_sck_pa5 {
pinmux = <0x05 1>;
};
/* ★ 条件包含:只有 64-pin 封装才有 PB13 */
#if defined(CONFIG_SOC_CS32A1_HAS_PB13)
spi0_sck_pb13: spi0_sck_pb13 {
pinmux = <0x2d 1>; /* PB13 复用为 SPI0_SCK */
};
#endif
};
};
};
作用:Package Variant 的本质是「引脚可用性」的差异,而不是「芯片能力」的差异。CS32A1 的 silicon 始终支持 SPI0_SCK 复用到 PB13,但 48-pin 封装没有把 PB13 引出来,所以这个复用选项必须被裁掉。通过 Kconfig 的 SOC_CS32A1_HAS_PB13 symbol + dtsi 的 #if defined() 条件包含,同一个 dtsi 就能同时服务两种封装。
第三步:Board 层 dts —— 通过 pinctrl-0 选择具体引脚
现在有两块板子,芯片都是 CS32A1,但封装不同:
cs32a1_64evb(64-pin 封装):
# boards/company/cs32/cs32a1_64evb/cs32a1_64evb.yaml
identifier: cs32a1_64evb
name: CS32A1 64–pin Evaluation Board
type: board
arch: arm
toolchain:
– zephyr
ram: 64
flash: 256
soc: cs32a1
# boards/company/cs32/cs32a1_64evb/Kconfig.defconfig
# ★ 选择 64-pin 封装,自动带上 SOC_CS32A1_HAS_PB13
config SOC_CS32A1_PACKAGE_64
default y
/* boards/company/cs32/cs32a1_64evb/cs32a1_64evb.dts —— 64-pin 封装 */
#include <company/cs32/cs32a1.dtsi>
/ {
model = "CS32A1 64-pin Evaluation Board";
compatible = "company,cs32a1-64evb", "company,cs32a1";
chosen {
zephyr,console = &uart0;
zephyr,shell-uart = &uart0;
};
};
/* ★ 64-pin 封装:UART0 走 PA2/PA3,SPI0 走 PB13(有 PB13) */
&uart0 {
status = "okay";
current-speed = <115200>;
pinctrl-0 = <&uart0_tx_pa2 &uart0_rx_pa3>;
pinctrl-names = "default";
};
&spi0 {
status = "okay";
pinctrl-0 = <&spi0_sck_pb13>; /* ★ 引用 PB13 复用选项 */
pinctrl-names = "default";
};
cs32a1_48evb(48-pin 封装):
# boards/company/cs32/cs32a1_48evb/cs32a1_48evb.yaml
identifier: cs32a1_48evb
name: CS32A1 48–pin Evaluation Board
type: board
arch: arm
toolchain:
– zephyr
ram: 64
flash: 256
soc: cs32a1
# boards/company/cs32/cs32a1_48evb/Kconfig.defconfig
# ★ 选择 48-pin 封装,不定义 SOC_CS32A1_HAS_PB13
config SOC_CS32A1_PACKAGE_48
default y
/* boards/company/cs32/cs32a1_48evb/cs32a1_48evb.dts —— 48-pin 封装 */
#include <company/cs32/cs32a1.dtsi>
/ {
model = "CS32A1 48-pin Evaluation Board";
compatible = "company,cs32a1-48evb", "company,cs32a1";
chosen {
zephyr,console = &uart0;
zephyr,shell-uart = &uart0;
};
};
/* ★ 48-pin 封装:UART0 走 PA2/PA3,SPI0 只能走 PA5(没有 PB13) */
&uart0 {
status = "okay";
current-speed = <115200>;
pinctrl-0 = <&uart0_tx_pa2 &uart0_rx_pa3>;
pinctrl-names = "default";
};
&spi0 {
status = "okay";
pinctrl-0 = <&spi0_sck_pa5>; /* ★ 只能引用 PA5,PB13 节点不存在 */
pinctrl-names = "default";
};
作用:两块板子的 &uart0 配置完全一样(都走 PA2/PA3),但 &spi0 的 pinctrl-0 不同——64-pin 封装可以引用 &spi0_sck_pb13,48-pin 封装不能(因为 #if defined(CONFIG_SOC_CS32A1_HAS_PB13) 没被选中,spi0_sck_pb13 节点根本不存在)。如果 48-pin 的板子错误地引用了 &spi0_sck_pb13,Devicetree 编译会直接报错——这就是 Package Variant 在编译期帮你拦截「用了不存在的引脚」的机制。
整条链路串起来看:
cs32a1_64evb.yaml / cs32a1_48evb.yaml
│ soc: cs32a1(同一颗 SoC)
▼
Kconfig → SOC_CS32A1_PACKAGE_64=y(有 PB13)
或 SOC_CS32A1_PACKAGE_48=y(无 PB13)
│ 决定 SOC_CS32A1_HAS_PB13 是否定义
▼
cs32a1.dtsi → #if defined(CONFIG_SOC_CS32A1_HAS_PB13)
│ 决定 spi0_sck_pb13 节点是否存在
▼
Board dts → &spi0 { pinctrl-0 = <&spi0_sck_pb13> }(64-pin)
或 &spi0 { pinctrl-0 = <&spi0_sck_pa5> }(48-pin)
│ 选择具体引脚复用
▼
Zephyr pinctrl 驱动 → 初始化时配置引脚 → SPI0 正常工作
两种封装的对比:
| board.yaml 的 soc 字段 | soc: cs32a1 | soc: cs32a1(同一颗 SoC) |
| Kconfig Package 选择 | SOC_CS32A1_PACKAGE_64=y | SOC_CS32A1_PACKAGE_48=y |
| 是否定义 SOC_CS32A1_HAS_PB13 | 是(select 自动带上) | 否 |
| dtsi 中 spi0_sck_pb13 节点 | 存在 | 不存在(被 #if 裁掉) |
| SPI0 的 pinctrl-0 | <&spi0_sck_pb13> | <&spi0_sck_pa5> |
| UART0 的 pinctrl-0 | <&uart0_tx_pa2 &uart0_rx_pa3> | <&uart0_tx_pa2 &uart0_rx_pa3>(完全一致) |
七点八、实战示例:SPI 与 I2C 的 Multi-Board 差异处理
前面我们用 UART 走通了「同一外设、不同 Board、不同引脚」的 pinctrl 链路。这一节我们把视角从「同一外设的不同引脚」切换到「不同 Board 使能不同外设」——以 cs32a1 的 SPI 与 I2C 为例,展示 cs32a1_evb 使能 spi0、cs32a1_devkit 使能 i2c0 的差异。整个过程分三步:SoC 层 dtsi 定义 spi0/spi1 与 i2c0/i2c1 节点及 pinctrl 复用选项 → Board 层 dts 分别使能不同外设 → 对比表总结差异。
第一步:SoC 层 dtsi —— 定义 spi0/spi1 与 i2c0/i2c1 节点及 pinctrl 复用选项
CS32A1 有 2 个 SPI 和 2 个 I2C(见第二节的对比表)。SoC 的 .dtsi 里,这些外设默认都是 disabled(芯片有,但板子不一定用),同时为每个外设定义可用的引脚复用选项:
/* dts/arm/company/cs32/cs32a1.dtsi —— SoC 层:描述芯片有什么 */
/ {
soc {
/* pinctrl 控制器:负责管理所有引脚的复用功能 */
pinctrl: pin-controller@40028000 {
compatible = "company,cs32-pinctrl";
reg = <0x40028000 0x400>;
status = "okay";
/* ★ SPI0 的引脚复用选项:SCK 可映射到 PA5 或 PB13 */
spi0_sck_pa5: spi0_sck_pa5 {
pinmux = <0x05 1>; /* PA5 复用为 SPI0_SCK */
};
spi0_sck_pb13: spi0_sck_pb13 {
pinmux = <0x2d 1>; /* PB13 复用为 SPI0_SCK */
};
spi0_miso_pa6: spi0_miso_pa6 {
pinmux = <0x06 1>; /* PA6 复用为 SPI0_MISO */
};
spi0_mosi_pa7: spi0_mosi_pa7 {
pinmux = <0x07 1>; /* PA7 复用为 SPI0_MOSI */
};
/* ★ SPI1 的引脚复用选项:SCK 可映射到 PB3 或 PC10 */
spi1_sck_pb3: spi1_sck_pb3 {
pinmux = <0x23 1>; /* PB3 复用为 SPI1_SCK */
};
spi1_sck_pc10: spi1_sck_pc10 {
pinmux = <0x4a 1>; /* PC10 复用为 SPI1_SCK */
};
spi1_miso_pb4: spi1_miso_pb4 {
pinmux = <0x24 1>; /* PB4 复用为 SPI1_MISO */
};
spi1_mosi_pb5: spi1_mosi_pb5 {
pinmux = <0x25 1>; /* PB5 复用为 SPI1_MOSI */
};
/* ★ I2C0 的引脚复用选项:SCL 可映射到 PB6 或 PA9 */
i2c0_scl_pb6: i2c0_scl_pb6 {
pinmux = <0x16 2>; /* PB6 复用为 I2C0_SCL */
};
i2c0_scl_pa9: i2c0_scl_pa9 {
pinmux = <0x09 2>; /* PA9 复用为 I2C0_SCL */
};
i2c0_sda_pb7: i2c0_sda_pb7 {
pinmux = <0x17 2>; /* PB7 复用为 I2C0_SDA */
};
i2c0_sda_pa10: i2c0_sda_pa10 {
pinmux = <0x0a 2>; /* PA10 复用为 I2C0_SDA */
};
/* ★ I2C1 的引脚复用选项:SCL 可映射到 PB8 或 PC4 */
i2c1_scl_pb8: i2c1_scl_pb8 {
pinmux = <0x28 2>; /* PB8 复用为 I2C1_SCL */
};
i2c1_scl_pc4: i2c1_scl_pc4 {
pinmux = <0x44 2>; /* PC4 复用为 I2C1_SCL */
};
i2c1_sda_pb9: i2c1_sda_pb9 {
pinmux = <0x29 2>; /* PB9 复用为 I2C1_SDA */
};
i2c1_sda_pc5: i2c1_sda_pc5 {
pinmux = <0x45 2>; /* PC5 复用为 I2C1_SDA */
};
};
/* ★ SPI0/SPI1:芯片有,但默认 disabled,等 Board 决定 */
spi0: spi@40010000 {
compatible = "company,cs32-spi";
reg = <0x40010000 0x1000>;
interrupts = <30>;
status = "disabled";
};
spi1: spi@40011000 {
compatible = "company,cs32-spi";
reg = <0x40011000 0x1000>;
interrupts = <31>;
status = "disabled";
};
/* ★ I2C0/I2C1:芯片有,但默认 disabled,等 Board 决定 */
i2c0: i2c@40018000 {
compatible = "company,cs32-i2c";
reg = <0x40018000 0x1000>;
interrupts = <40>;
status = "disabled";
};
i2c1: i2c@40019000 {
compatible = "company,cs32-i2c";
reg = <0x40019000 0x1000>;
interrupts = <41>;
status = "disabled";
};
};
};
作用:SoC 层只负责「声明能力」——spi0、spi1、i2c0、i2c1 四个外设节点都存在但默认 disabled,同时为每个外设定义了多组引脚复用选项(如 spi0_sck_pa5、spi0_sck_pb13、i2c0_scl_pb6、i2c0_scl_pa9 等)。具体使能哪个外设、选哪组引脚,完全由 Board 层决定。这就是「SoC 描述芯片有什么」的体现。
第二步:Board 层 dts —— 一块板使能 spi0,另一块使能 i2c0
现在有两块板子,芯片都是 CS32A1,但板级外设需求不同:
cs32a1_evb —— 使能 SPI0(外接 Flash 或 LCD):
/* boards/company/cs32/cs32a1_evb/cs32a1_evb.dts —— EVB 板:使能 SPI0 */
#include <company/cs32/cs32a1.dtsi>
/ {
model = "CS32A1 Evaluation Board";
compatible = "company,cs32a1-evb", "company,cs32a1";
chosen {
zephyr,console = &uart0;
zephyr,shell-uart = &uart0;
};
};
/* ★ EVB 板:使能 UART0 作为控制台 */
&uart0 {
status = "okay";
current-speed = <115200>;
pinctrl-0 = <&uart0_tx_pa2 &uart0_rx_pa3>;
pinctrl-names = "default";
};
/* ★ EVB 板:使能 SPI0,SCK→PA5, MISO→PA6, MOSI→PA7 */
&spi0 {
status = "okay";
pinctrl-0 = <&spi0_sck_pa5 &spi0_miso_pa6 &spi0_mosi_pa7>;
pinctrl-names = "default";
};
/* ★ EVB 板:I2C0 保持 disabled,板子上没接 I2C 器件 */
/* &i2c0 不覆盖,保持 SoC 层的 disabled */
作用:cs32a1_evb 通过 &spi0 { status = "okay"; } 使能了 SPI0,并配置了 SCK/MISO/MOSI 三根线的引脚复用。I2C0 没有被覆盖,保持 SoC 层的 disabled,因此不会生成 I2C0 设备实例——这就是「Board 描述板子实际用了什么」。
cs32a1_devkit —— 使能 I2C0(外接传感器或 EEPROM):
/* boards/company/cs32/cs32a1_devkit/cs32a1_devkit.dts —— DevKit 板:使能 I2C0 */
#include <company/cs32/cs32a1.dtsi>
/ {
model = "CS32A1 Development Kit";
compatible = "company,cs32a1-devkit", "company,cs32a1";
chosen {
zephyr,console = &uart0;
zephyr,shell-uart = &uart0;
};
};
/* ★ DevKit 板:使能 UART0 作为控制台(注意引脚不同:TX→PB6, RX→PB7) */
&uart0 {
status = "okay";
current-speed = <115200>;
pinctrl-0 = <&uart0_tx_pb6 &uart0_rx_pb7>;
pinctrl-names = "default";
};
/* ★ DevKit 板:使能 I2C0,SCL→PB6, SDA→PB7 */
&i2c0 {
status = "okay";
pinctrl-0 = <&i2c0_scl_pb6 &i2c0_sda_pb7>;
pinctrl-names = "default";
};
/* ★ DevKit 板:SPI0 保持 disabled,板子上没接 SPI 器件 */
/* &spi0 不覆盖,保持 SoC 层的 disabled */
作用:cs32a1_devkit 恰好相反——它使能了 I2C0(SCL→PB6, SDA→PB7),而 SPI0 保持 disabled。两块板子用的是同一个 SoC 层 dtsi、同一份 SPI/I2C driver,唯一的差异就是 Board 层 dts 里使能了哪个外设。这就是「Board variation 被 Devicetree 吸收」在外设选择层面的体现。
第三步:两块板子的外设使能对比
把 cs32a1_evb 和 cs32a1_devkit 两块板子的外设使能情况放在一起对比,差异一目了然:
| UART0 | status = "okay"(TX→PA2, RX→PA3) | status = "okay"(TX→PB6, RX→PB7) | 都使能,但引脚不同 |
| SPI0 | status = "okay"(SCK→PA5, MISO→PA6, MOSI→PA7) | 保持 disabled | EVB 独有 |
| SPI1 | 保持 disabled | 保持 disabled | 都未使能 |
| I2C0 | 保持 disabled | status = "okay"(SCL→PB6, SDA→PB7) | DevKit 独有 |
| I2C1 | 保持 disabled | 保持 disabled | 都未使能 |
对比要点:
整条链路串起来看:
cs32a1.dtsi(SoC 层)
│ 定义 spi0/spi1/i2c0/i2c1 节点(默认 disabled)+ 所有 pinctrl 复用选项
▼
cs32a1_evb.dts(Board 层)
│ &spi0 { status = "okay" } + pinctrl-0 = <&spi0_sck_pa5 ...>
│ &i2c0 不覆盖 → 保持 disabled
▼
Zephyr 生成 spi0 设备实例 → SPI driver 正常工作(I2C0 无实例)
cs32a1.dtsi(SoC 层)
│ 定义 spi0/spi1/i2c0/i2c1 节点(默认 disabled)+ 所有 pinctrl 复用选项
▼
cs32a1_devkit.dts(Board 层)
│ &i2c0 { status = "okay" } + pinctrl-0 = <&i2c0_scl_pb6 &i2c0_sda_pb7>
│ &spi0 不覆盖 → 保持 disabled
▼
Zephyr 生成 i2c0 设备实例 → I2C driver 正常工作(SPI0 无实例)
一句话总结:SoC 层 dtsi 定义「芯片有哪些外设、支持哪些引脚复用」,Board 层 dts 通过 status = "okay" 决定「这块板子实际使能哪个外设」——cs32a1_evb 使能 spi0、cs32a1_devkit 使能 i2c0,未使能的外设保持 disabled 不产生设备实例。同一颗 SoC、同一份 dtsi、同一套 driver,通过 Board 层 dts 就能让不同板子使用不同的外设组合——这就是 Multi-Board 在外设选择层面的完整落地。
一句话总结:Package Variant 是「引脚可用性」的差异,不是「芯片能力」的差异。SoC 层 dtsi 定义所有复用选项,Kconfig 通过 SOC_CS32A1_HAS_PB13 这样的 symbol 表达「这个封装有没有引出某个引脚」,dtsi 用 #if defined() 条件包含裁掉不可用的节点,Board 层再通过 pinctrl-0 选择具体引脚。同一个 silicon、同一个 dtsi、同一个 driver,通过 Kconfig + 条件包含就能优雅地服务多种封装——这就是 Package Variant 进入 Multi-Board 模型后的完整落地。 一句话总结:Package Variant 是「引脚可用性」的差异,不是「芯片能力」的差异。SoC 层 dtsi 定义所有复用选项,Kconfig 通过 SOC_CS32A1_HAS_PB13 这样的 symbol 表达「这个封装有没有引出某个引脚」,dtsi 用 #if defined() 条件包含裁掉不可用的节点,Board 层再通过 pinctrl-0 选择具体引脚。同一个 silicon、同一个 dtsi、同一个 driver,通过 Kconfig + 条件包含就能优雅地服务多种封装——这就是 Package Variant 进入 Multi-Board 模型后的完整落地。
总结
回顾全文,我们把 Multi-Board / Multi-Chip 的核心结论浓缩成下面几条要点:
SoC 描述芯片有什么,Board 描述板子实际用了什么——这是贯穿全文的第一原则。SoC 层声明芯片具备的外设与引脚复用能力,Board 层决定这块板子真正使能了哪些外设、接在哪些引脚上。
Family → Variant 是 SoC 层的组织方式——同一个 SoC 家族共享一份 common/ HAL 代码,每个 Variant(如 CS32A1、CS32A2)只保留自己的 soc.c 与 Kconfig,避免复制多套完整 BSP。
共享能力共享代码,芯片差异单独处理——UART、SPI、I2C 等通用外设驱动放在 common/ 里复用;不同芯片的寄存器差异通过 Devicetree 的 compatible 与 SoC-specific data 表达,而不是堆砌 #ifdef。
Devicetree 吸收 Board variation——LED 接 GPIOA.5 还是 GPIOB.3、UART0 走 PA2/PA3 还是 PB6/PB7,都由 Board 层 dts 描述。Application 只认 led0、uart0 这样的别名,代码里没有任何硬编码引脚号。
Board 通过 board.yaml 的 soc 字段选择 SoC——soc: cs32a1 是 Board 与 SoC 之间的「契约」,Zephyr 据此找到对应的 SoC 目录并触发后续 Kconfig 默认选择。换一颗 SoC,Board 只需改这一行声明。
Kconfig 使能 SoC,Devicetree 使能外设——Kconfig 回答「我支持什么」(CONFIG_SOC_CS32A1=y、CONFIG_GPIO=y),Devicetree 回答「硬件实际上怎么连接」(&uart0 { status = "okay"; })。两者分工明确、各司其职。
Pinmux 与 Package Variant 通过 Kconfig / dtsi 条件包含区分——SoC 层 dtsi 定义所有引脚复用选项,Kconfig 用 SOC_CS32A1_HAS_PB13 这样的 symbol 表达「这个封装有没有引出某个引脚」,dtsi 用 #if defined() 裁掉不可用的节点,Board 层再通过 pinctrl-0 选择具体引脚。
硬件差异优先通过 Devicetree 表达,而不是修改 driver 代码——无论是不同 Board 的引脚映射、不同 SoC 的外设数量,还是不同封装的引脚可用性,都能在 Devicetree / Kconfig 层面优雅解决,driver 保持零改动。
一句话收束全文:Board 是产品、SoC 是平台、Driver 是能力、Devicetree 是连接三者的描述语言——理解了这句话,你就掌握了公司级 Zephyr BSP 的设计精髓。
一句话总结:cs32a1_evb 和 cs32a2_evb 走的是同一条 SoC 选择链路——board.yaml 声明 → Kconfig 生成 symbol → dtsi 描述能力 → dts 板级使能。差异只在「内容」:soc 字段不同、生成的 CONFIG symbol 不同、dtsi 里外设数量不同、Board 使能的外设不同。链路结构完全一致,这就是 Family → Variant 让 Multi-Chip 变得可维护的核心。
整条链路串起来看:
cs32a1_evb.yaml
│ soc: cs32a1
▼
Kconfig → CONFIG_SOC_CS32A1=y
│ 使能 SoC 及共享 HAL
▼
cs32a1.dtsi → uart0 存在但 disabled(芯片能力)
│
▼
cs32a1_evb.dts → &uart0 { status = "okay" }(板级使能)
│
▼
Zephyr 生成 uart0 设备实例 → Application 直接使用
七点六、实战示例:cs32a1_evb 的 GPIO LED 点灯
上一节我们走通了「Board 如何选择 SoC」的链路。这一节我们更进一步,在 cs32a1_evb 上点亮一颗 LED,把 board.yaml 声明 → Kconfig 配置 → Devicetree 节点 → Application 代码 的完整链路串起来。整个过程分四步。
第一步:board.yaml —— 声明板卡与 SoC
# boards/company/cs32/cs32a1_evb/cs32a1_evb.yaml
identifier: cs32a1_evb # Board 的唯一标识,构建时用 -b cs32a1_evb 指定
name: CS32A1 Evaluation Board # 人类可读的板卡名称
type: board # 类型固定为 board
arch: arm # 架构:ARM Cortex-M4
toolchain:
– zephyr # 使用 Zephyr SDK 工具链
ram: 64 # 板载 RAM 大小(KB)
flash: 256 # 板载 Flash 大小(KB)
soc: cs32a1 # ★ 声明本 Board 使用的 SoC 是 cs32a1
作用:soc: cs32a1 让 Zephyr 找到 soc/company/cs32/cs32a1/ 目录,并触发后续 Kconfig 的默认选择。这一步与上一节完全一致,是整条链路的地基。
第二步:Kconfig —— 使能 GPIO 驱动
SoC 的 Kconfig 里已经声明了 GPIO 能力:
# soc/company/cs32/cs32a1/Kconfig
config SOC_CS32A1
bool "CS32A1 SoC"
select CPU_CORTEX_M4 # 自动选中 Cortex-M4 内核
select HAS_FLASH_LOAD_OFFSET # 声明支持 Flash 加载偏移
select GPIO # ★ 使能 GPIO 子系统
help
Company CS32A1 SoC, Cortex-M4 based.
Application 的 prj.conf 里再显式打开 GPIO:
# applications/led_blink/prj.conf
CONFIG_GPIO=y # ★ 使能 GPIO 驱动
CONFIG_LOG=y # 开启日志,便于观察运行结果
构建时,Zephyr 根据 board.yaml 的 soc: cs32a1 自动生成默认配置:
# build/zephyr/.config 中自动生成的关键项
CONFIG_SOC_CS32A1=y # ★ 由 board.yaml 的 soc 字段自动推导
CONFIG_GPIO=y # ★ 由 select GPIO 自动带上
CONFIG_CPU_CORTEX_M4=y # 由 select CPU_CORTEX_M4 自动带上
作用:CONFIG_GPIO=y 让 Zephyr 把 GPIO 驱动编进来。注意:Kconfig 只负责「我支持什么能力」,不负责「LED 接在哪个引脚」——那是 Devicetree 的事。
第三步:Devicetree —— 描述 LED 节点
SoC 的 .dtsi 里,GPIOA 默认是 disabled(芯片有,但板子不一定用):
/* dts/arm/company/cs32/cs32a1.dtsi —— SoC 层:描述芯片有什么 */
/ {
soc {
gpioa: gpio@40020000 {
compatible = "company,cs32-gpio";
reg = <0x40020000 0x400>;
interrupts = <20>;
gpio-controller;
#gpio-cells = <2>;
status = "disabled"; /* 默认关闭,等 Board 决定 */
};
};
};
Board 的 .dts 里,打开 GPIOA 并定义 LED 节点:
/* boards/company/cs32/cs32a1_evb/cs32a1_evb.dts —— Board 层:板子实际用了什么 */
#include <company/cs32/cs32a1.dtsi> /* 先引入 SoC 的完整描述 */
/ {
model = "CS32A1 Evaluation Board";
compatible = "company,cs32a1-evb", "company,cs32a1";
aliases {
led0 = &user_led; /* ★ 给 LED 起个别名,Application 用 DT_ALIAS 引用 */
};
leds {
compatible = "gpio-leds"; /* 标准 LED 节点 compatible */
user_led: led_0 {
gpios = <&gpioa 5 GPIO_ACTIVE_HIGH>; /* ★ LED 接在 GPIOA.5,高电平点亮 */
label = "User LED";
};
};
chosen {
zephyr,console = &uart0; /* 控制台输出到 UART0 */
zephyr,shell-uart = &uart0;
};
};
/* ★ 关键:Board 覆盖 SoC 的默认配置,使能 GPIOA */
&gpioa {
status = "okay"; /* 打开 GPIOA */
};
作用:gpios = <&gpioa 5 GPIO_ACTIVE_HIGH> 是 Board 与 LED 之间的「契约」——它告诉 Zephyr「这颗 LED 接在 GPIOA 的第 5 脚,高电平点亮」。Application 完全不需要知道这个引脚号,它只认 led0 这个别名。
第四步:Application —— 点灯代码
/* applications/led_blink/src/main.c */
#include <zephyr/kernel.h>
#include <zephyr/device.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/logging/log.h>
LOG_MODULE_REGISTER(main);
/* ★ 通过 DT_ALIAS 拿到 led0 对应的 GPIO 规格,不关心具体引脚 */
static const struct gpio_dt_spec led = GPIO_DT_SPEC_GET(DT_ALIAS(led0), gpios);
void main(void)
{
int ret;
/* 检查 LED 对应的 GPIO 控制器是否就绪 */
if (!gpio_is_ready_dt(&led)) {
LOG_ERR("GPIO controller not ready");
return;
}
/* 把 LED 引脚配置为输出,初始电平为低(灭) */
ret = gpio_pin_configure_dt(&led, GPIO_OUTPUT_INACTIVE);
if (ret < 0) {
LOG_ERR("Failed to configure LED pin: %d", ret);
return;
}
LOG_INF("LED blink started");
/* 主循环:每 500ms 翻转一次 LED 状态 */
while (1) {
gpio_pin_toggle_dt(&led); /* 翻转电平:亮 → 灭 → 亮 → 灭 */
k_msleep(500);
}
}
作用:GPIO_DT_SPEC_GET(DT_ALIAS(led0), gpios) 是 Application 与 Devicetree 之间的「契约」——它从 Devicetree 里读出 LED 的引脚号和极性,生成一个 gpio_dt_spec 结构体。之后所有操作都通过这个结构体完成,代码里没有任何硬编码的引脚号。
构建命令
# 在 Zephyr 根目录下,构建 led_blink 示例
west build -b cs32a1_evb applications/led_blink
# 烧录到板卡
west flash
运行结果
烧录后,板载 LED 以 500ms 为周期闪烁。串口控制台(UART0,115200)输出:
*** Booting Zephyr OS build zephyr-v3.7.0 ***
[00:00:00.000,000] <inf> main: LED blink started
整条链路串起来看:
cs32a1_evb.yaml
│ soc: cs32a1
▼
Kconfig → CONFIG_SOC_CS32A1=y + CONFIG_GPIO=y
│ 使能 SoC 及 GPIO 驱动
▼
cs32a1.dtsi → gpioa 存在但 disabled(芯片能力)
│
▼
cs32a1_evb.dts → &gpioa { status = "okay" } + led0 节点(板级使能)
│
▼
Application → GPIO_DT_SPEC_GET(DT_ALIAS(led0), gpios) 点灯
一句话总结:board.yaml 声明「用哪个 SoC」,Kconfig 使能「SoC 和 GPIO 驱动」,Devicetree 描述「LED 接在哪个引脚」,Application 只认 led0 别名。四层各司其职、层层解耦——这就是 Zephyr 里一个外设从「芯片能力」到「板级连接」再到「应用使用」的完整生命周期。 一句话总结:board.yaml 告诉 Zephyr「用哪个 SoC」,Kconfig 据此「使能 SoC 及其能力」,Devicetree 再「决定板子上哪些外设真正打开」。三者各司其职,这就是 Multi-Board 能同时支持多个 Board 的底层机制。 而:
cs32a2_evb
│
▼
CS32A2
│
▼
Cortex-M4
八、这时候 Kconfig 和 Devicetree 分工就非常清楚
这是前面几篇知识真正开始汇合的地方。 Kconfig
负责: 「我支持什么?」
例如:
CONFIG_SOC_CS32A1=y
CONFIG_SOC_CS32A2=n
以及:
CONFIG_UART=y
CONFIG_SPI=y
CONFIG_I2C=y
Devicetree 负责: 「硬件实际上怎么连接?」 例如:
&uart0 {
status = "okay";
};
以及:
&gpioa {
status = "okay";
};
所以可以把它记成:
Kconfig
↓
Capability / Feature selection
Devicetree
↓
Hardware description
九、一个很容易犯的错误
假设 CS32A1 有:
UART0
UART1
UART2
UART3
Board A 只连接:
UART0 → USB-UART
不要写:
# define DEBUG_UART UART0
然后让整个 UART driver 根据这个宏工作。 应该是:
&uart0 {
status = "okay";
};
&uart1 {
status = "disabled";
};
&uart2 {
status = "disabled";
};
&uart3 {
status = "disabled";
};
于是 Zephyr 的硬件描述就是:
SoC:
UART0
UART1
UART2
UART3
Board:
UART0 enabled
UART1 disabled
UART2 disabled
UART3 disabled
十、再进一步:不同 SoC 的外设数量不同
例如:
CS32A1
UART0
UART1
UART2
UART3
CS32A2
UART0
UART1
UART2
UART3
UART4
UART5
那么 Devicetree 可以分别描述:
CS32A1.dtsi
├── uart0
├── uart1
├── uart2
└── uart3
而:
CS32A2.dtsi
├── uart0
├── uart1
├── uart2
├── uart3
├── uart4
└── uart5
Board 再进行 override。 例如:
&uart0 {
status = "okay";
};
这样形成:
SoC DTSI
│
┌───────┴───────┐
│ │
CS32A1 CS32A2
│ │
▼ ▼
Board DTS Board DTS
│ │
▼ ▼
EVB-A1 EVB-A2
十一、这其实就是 Devicetree 的继承/覆盖模型
可以把它理解成:
soc.dtsi
│
│ 描述芯片
▼
board.dts
│
│ 修改板级配置
▼
最终 devicetree
│
▼
devicetree_generated.h
例如 SoC:
uart0: uart@40000000 {
compatible = "company,cs32-uart";
reg = <0x40000000 0x1000>;
interrupts = <10>;
status = "disabled";
};
Board:
&uart0 {
status = "okay";
current–speed = <115200>;
};
最终:
uart0
├── compatible = company,cs32–uart
├── reg = 0x40000000
├── interrupts = 10
├── status = okay
└── current–speed = 115200
十二、Multi-Chip 最重要的设计原则
假设:
CS32A1
CS32A2
CS32B1
它们都有 UART。 不要让 UART driver 变成:
#ifdef CS32A1
...
#elif defined(CS32A2)
...
#elif defined(CS32B1)
...
#endif
然后里面几千行寄存器差异。
应该尽量让:
Devicetree
│
▼
compatible
│
▼
common driver
│
▼
register definitions / SoC-specific data
也就是说: 硬件差异优先通过 Devicetree 和 SoC-specific data 表达,而不是疯狂增加 C preprocessor。
十三、什么时候才应该真正分 Driver?
这是公司 BSP 很实际的问题。
假设:
CS32A1 UART
寄存器:
CTRL
STATUS
BAUD
TXDATA
RXDATA
而:
CS32B1 UART
完全不同:
CR
SR
BRR
TDR
RDR
这时候不要为了「代码复用」强行塞进一个 driver。
可以:
drivers/serial/
├── uart_cs32a.c
└── uart_cs32b.c
Devicetree:
compatible = "company,cs32a-uart"
和:
compatible = "company,cs32b-uart"
分别绑定。
但上层 Zephyr API 仍然保持:
uart_poll_out(dev, c);
uart_irq_rx_enable(dev);
于是:
Application
│
▼
Zephyr UART API
│
┌────┴─────┐
▼ ▼
CS32A CS32B
driver driver
这才是真正的 abstraction。
十四、Multi-Board 的另一个关键问题:Pinmux
这是实际 BSP 中最容易出问题的地方之一。
同一个 UART:
UART0 TX
UART0 RX
在不同 Board 上可能:
Board A:
TX → PA2
RX → PA3
而:
Board B:
TX → PB6
RX → PB7
SoC driver 不应该知道:
PA2
PA3
PB6
PB7
应该:
SoC
│
├── UART0
│
└── Pinmux capability
Board:
Board A
└── uart0
├── tx → PA2
└── rx → PA3
Board B:
Board B
└── uart0
├── tx → PB6
└── rx → PB7
于是:
UART Driver
│
│
UART0 device
│
pinctrl
│
┌────────┴────────┐
▼ ▼
Board A Board B
PA2/PA3 PB6/PB7
十四点五、实战示例:cs32a1_evb 的 UART0 pinctrl 配置
上一节我们讲了 Pinmux 的概念:同一个 UART,在不同 Board 上可以映射到不同的引脚。这一节我们以 cs32a1_evb 的 UART0 为例,把 pinctrl 的完整链路走一遍:SoC 层 dtsi 定义 pinctrl 节点 → Board 层 dts 配置引脚复用 → pinctrl-0 属性完成映射。整个过程分三步。
第一步:SoC 层 dtsi —— 定义 pinctrl 节点与引脚复用选项
SoC 的 .dtsi 里,需要先声明 pinctrl 控制器,并定义该 SoC 支持的所有引脚复用组合。注意:这里只描述「芯片支持哪些复用」,不决定「板子用哪一组」:
/* dts/arm/company/cs32/cs32a1.dtsi —— SoC 层:描述芯片有什么 */
/ {
soc {
/* pinctrl 控制器:负责管理所有引脚的复用功能 */
pinctrl: pin-controller@40028000 {
compatible = "company,cs32-pinctrl";
reg = <0x40028000 0x400>;
status = "okay";
/* ★ 引脚复用选项:UART0 的 TX 可以映射到 PA2 或 PB6 */
uart0_tx_pa2: uart0_tx_pa2 {
pinmux = <0x02 0>; /* PA2 复用为 UART0_TX */
};
uart0_tx_pb6: uart0_tx_pb6 {
pinmux = <0x16 0>; /* PB6 复用为 UART0_TX */
};
/* ★ 引脚复用选项:UART0 的 RX 可以映射到 PA3 或 PB7 */
uart0_rx_pa3: uart0_rx_pa3 {
pinmux = <0x03 0>; /* PA3 复用为 UART0_RX */
};
uart0_rx_pb7: uart0_rx_pb7 {
pinmux = <0x17 0>; /* PB7 复用为 UART0_RX */
};
};
uart0: uart@40000000 {
compatible = "company,cs32-uart";
reg = <0x40000000 0x1000>;
interrupts = <10>;
status = "disabled"; /* 默认关闭,等 Board 决定 */
};
};
};
作用:SoC 层只负责「声明能力」——uart0_tx_pa2、uart0_tx_pb6 这些节点表示「UART0 的 TX 引脚可以复用到 PA2 或 PB6」。具体选哪一组,由 Board 层决定。这就是「SoC 描述芯片有什么」的体现。
第二步:Board 层 dts —— 配置引脚复用并绑定到 UART0
Board 的 .dts 里,通过 pinctrl-0 属性把 UART0 的 TX/RX 映射到具体引脚:
/* boards/company/cs32/cs32a1_evb/cs32a1_evb.dts —— Board 层:板子实际用了什么 */
#include <company/cs32/cs32a1.dtsi> /* 先引入 SoC 的完整描述 */
/ {
model = "CS32A1 Evaluation Board";
compatible = "company,cs32a1-evb", "company,cs32a1";
chosen {
zephyr,console = &uart0; /* 控制台输出到 UART0 */
zephyr,shell-uart = &uart0;
};
};
/* ★ 关键:Board 覆盖 SoC 的默认配置,使能 UART0 并配置引脚复用 */
&uart0 {
status = "okay"; /* 打开 UART0 */
current-speed = <115200>; /* 波特率 115200 */
/* ★ pinctrl-0:把 UART0 的 TX 映射到 PA2,RX 映射到 PA3 */
pinctrl-0 = <&uart0_tx_pa2 &uart0_rx_pa3>;
pinctrl-names = "default"; /* 使用默认引脚配置状态 */
};
作用:pinctrl-0 = <&uart0_tx_pa2 &uart0_rx_pa3> 是 Board 与引脚之间的「契约」——它告诉 Zephyr「这块板子上,UART0 的 TX 接 PA2、RX 接 PA3」。Zephyr 的 pinctrl 驱动会在 UART0 设备初始化时,自动把这些引脚配置为复用功能,UART driver 本身完全不需要关心引脚号。
第四步:两块板子的 pinctrl-0 写法对比
把 cs32a1_evb 和 cs32a1_devkit 两块板子的 UART0 配置放在一起对比,差异一目了然:
┌─────────────────────────────┬──────────────────────────────────────────────┐
│ Board │ &uart0 的 pinctrl-0 │
├─────────────────────────────┼──────────────────────────────────────────────┤
│ cs32a1_evb │ pinctrl-0 = <&uart0_tx_pa2 &uart0_rx_pa3> │
│ │ TX → PA2, RX → PA3 │
├─────────────────────────────┼──────────────────────────────────────────────┤
│ cs32a1_devkit │ pinctrl-0 = <&uart0_tx_pb6 &uart0_rx_pb7> │
│ │ TX → PB6, RX → PB7 │
└─────────────────────────────┴──────────────────────────────────────────────┘
cs32a1_evb 的完整 UART0 配置:
/* boards/company/cs32/cs32a1_evb/cs32a1_evb.dts —— EVB 板:UART0 走 PA2/PA3 */
#include <company/cs32/cs32a1.dtsi>
/ {
model = "CS32A1 Evaluation Board";
compatible = "company,cs32a1-evb", "company,cs32a1";
chosen {
zephyr,console = &uart0;
zephyr,shell-uart = &uart0;
};
};
&uart0 {
status = "okay";
current-speed = <115200>;
/* ★ EVB 板:UART0 的 TX 复用为 PA2,RX 复用为 PA3 */
pinctrl-0 = <&uart0_tx_pa2 &uart0_rx_pa3>;
pinctrl-names = "default";
};
cs32a1_devkit 的完整 UART0 配置:
/* boards/company/cs32/cs32a1_devkit/cs32a1_devkit.dts —— DevKit 板:UART0 走 PB6/PB7 */
#include <company/cs32/cs32a1.dtsi>
/ {
model = "CS32A1 Development Kit";
compatible = "company,cs32a1-devkit", "company,cs32a1";
chosen {
zephyr,console = &uart0;
zephyr,shell-uart = &uart0;
};
};
&uart0 {
status = "okay";
current-speed = <115200>;
/* ★ DevKit 板:UART0 的 TX 复用为 PB6,RX 复用为 PB7 */
pinctrl-0 = <&uart0_tx_pb6 &uart0_rx_pb7>;
pinctrl-names = "default";
};
对比要点:
一句话总结:cs32a1_evb 和 cs32a1_devkit 共用同一个 SoC 层 dtsi 和同一个 UART driver,唯一的差异就是 pinctrl-0 里引用的引脚复用节点不同——EVB 用 &uart0_tx_pa2 &uart0_rx_pa3,DevKit 用 &uart0_tx_pb6 &uart0_rx_pb7。这就是「同一 UART,不同 Board,不同引脚」在 Devicetree 层面的完整表达。 第三步:不同 Board 覆盖 pinctrl —— 同一 UART,不同引脚
现在假设另一块板子 cs32a1_devkit,芯片同样是 CS32A1,但 UART0 的引脚接法不同:
/* boards/company/cs32/cs32a1_devkit/cs32a1_devkit.dts —— 另一块 Board */
#include <company/cs32/cs32a1.dtsi>
/ {
model = "CS32A1 Development Kit";
compatible = "company,cs32a1-devkit", "company,cs32a1";
chosen {
zephyr,console = &uart0;
zephyr,shell-uart = &uart0;
};
};
/* ★ 同一 UART0,但引脚映射不同:TX → PB6, RX → PB7 */
&uart0 {
status = "okay";
current-speed = <115200>;
/* ★ 只需换一组 pinctrl 引用,UART driver 代码零改动 */
pinctrl-0 = <&uart0_tx_pb6 &uart0_rx_pb7>;
pinctrl-names = "default";
};
作用:两块板子用的是同一个 UART0 设备、同一个 UART driver,唯一的区别就是 pinctrl-0 引用的引脚复用节点不同。这就是「Board variation 被 Devicetree 吸收」在引脚层面的体现——硬件差异通过 Devicetree 表达,而不是修改 driver 代码。
整条链路串起来看:
cs32a1.dtsi(SoC 层)
│ 定义 pinctrl 控制器 + 所有复用选项
│ uart0_tx_pa2 / uart0_tx_pb6 / uart0_rx_pa3 / uart0_rx_pb7
▼
cs32a1_evb.dts(Board 层)
│ &uart0 { pinctrl-0 = <&uart0_tx_pa2 &uart0_rx_pa3> }
▼
Zephyr pinctrl 驱动
│ 初始化时自动把 PA2/PA3 配置为 UART0 复用功能
▼
UART0 设备实例 → UART driver 正常工作(不关心引脚号)
一句话总结:SoC 层 dtsi 定义「芯片支持哪些引脚复用」,Board 层 dts 通过 pinctrl-0 决定「这块板子用哪一组」,不同 Board 只需覆盖 pinctrl-0 就能让同一个 UART 映射到不同引脚——UART driver 代码完全不用改。这就是 pinctrl 让 Multi-Board 变得优雅的核心机制。 这就是为什么现代 Zephyr BSP 里:
pinctrl
非常重要。
十五、Package Variant 也可以进入这个模型
例如:
CS32A1
有:
CS32A1-48
CS32A1-64
CS32A1-100
芯片内部可能都是同一个 silicon。 区别只是:
pin count
package
available pins
这时候通常不应该复制:
CS32A1-48
CS32A1-64
CS32A1-100
三套完整 SoC。 更合理的是:
CS32A1
│
├── common SoC
│
└── package/pin configuration
例如:
SoC capability
│
▼
Package constraints
│
▼
Board pinctrl
十六、最终你会得到这样的架构
Zephyr Application
│
▼
Zephyr Driver API
│
┌───────────────┼───────────────┐
│ │ │
GPIO UART SPI
│ │ │
└───────────────┼───────────────┘
│
Devicetree
│
┌─────────────┴─────────────┐
│ │
Board DTS SoC DTSI
│ │
┌───────┼────────┐ │
│ │ │ │
LED Button UART UART0..N
│ │ │ │
└───────┴────────┴──────────────────┘
│
SoC HAL
│
┌──────────┴──────────┐
│ │
CS32A1 CS32A2
│ │
└──────────┬──────────┘
│
ARM Cortex-M
十七、一个非常实用的目录设计
如果你以后真的负责公司 BSP,我会建议你首先朝这个方向设计:
zephyr/
│
├── boards/
│ └── company/
│ └── cs32/
│ ├── cs32a1_evb/
│ ├── cs32a1_devkit/
│ ├── cs32a2_evb/
│ └── cs32a2_devkit/
│
├── soc/
│ └── company/
│ └── cs32/
│ ├── cs32a1/
│ │ ├── soc.c
│ │ ├── Kconfig
│ │ └── CMakeLists.txt
│ │
│ ├── cs32a2/
│ │ ├── soc.c
│ │ ├── Kconfig
│ │ └── CMakeLists.txt
│ │
│ └── common/
│
├── dts/
│ ├── arm/
│ │ └── company/
│ │ └── cs32/
│ │ ├── cs32a1.dtsi
│ │ └── cs32a2.dtsi
│ │
│ └── bindings/
│ └── company/
│
└── drivers/
├── serial/
├── gpio/
├── spi/
└── clock_control/
这里已经出现了一个非常重要的概念:
Board 是产品。SoC 是平台。Driver 是能力。Devicetree 是连接三者的描述语言。
十八、你现在应该形成的「脑内模型」
以后看到:
board_x
不要马上想: “这是一个完整 BSP。” 而应该问:
这个 Board 用什么 SoC?
│
▼
这个 SoC 有什么硬件?
│
▼
哪些硬件是 SoC 固有的?
│
▼
哪些硬件是 Board 外部连接的?
│
▼
哪些东西由 Devicetree 描述?
│
▼
哪些东西由 Kconfig 控制?
│
▼
哪些东西由 Driver 实现?
这条思路非常重要。
十九、最终把整个 20~38 篇串起来
现在你已经可以把前面的课程压缩成一张图:
Application
│
▼
Zephyr API
│
▼
Driver
│
┌────────┴────────┐
│ │
Kconfig Devicetree
│ │
│ ┌──────┴──────┐
│ │ │
│ Board SoC
│ │ │
│ │ │
└──────────┴──────┬──────┘
│
BSP Layer
│
Company HAL / SDK
│
┌────────┴────────┐
│ │
SoC-A SoC-B
│ │
┌──────┴─────┐ ┌────┴─────┐
│ │ │ │
Board A1 A2 Board B1 B2
所以真正成熟的 BSP 并不是:
一个芯片 + 一个 Board
而是:
Company Platform
│
┌─────────┴─────────┐
│ │
SoC Family Common HAL
│
┌─────┴─────┐
│ │
SoC-A SoC-B
│ │
┌───┴───┐ ┌───┴───┐
│ │ │ │
Board1 Board2 Board3 Board4
这才是公司级 Zephyr BSP 的基本形态。




