欢迎光临
我们一直在努力

轻松学习Zephyr BSP: 38-多板多芯片支持

摘要:本文围绕 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

它们非常接近:

FeatureCS32A1CS32A2
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:cs32a1_64evb 和 cs32a1_48evb 的 board.yaml 里 soc: cs32a1 完全一样,都指向 soc/company/cs32/cs32a1/ 和 dts/arm/company/cs32/cs32a1.dtsi。
  • 差异只在封装:64-pin 封装有 PB13,48-pin 没有。这个差异通过 Kconfig.defconfig 选择 SOC_CS32A1_PACKAGE_64 或 SOC_CS32A1_PACKAGE_48 来表达,dtsi 里用 #if defined(CONFIG_SOC_CS32A1_HAS_PB13) 裁掉不可用的引脚节点。
  • Board 层只做两件事:在 board.yaml 声明 soc: cs32a1,在 Kconfig.defconfig 选择封装,在 .dts 里使能外设并选择具体引脚。
  • 这就是「同一 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 选择链路对比:

    环节cs32a1_evbcs32a2_evb
    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 外设能力对比:

    外设CS32A1CS32A2差异类型
    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 64pin 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 48pin 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 正常工作

    两种封装的对比:

    环节cs32a1_64evb(64-pin)cs32a1_48evb(48-pin)
    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 两块板子的外设使能情况放在一起对比,差异一目了然:

    外设cs32a1_evbcs32a1_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 都未使能

    对比要点:

  • SoC 层完全一致:两块板子都 #include <company/cs32/cs32a1.dtsi>,SoC 层定义的 spi0、spi1、i2c0、i2c1 四个外设节点及所有 pinctrl 复用选项被两块板子共用。
  • Board 层使能的外设不同:cs32a1_evb 使能 spi0(外接 Flash/LCD),cs32a1_devkit 使能 i2c0(外接传感器/EEPROM)。未使能的外设保持 SoC 层的 disabled,不产生设备实例。
  • SPI/I2C driver 零改动:无论使能 SPI0 还是 I2C0,driver 拿到的都是同一个 spi0 / i2c0 设备,引脚复用由 pinctrl 驱动在初始化时自动完成。
  • 整条链路串起来看:

    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";
    currentspeed = <115200>;
    };

    最终:

    uart0
    ├── compatible = company,cs32uart
    ├── reg = 0x40000000
    ├── interrupts = 10
    ├── status = okay
    └── currentspeed = 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";
    };

    对比要点:

  • SoC 层完全一致:两块板子都 #include <company/cs32/cs32a1.dtsi>,SoC 层定义的 uart0_tx_pa2、uart0_tx_pb6、uart0_rx_pa3、uart0_rx_pb7 四个复用选项被两块板子共用。
  • Board 层只差一行:两块板子的 &uart0 节点里,status、current-speed、pinctrl-names 完全一样,唯一区别就是 pinctrl-0 引用的节点不同。
  • UART driver 零改动:无论引脚映射到 PA2/PA3 还是 PB6/PB7,UART driver 拿到的都是同一个 uart0 设备,引脚复用由 pinctrl 驱动在初始化时自动完成。
  • 一句话总结: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 的基本形态。

    赞(0)
    未经允许不得转载:171主机测评 » 轻松学习Zephyr BSP: 38-多板多芯片支持
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址