摘要:本文是 Zephyr BSP 系列(第 20~40 篇)的收束篇,系统梳理一个公司 SoC 从芯片定义到 BSP 可交付的完整生命周期。全文按 18 个阶段展开:从 SoC 规格、Company HAL、Zephyr SoC Port、Startup、中断控制器、时钟/复位,到 Devicetree、Binding、驱动、Kconfig、CMake、Linker、Board、Flash/Debug、Validation、SoC Family、Release 直至 CI 落地。核心观点是:BSP 不是"一个 boards/xxx/ 目录",而是一条从 Silicon 到 Application 的软件供应链。文章最后给出公司 BSP 需要维护的"四条线"(硬件线、软件抽象线、构建线、验证线),帮助读者建立从"学 Zephyr"到"交付可维护 BSP"的工程视角。
公司 BSP 的完整生命周期
到第 39 篇,我们已经把 SoC Family Architecture 讲清楚了。
现在进入一个非常重要的收束篇:
一个公司 SoC,从"芯片刚出来"到"Zephyr BSP 真正可交付",整个生命周期到底是什么?
这篇不再只讲某一个文件,而是把前面 20~39 篇全部串起来。
一、先建立一个总模型
一个真正的公司 BSP,不是:
写几个 driver
↓
能跑 blinky
↓
结束
而应该是:
SoC Silicon
↓
Company HAL
↓
Zephyr SoC Port
↓
Devicetree / Binding
↓
Kconfig
↓
Drivers
↓
Board
↓
Build System
↓
Linker / Memory
↓
Flash / Debug / Runner
↓
BSP Validation
↓
Release / Maintenance
再进一步:
┌──────────────────────┐
│ SoC Silicon │
└──────────┬───────────┘
↓
┌──────────────────────┐
│ Company HAL │
│ Clock / IRQ / GPIO │
│ UART / SPI / I2C... │
└──────────┬───────────┘
↓
┌──────────────────────┐
│ Zephyr SoC Port │
│ startup / soc.c │
│ CMake / Kconfig │
└──────────┬───────────┘
↓
┌──────────────────────┐
│ Devicetree + DT │
│ hardware description │
└──────────┬───────────┘
↓
┌──────────────────────┐
│ Zephyr Drivers │
│ UART GPIO SPI I2C... │
└──────────┬───────────┘
↓
┌──────────────────────┐
│ Board │
│ pins / LEDs / buttons│
└──────────┬───────────┘
↓
┌──────────────────────┐
│ Build / Link / Flash │
└──────────┬───────────┘
↓
┌──────────────────────┐
│ BSP Validation │
└──────────────────────┘
这就是公司 BSP 的完整生命周期。
二、第一阶段:SoC 还没出来
这是很多 BSP 工程师容易忽略的一个阶段。
芯片甚至还没有回来。
但是 BSP 工作已经可以开始。
例如公司决定做:
Company SoC X1
├── Cortex-M4
├── 512 KB Flash
├── 128 KB SRAM
├── UART × 3
├── SPI × 2
├── I2C × 2
├── GPIO
├── Timer
├── DMA
└── Interrupt Controller
此时首先需要建立:
SoC specification
↓
memory map
↓
interrupt map
↓
peripheral map
↓
clock tree
↓
reset tree
↓
pinmux
也就是说:
BSP 的第一份「代码」往往不是 C 代码,而是硬件规格。
三、第二阶段:Company HAL 出现
芯片团队通常会先提供自己的 HAL。
例如:
company-hal/
├── include/
│ ├── company_uart.h
│ ├── company_gpio.h
│ ├── company_clock.h
│ └── company_irq.h
│
├── src/
│ ├── company_uart.c
│ ├── company_gpio.c
│ ├── company_clock.c
│ └── company_irq.c
│
└── startup/
└── startup_company.S
HAL 负责:
寄存器
↓
Company API
例如:
company_uart_init();
company_uart_write();
company_uart_read();
这层通常不应该知道:
struct device
DEVICE_DT_DEFINE()
DT_INST_FOREACH_STATUS_OKAY()
因为这些是 Zephyr 的概念。
所以我们之前第 37 篇讲的边界非常重要:
┌─────────────────────────┐
│ Zephyr │
│ │
│ struct device │
│ driver API │
│ devicetree │
│ Kconfig │
└────────────┬────────────┘
│
│ adapter
↓
┌─────────────────────────┐
│ Company HAL │
│ │
│ company_uart_xxx() │
│ company_clock_xxx() │
│ company_gpio_xxx() │
└────────────┬────────────┘
│
↓
┌─────────────────────────┐
│ Registers │
└─────────────────────────┘
四、第三阶段:建立 Zephyr SoC Port
接下来正式进入 Zephyr。
目录可能开始变成:
soc/
└── company/
└── cx1/
├── CMakeLists.txt
├── Kconfig
├── Kconfig.soc
├── soc.c
├── soc.h
└── ...
这时候还不是 Board。
而是:
告诉 Zephyr:「这个 CPU + SoC 是什么。」
例如:
Company CX1
↓
Cortex-M4
↓
clock
↓
interrupt controller
↓
memory
↓
SoC peripherals
这里对应我们之前:
21 SoC Port Skeleton
下面是一个完整的 `soc.c` 示例,它把 SoC 初始化、时钟配置和中断控制器初始化串在一起:
```c
/*
* soc.c – Company CX1 SoC 初始化
*
* 该文件是 Zephyr SoC Port 的核心,负责在 Zephyr 内核启动前
* 完成最基本的硬件初始化,让 CPU 具备运行内核的条件。
*/
#include <zephyr/kernel.h>
#include <zephyr/init.h>
#include <soc.h>
/* 引入 Company HAL 提供的底层接口 */
#include <company_clock.h>
#include <company_irq.h>
/* ——————————————————————
* 1. 时钟配置
*
* 目标:把 SoC 的时钟树配置到稳定状态。
* 顺序很重要:先配 PLL,再配总线分频,最后使能外设时钟。
* 如果时钟没配好,后面所有外设(UART/SPI/I2C)都无法工作。
* —————————————————————— */
static void soc_clock_init(void)
{
/* 1.1 配置系统 PLL:输入 8 MHz 晶振,倍频到 96 MHz 主频 */
company_clock_pll_config(COMPANY_PLL_SRC_HSE, 8, 96);
/* 1.2 配置总线分频:HCLK = 96 MHz,PCLK1 = 48 MHz,PCLK2 = 96 MHz */
company_clock_bus_config(COMPANY_BUS_HCLK, 96);
company_clock_bus_config(COMPANY_BUS_PCLK1, 48);
company_clock_bus_config(COMPANY_BUS_PCLK2, 96);
/* 1.3 使能基础外设时钟:GPIO、UART0、SPI0 */
company_clock_enable(COMPANY_CLOCK_GPIO);
company_clock_enable(COMPANY_CLOCK_UART0);
company_clock_enable(COMPANY_CLOCK_SPI0);
}
/* ——————————————————————
* 2. 中断控制器初始化
*
* 目标:让 Zephyr 能够正确接收并分发 SoC 的中断。
* 这里只做最基础的 NVIC 配置,具体外设的 IRQ 由各 driver 自行注册。
* —————————————————————— */
static void soc_irq_init(void)
{
/* 2.1 关闭所有外设中断,保证启动阶段处于干净状态 */
company_irq_disable_all();
/* 2.2 设置中断优先级分组:4 位抢占优先级 + 0 位子优先级 */
company_irq_set_priority_group(COMPANY_IRQ_PRIO_GROUP_4);
/* 2.3 使能全局中断(由 Zephyr 内核在启动后期统一打开,
* 这里只做控制器层面的准备) */
company_irq_global_enable();
}
/* ——————————————————————
* 3. SoC 总初始化入口
*
* 这是 Zephyr 在启动早期(PRE_KERNEL_1)调用的函数,
* 必须在内核调度器运行之前完成所有基础硬件配置。
* —————————————————————— */
static int soc_init(void)
{
/* 3.1 先配时钟:一切外设工作的前提 */
soc_clock_init();
/* 3.2 再配中断控制器:保证后续 driver 能注册 ISR */
soc_irq_init();
/* 3.3 最后做 SoC 级杂项初始化(如 memory protection 等) */
company_soc_lowlevel_init();
return 0;
}
/* 注册为 PRE_KERNEL_1 阶段初始化,优先级 0(最早执行) */
SYS_INIT(soc_init, PRE_KERNEL_1, CONFIG_KERNEL_INIT_PRIORITY_DEFAULT);
关键点说明:
- soc_clock_init():先配 PLL 再配分频,最后使能外设时钟,顺序不能颠倒;
- soc_irq_init():先关中断、再设优先级分组,保证启动阶段干净;
- SYS_INIT(…, PRE_KERNEL_1, …):让 Zephyr 在内核启动前自动调用 soc_init(),这是 SoC Port 与内核的正式接缝。
22 CPU / Architecture
23 Startup
24 Interrupt Controller
25 Clock / Reset
下面是一个完整的 Devicetree 节点示例,它把 uart0、gpio0、spi0 等外设统一描述出来:
```dts
/ {
soc {
/* UART0:串口控制器,基地址 0x40000000,IRQ 5 */
uart0: uart@40000000 {
compatible = "company,cx1-uart";
reg = <0x40000000 0x1000>;
interrupts = <5>;
clocks = <&clk 0>;
status = "okay";
};
/* GPIO0:通用输入输出控制器,基地址 0x40020000,IRQ 6 */
gpio0: gpio@40020000 {
compatible = "company,cx1-gpio";
reg = <0x40020000 0x1000>;
interrupts = <6>;
clocks = <&clk 1>;
status = "okay";
};
/* SPI0:串行外设接口,基地址 0x40040000,IRQ 7 */
spi0: spi@40040000 {
compatible = "company,cx1-spi";
reg = <0x40040000 0x1000>;
interrupts = <7>;
clocks = <&clk 2>;
status = "okay";
};
};
};
对应的 binding YAML 文件内容如下:
# dts/bindings/serial/company,cx1-uart.yaml
description: Company CX1 UART controller
compatible: "company,cx1-uart"
include: [uart–controller.yaml]
properties:
reg:
required: true
interrupts:
required: true
clocks:
required: true
# dts/bindings/gpio/company,cx1-gpio.yaml
description: Company CX1 GPIO controller
compatible: "company,cx1-gpio"
include: [gpio–controller.yaml]
properties:
reg:
required: true
interrupts:
required: true
clocks:
required: true
# dts/bindings/spi/company,cx1-spi.yaml
description: Company CX1 SPI controller
compatible: "company,cx1-spi"
include: [spi–controller.yaml]
properties:
reg:
required: true
interrupts:
required: true
clocks:
required: true
DTS 与 Binding 如何协同工作
DTS 描述的是「这块板子上有哪些硬件、它们长什么样」,而 Binding 描述的是「这些硬件节点里每个属性到底是什么意思、哪些是必须的」。两者通过 compatible 字符串一一对应:Zephyr 在构建时读取 DTS 中的每个节点,根据它的 compatible 找到对应的 Binding YAML,再用 Binding 里的规则去校验和解析这个节点。
例如 uart0 节点声明了 compatible = "company,cx1-uart",Zephyr 就会去 dts/bindings/serial/company,cx1-uart.yaml 里查它的定义,发现 reg、interrupts、clocks 都是必填属性,于是校验通过后生成对应的宏(如 DT_INST_REG_ADDR(0)、DT_INST_IRQN(0)),driver 就能通过这些宏拿到硬件地址和中断号,而不用在 C 代码里硬编码。这样 DTS 负责「描述事实」,Binding 负责「解释规则」,两者配合,硬件描述体系才算完整。
下面是一个完整的 startup_company.S 汇编示例,它把向量表、复位处理、栈指针初始化和跳转到 main 的流程串在一起:
/*
* startup_company.S – Company CX1 启动代码
*
* 该文件是 Zephyr SoC Port 中 CPU 上电后的第一段执行代码,
* 负责建立向量表、初始化栈指针、调用 SystemInit 并最终跳转到 main。
* 它是"CPU reset → startup → stack → vector table → clock → kernel → main"
* 这条启动链路的起点。
*/
/* ==================================================================
* 1. 向量表(Vector Table)
*
* Cortex-M 上电后,CPU 从 0x00000000 读取初始栈指针(MSP),
* 从 0x00000004 读取复位向量(Reset_Handler)并跳转执行。
* 因此向量表的前两项必须严格按顺序排列。
* ================================================================== */
.section .isr_vector, "a"
.align 2
.global _vector_table
_vector_table:
.long __stack_top /* 0x00: 初始栈指针(MSP) */
.long Reset_Handler /* 0x04: 复位向量 */
.long NMI_Handler /* 0x08: NMI 异常 */
.long HardFault_Handler /* 0x0C: HardFault 异常 */
.long MemManage_Handler /* 0x10: MemManage 异常 */
.long BusFault_Handler /* 0x14: BusFault 异常 */
.long UsageFault_Handler /* 0x18: UsageFault 异常 */
.long 0 /* 0x1C: 保留 */
.long 0 /* 0x20: 保留 */
.long 0 /* 0x24: 保留 */
.long 0 /* 0x28: 保留 */
.long SVC_Handler /* 0x2C: SVC 系统调用 */
.long DebugMon_Handler /* 0x30: 调试监视器 */
.long 0 /* 0x34: 保留 */
.long PendSV_Handler /* 0x38: PendSV 可挂起系统调用 */
.long SysTick_Handler /* 0x3C: SysTick 系统节拍 */
/* ==================================================================
* 2. 复位处理(Reset_Handler)
*
* 这是 CPU 上电/复位后真正执行的第一段代码。
* 流程:初始化栈指针 → 调用 SystemInit → 跳转 main。
* ================================================================== */
.section .text
.align 2
.global Reset_Handler
.thumb_func
.type Reset_Handler, %function
Reset_Handler:
/* 2.1 初始化栈指针(MSP)
* 虽然向量表第一项已经给出了初始 MSP,但这里再次显式设置,
* 确保即使链接脚本调整了 __stack_top 也能正确加载。 */
ldr r0, =__stack_top
msr msp, r0
/* 2.2 调用 SystemInit()
* 该函数由 SoC 提供,负责最基础的时钟/电源初始化,
* 让 CPU 具备运行 C 代码的条件(如配置 Flash 等待周期、PLL 等)。 */
bl SystemInit
/* 2.3 跳转到 main()
* 进入 C 世界。main 内部会继续完成 Zephyr 内核的启动。 */
bl main
/* 2.4 防御:如果 main 意外返回,则死循环 */
1: b 1b
.size Reset_Handler, .-Reset_Handler
/* ==================================================================
* 3. 默认异常处理(弱符号,可被覆盖)
*
* 所有异常处理函数都定义为 weak,这样 Zephyr 或应用层
* 可以定义同名强符号来覆盖默认实现。
* ================================================================== */
.macro def_weak_handler name
.section .text.\\name, "ax"
.align 2
.global \\name
.weak \\name
.thumb_func
.type \\name, %function
\\name:
b .
.size \\name, .-\\name
.endm
def_weak_handler NMI_Handler
def_weak_handler HardFault_Handler
def_weak_handler MemManage_Handler
def_weak_handler BusFault_Handler
def_weak_handler UsageFault_Handler
def_weak_handler SVC_Handler
def_weak_handler DebugMon_Handler
def_weak_handler PendSV_Handler
def_weak_handler SysTick_Handler
/* ==================================================================
* 4. 外部符号声明
*
* __stack_top 由链接脚本(linker script)定义,
* SystemInit 和 main 由 C 代码提供。
* ================================================================== */
.extern __stack_top
.extern SystemInit
.extern main
.end
关键点说明:
- 向量表:前两项必须是 __stack_top(初始 MSP)和 Reset_Handler(复位向量),这是 Cortex-M 硬件规定的启动协议;
- Reset_Handler:上电后第一段代码,依次完成栈指针初始化、调用 SystemInit()、跳转 main();
- SystemInit():由 SoC 提供的 C 函数,负责最基础的时钟/电源配置,是 C 世界能运行的前提;
- 弱符号异常处理:所有异常处理都定义为 weak,Zephyr 内核或驱动可以按需覆盖,实现真正的可扩展性;
- .thumb_func:Cortex-M 只支持 Thumb 指令集,必须显式声明,否则链接时可能出错。
五、第四阶段:Startup 跑起来
这是第一个真正的 milestone。
目标不是:
UART OK
SPI OK
I2C OK
而是:
CPU reset
↓
startup
↓
stack
↓
vector table
↓
clock
↓
interrupt controller
↓
Zephyr kernel
↓
main()
最终:
int main(void)
{
printk("Hello Company SoC\\n");
while (1) {
}
}
如果能看到:
Hello Company SoC
意味着:
CPU + startup + linker + basic SoC initialization + Zephyr kernel 已经形成闭环。
六、第五阶段:Interrupt Controller
下一步不是立刻写 UART。
而是:
让 Zephyr 真正能够处理这个 SoC 的中断。
例如:
UART0 IRQ
↓
NVIC / Company IRQ Controller
↓
Zephyr ISR
↓
UART driver
如果 IRQ mapping 错误:
UART driver
↓
interrupt
↓
错误 IRQ
↓
系统完全没有反应
所以 BSP 中:
IRQ architecture
是非常核心的一层。
七、第六阶段:Clock / Reset
然后建立:
clock
reset
power
例如:
HCLK
├── CPU
├── GPIO
├── UART
├── SPI
└── TIMER
此时一个 UART driver 才真正有条件运行:
UART driver
↓
clock enable
↓
pinmux
↓
register config
↓
IRQ
所以:
UART 从来不是孤立 driver。
它背后可能依赖:
Clock
Reset
Pinmux
IRQ
DMA
Power
这也是第 14 篇讲 dependency graph 的实际落地。
八、第七阶段:Devicetree
接下来把硬件描述正式交给 Devicetree。
例如:
soc {
uart0: uart@40000000 {
compatible = "company,cx1-uart";
reg = <0x40000000 0x1000>;
interrupts = <5>;
status = "okay";
};
};
这一步非常关键。
因为从这里开始:
硬件事实
和:
driver implementation
开始解耦。
Driver 不应该写:
# define UART0_BASE 0x40000000
而应该通过 DT 获取:
DT_INST_REG_ADDR(0)
或者:
DT_INST_IRQN(0)
这样:
UART0
UART1
UART2
都可以由同一套 driver 支持。
九、第八阶段:Binding
然后告诉 Zephyr:
“company,cx1-uart” 到底意味着什么?
例如:
dts/bindings/serial/company,cx1–uart.yaml
定义:
properties:
reg:
required: true
interrupts:
required: true
clocks:
required: true
于是:
DTS
↓
Binding
↓
Validation
↓
Generated Devicetree
整个硬件描述体系就完整了。
十、第九阶段:第一个真正的 Driver
现在才进入:
UART driver
典型结构:
drivers/
└── serial/
└── uart_company.c
里面:
static int uart_company_init(const struct device *dev)
{
...
}
然后:
static DEVICE_API(uart, uart_company_driver_api) = {
.poll_in = ...,
.poll_out = ...,
};
最终:
DEVICE_DT_INST_DEFINE(
0,
uart_company_init,
NULL,
...
);
于是整个链路形成:
DTS node
↓
DT_INST
↓
DEVICE_DT_INST_DEFINE()
↓
struct device
↓
UART API
↓
Company HAL
↓
register
这就是我们第 11~19 篇反复拆解的东西。
十一、第十阶段:从 UART 扩展到整个外设体系
UART 跑起来之后:
UART
↓
GPIO
↓
SPI
↓
I2C
↓
Timer
↓
PWM
↓
ADC
↓
DMA
↓
Flash
↓
Watchdog
但是不要理解成:
“一个一个写 driver 就完事。”
真正应该形成的是:
Common SoC infrastructure
│
├── clock
├── reset
├── irq
├── pinctrl
└── power
│
↓
Peripheral drivers
这样 BSP 才不会越来越乱。
十二、第十一阶段:Kconfig
到了这里,BSP 开始真正变成:
可配置的平台。
例如:
CONFIG_SOC_COMPANY_CX1
CONFIG_UART_COMPANY
CONFIG_GPIO
CONFIG_SPI
CONFIG_I2C
CONFIG_CLOCK_CONTROL
CONFIG_PINCTRL
于是:
Board
↓
SoC
↓
Kconfig
↓
Driver selection
最终:
west build
可以决定:
哪些代码进入最终 firmware
而不是所有东西全部编译进去。
十三、第十二阶段:CMake / Build System
此时开始解决:
到底编译哪些 .c?
例如:
zephyr_library()
zephyr_library_sources(
soc.c
clock.c
)
Driver:
zephyr_library()
zephyr_library_sources_ifdef(
CONFIG_UART_COMPANY
uart_company.c
)
最终形成:
Kconfig
↓
CMake
↓
source selection
↓
compiler
↓
linker
这就是第 32 篇。
十四、第十三阶段:Linker / Memory Map
这一步特别容易被初学者低估。
因为 BSP 不是:
C code compile
而是:
C code
+
startup
+
vector table
+
Flash
+
RAM
+
sections
+
stack
+
heap
最终:
firmware.elf
必须符合:
FLASH
0x08000000
│
├── vector table
├── text
├── rodata
└── ...
│
└── FLASH END
RAM
0x20000000
│
├── data
├── bss
├── heap
├── stack
└── ...
如果 linker script 错了:
代码可能编译成功
但:
CPU 一启动就 HardFault
所以:
Build successful ≠ BSP successful。
十五、第十四阶段:Board Support Package
到这里 SoC 已经基本存在。
现在才建立:
boards/
└── company/
└── cx1_devkit/
Board 描述:
SoC
+
Crystal
+
LED
+
Button
+
UART pins
+
SPI pins
+
I2C pins
例如:
/ {
aliases {
led0 = &led0;
sw0 = &button0;
};
};
于是:
SoC
和:
Board
真正分离。
十六、SoC 与 Board 的关系
这点非常重要。
假设:
CX1 SoC
有:
UART0
UART1
UART2
那么可以出现:
CX1-DevKit
CX1-EVB
CX1-SensorBoard
CX1-Module
它们:
共享 SoC
但:
Board DTS
不同。
所以:
CX1 SoC
/ | \\
/ | \\
↓ ↓ ↓
DevKit EVB Module
这就是为什么我们必须把:
SoC
和:
Board
分层。
为了更直观地理解两者的分层关系,下面用一张表格对比 SoC 与 Board 在 Zephyr BSP 中的职责差异:
| 描述对象 | 芯片本身:CPU 内核、内存、外设控制器、中断、时钟树 | 具体开发板:引脚分配、LED、按键、晶振、板载外设 |
| 文件位置 | soc/company/cx1/(如 soc.c、soc.h、Kconfig.soc) | boards/company/cx1_devkit/(如 cx1_devkit.dts、board.cmake) |
| 典型内容 | SoC 初始化、时钟/复位配置、中断控制器、SoC 级 Devicetree 节点 | 板级 DTS(引脚 mux、LED/按键别名)、板级 Kconfig、runner 配置 |
| 变更影响 | 影响所有使用该 SoC 的板子,改动需回归整个 SoC Family | 只影响当前这块板子,不影响其它板卡 |
| 复用关系 | 一颗 SoC 可被多块板子共享 | 一块板子只能绑定一颗 SoC |
| 生命周期 | 随芯片演进(CX1 → CX2 → CX3) | 随硬件改版(DevKit Rev.1 → Rev.2) |
从表格可以看出:SoC 是"芯片能力"的抽象,Board 是"具体板卡"的实例化。SoC 层回答"这颗芯片能做什么",Board 层回答"这块板子上怎么用"。因此 SoC 的改动影响面大、需要谨慎回归,而 Board 的改动相对局部、风险可控——这正是我们必须把两者分层管理的根本原因。
除了 DTS 和 Kconfig,Board 目录里还有一个容易被忽略但非常关键的文件:board.cmake。它负责告诉 Zephyr 的构建系统"这块板子应该用什么工具烧录和调试",也就是配置 west flash / west debug 背后的 runner。下面是一个完整的 board.cmake 示例,为 CX1 DevKit 配置 J-Link 作为默认 runner:
# board.cmake – CX1 DevKit 的 flash / debug runner 配置
#
# 该文件位于 boards/company/cx1_devkit/ 目录下,
# 作用是告诉 Zephyr 构建系统:这块板子用哪个 runner 烧录和调试。
# 没有它,west flash / west debug 将无法工作。
# 1. 声明本板支持的 runner 列表
#
# 这里列出所有可用的 runner,按优先级从高到低排列。
# west flash 时会依次尝试,直到找到可用的那个。
# 我们把 jlink 放在最前面,表示它是默认首选。
set(SUPPORTED_RUNNERS jlink openocd)
# 2. 配置 J-Link runner
#
# J-Link 是 SEGGER 的调试探针,通过 SWD 接口连接 CX1 DevKit。
# 这是公司内部最常用的烧录方式,因此作为默认配置。
board_runner_args(jlink
# –device:指定目标芯片型号,J-Link 根据它选择正确的 Flash 算法
–device=CX1_M4
# –if:指定调试接口,SWD 是 Cortex-M 最常用的两线调试接口
–if=swd
# –speed:设置 SWD 时钟频率(kHz),4000 表示 4 MHz
–speed=4000
# –reset:烧录完成后自动复位并运行固件
–reset
)
# 3. 配置 OpenOCD runner(备用)
#
# 当没有 J-Link、只有通用调试探针(如 CMSIS-DAP)时,
# 可以改用 OpenOCD。这里通过 board.cfg 复用 Zephyr 自带的
# 通用 Cortex-M 配置,再叠加 CX1 的复位配置。
board_runner_args(openocd
# –cmd-load:OpenOCD 烧录时执行的命令,program 表示写入固件
–cmd-load="program {0} verify reset exit"
# –cmd-attach:OpenOCD 调试时执行的命令,halt 表示先暂停 CPU
–cmd-attach="halt"
# –board-cfg:指定板级 OpenOCD 配置文件
–board-cfg=board/company_cx1_devkit.cfg
)
# 4. 注册 runner 到构建系统
#
# 这一行把上面配置的 runner 真正挂载到 Zephyr 的构建流程中,
# 让 west flash / west debug 能够识别并调用它们。
include(${ZEPHYR_BASE}/boards/common/jlink.board.cmake)
include(${ZEPHYR_BASE}/boards/common/openocd.board.cmake)
关键点说明:
- SUPPORTED_RUNNERS:声明板子支持的 runner 列表,顺序即优先级,jlink 在前表示默认首选;
- board_runner_args(jlink …):为 J-Link 配置目标芯片型号、SWD 接口、时钟频率和烧录后自动复位;
- board_runner_args(openocd …):为 OpenOCD 配置烧录/调试命令和板级配置文件,作为无 J-Link 时的备用方案;
- include(…jlink.board.cmake):把 runner 真正注册进 Zephyr 构建系统,没有这行配置不会生效。
这样配置之后,工程师只需要执行 west flash 就能自动调用 J-Link 烧录 CX1 DevKit,west debug 也能直接拉起 GDB 连接调试——Flash 和 Debug 真正成为 BSP 交付链的一部分。
十七、第十五阶段:Flash / Debug / Runner
十八、第十六阶段:BSP Validation
阶段目标
BSP Validation 的目标不是"能跑 blinky 就算成功",而是逐层证明这个 BSP 在硬件、驱动、内核三个层面都稳定可靠。它要回答三个问题:
1. 能不能编译通过? → Build 层
2. 能不能启动并跑起来? → Boot / Console 层
3. 外设能不能稳定工作? → Peripheral / Stress 层
Validation 不是一次性的"验收动作",而是每次代码变更后都要跑的回归基线。只有把验证固化下来,BSP 才敢说"可交付"。
关键交付物
tests/
├── boards/
│ └── company/
│ └── cx1_devkit/
│ ├── boot/
│ │ └── test_boot.c # 启动 + printk 验证
│ ├── gpio/
│ │ ├── test_led.c # LED 点亮/熄灭
│ │ └── test_button.c # 按键中断
│ ├── serial/
│ │ └── test_uart_loopback.c # UART 回环测试
│ ├── spi/
│ │ └── test_spi_flash.c # SPI Flash 读写
│ ├── timer/
│ │ └── test_timer.c # 定时器精度
│ └── stress/
│ └── test_uart_stress.c # 长时间压力测试
每个测试都对应一个 prj.conf 和 testcase.yaml,例如:
# tests/boards/company/cx1_devkit/gpio/testcase.yaml
tests:
gpio.basic:
build_only: false
platform_allow: company_cx1_devkit
tags: gpio
harness: ztest
常见问题与排查方法
| 编译通过但上电无输出 | 时钟未配置 / UART pinmux 错误 | 检查 soc_clock_init() 和 board DTS 的 pinctrl |
| printk 乱码 | 波特率不匹配 / 时钟分频错误 | 核对 UART 时钟源和 CONFIG_UART_BAUD_RATE |
| 按键中断不触发 | IRQ 号错误 / 未使能 NVIC | 对照 SoC 手册核对 interrupts = <N> |
| SPI 读写全 0xFF | 时钟极性/相位错误 | 检查 spi-config 的 CPOL/CPHA 与从设备匹配 |
| 长时间运行后死机 | 内存泄漏 / 中断风暴 | 用 CONFIG_ASSERT=y + 看门狗复现 |
十九、第十七阶段:SoC Family
阶段目标
当公司推出第二颗、第三颗 SoC 时,目标不是"再复制一份 BSP",而是通过 SoC Family 架构最大化复用、最小化差异。CX1、CX2、CX3 共享 CPU 架构和大部分外设 IP,只有 Flash/RAM 容量、外设数量和时钟树存在差异。
关键交付物
soc/
└── company/
├── common/
│ ├── CMakeLists.txt # 共享源文件
│ ├── Kconfig # 共享配置
│ ├── soc_common.c # 公共初始化
│ └── include/
│ └── company_soc_common.h
├── cx1/
│ ├── CMakeLists.txt
│ ├── Kconfig.soc
│ └── soc.c # CX1 特有初始化
├── cx2/
│ ├── CMakeLists.txt
│ ├── Kconfig.soc
│ └── soc.c
└── cx3/
├── CMakeLists.txt
├── Kconfig.soc
└── soc.c
共享部分通过 Kconfig 选择:
# soc/company/common/Kconfig
config SOC_COMPANY_COMMON
bool
select CLOCK_CONTROL
select PINCTRL
help
Common infrastructure for Company SoC family.
常见问题与排查方法
| 新 SoC 编译报"未定义符号" | 公共代码引用了某颗 SoC 特有符号 | 用 #if defined(CONFIG_SOC_COMPANY_CX2) 隔离差异 |
| 同一 driver 在不同 SoC 行为不同 | 寄存器偏移或时钟树差异 | 在 DTS 中通过 reg / clocks 属性区分,而非硬编码 |
| 修改公共代码导致旧 SoC 回归 | 公共层被过度抽象 | 建立 SoC Family 的 CI 矩阵,覆盖所有成员 |
二十、第十八阶段:Release / Maintenance
阶段目标
Release 阶段的目标是把 BSP 从"工程师电脑上的代码"变成"公司可维护的软件平台"。它要求版本可追溯、变更可回滚、升级可预期。Maintenance 则保证 BSP 在 Zephyr 上游升级、芯片改版后依然稳定。
关键交付物
release/
├── v1.0/
│ ├── zephyr-4.2.x/
│ ├── company-hal-2.1/
│ └── cx1-revB/
├── v1.1/
│ ├── zephyr-4.2.x/
│ ├── company-hal-2.2/
│ └── cx1-revB/
└── v2.0/
├── zephyr-4.3.x/
├── company-hal-3.0/
└── cx2-revA/
版本信息固化在代码中:
/* soc/company/cx1/soc.h */
#define COMPANY_SOC_VERSION_MAJOR 1
#define COMPANY_SOC_VERSION_MINOR 1
#define COMPANY_SOC_VERSION_PATCH 0
#define COMPANY_BSP_VERSION "1.1.0"
#define COMPANY_ZEPHYR_VERSION "4.2.x"
常见问题与排查方法
| 升级 Zephyr 后 BSP 编译失败 | API 变更 / Kconfig 改名 | 对照 Zephyr release notes 逐项迁移 |
| 芯片改版后行为异常 | 寄存器地址或时序变化 | 用 #ifdef 按 SoC revision 区分,或更新 DTS |
| 客户反馈问题无法复现 | 版本信息不完整 | 在固件中打印 BSP / HAL / Zephyr 版本号 |
二十一、最终形成 CI
阶段目标
CI 的目标是让每一次代码提交都自动跑完整个验证矩阵,把"人肉回归"变成"机器回归"。只有 CI 全绿,BSP 才允许合入主干。
关键交付物
# .github/workflows/bsp-ci.yml
name: BSP CI
on: [push, pull_request]
jobs:
build:
strategy:
matrix:
board: [cx1_devkit, cx1_evb, cx2_devkit, cx3_evb]
config: [debug, release]
runs-on: ubuntu–latest
steps:
– uses: actions/checkout@v4
– name: Build
run: |
west build -b ${{ matrix.board }} \\
-d build/${{ matrix.board }}/${{ matrix.config }} \\
— -DCONFIG_DEBUG=${{ matrix.config == 'debug' }}
– name: Test
run: |
west build -t test -d build/${{ matrix.board }}/${{ matrix.config }}
常见问题与排查方法
| CI 本地通过、服务器失败 | 工具链版本不一致 | 在 CI 中固定 Zephyr SDK 版本 |
| 矩阵任务超时 | 编译任务过多 | 拆分 build 和 test 为独立 job,并行执行 |
| 偶发失败无法复现 | 硬件依赖 / 时序敏感 | 标记为 flaky,单独重跑并记录日志 |
二十二、所以,一个 BSP 的完整生命周期其实是这个
把前面的全部压缩:
┌───────────────┐
│ SoC Spec │
└───────┬───────┘
↓
┌───────────────┐
│ Company HAL │
└───────┬───────┘
↓
┌───────────────┐
│ SoC Port │
└───────┬───────┘
↓
┌───────────────┐
│ CPU / Startup │
└───────┬───────┘
↓
┌───────────────┐
│ IRQ │
└───────┬───────┘
↓
┌───────────────┐
│ Clock / Reset │
└───────┬───────┘
↓
┌───────────────┐
│ Devicetree │
│ + Binding │
└───────┬───────┘
↓
┌───────────────┐
│ Kconfig │
└───────┬───────┘
↓
┌───────────────┐
│ Drivers │
└───────┬───────┘
↓
┌───────────────┐
│ CMake │
└───────┬───────┘
↓
┌───────────────┐
│ Linker │
└───────┬───────┘
↓
┌───────────────┐
│ Board │
└───────┬───────┘
↓
┌───────────────┐
│ Flash/Debug │
└───────┬───────┘
↓
┌───────────────┐
│ Validation │
└───────┬───────┘
↓
┌───────────────┐
│ Release │
└───────┬───────┘
↓
┌───────────────┐
│ CI │
└───────┬───────┘
↓
┌───────────────┐
│ Maintenance │
└───────┬───────┘
│
└───────────────→ 下一代 SoC
二十三、最重要的是:BSP 不是"一个目录"
这是整个 20~40 篇最值得记住的一句话:
BSP 是一条从 Silicon 到 Application 的软件供应链,而不是一个 boards/xxx/ 目录。
它至少包含:
SoC
HAL
Architecture
Startup
IRQ
Clock
Reset
Devicetree
Binding
Kconfig
Drivers
CMake
Linker
Board
Runner
Validation
CI
Release
Maintenance
二十四、公司真正需要维护的是"四条线"
以后你进入公司做 BSP,可以直接用这四条线检查。
① 硬件线
Silicon
↓
SoC
↓
Board
↓
Pin / Clock / IRQ / Memory
② 软件抽象线
Register
↓
HAL
↓
Zephyr Driver
↓
Zephyr API
↓
Application
③ 构建线
Kconfig
↓
Devicetree
↓
CMake
↓
Compiler
↓
Linker
↓
ELF
↓
Binary
④ 验证线
Build
↓
Boot
↓
Console
↓
Peripheral
↓
Interrupt
↓
Stress
↓
Regression
↓
CI
如果这四条线都能闭环:
Hardware
│
↓
HAL → SoC → Zephyr → Board
│
↓
Build System
│
↓
Firmware
│
↓
Validation
│
└────→ Release
那么你做的就已经不是一个"Zephyr demo"。
而是一个真正可以交给公司其他团队使用的 Zephyr BSP。
二十五、20~40 篇终于串起来了
你前面的学习路线其实正好形成了一条完整的工程路径:
20 理解 Zephyr BSP
↓
21 SoC Port Skeleton
↓
22 CPU / Architecture
↓
23 Startup
↓
24 Interrupt Controller
↓
25 Clock / Reset
↓
26 Devicetree
↓
27 Binding
↓
28 UART Driver
↓
29 GPIO / SPI / I2C / Timer
↓
30 Board
↓
31 Kconfig
↓
32 CMake
↓
33 Linker / Memory Map
↓
34 Flash / Debug / Runner
↓
35 BSP Validation
↓
36 Company HAL
↓
37 HAL / Zephyr Driver Boundary
↓
38 Multi-Board / Multi-Chip
↓
39 SoC Family Architecture
↓
40 BSP Lifecycle
所以从这里开始,你已经可以换一个视角:
不再是"我怎么学 Zephyr?"
而是:
“如果明天公司给我一颗自己的 SoC,我应该按照什么工程流程把它变成一个可交付的 Zephyr BSP?”
这就是 20~40 篇真正想建立起来的能力。
现在终于到了:
west flash
west debug
这一步需要:
Runner
↓
OpenOCD / J-Link / pyOCD / vendor tool
↓
SWD/JTAG
↓
MCU
例如:
west build
↓
firmware.elf
↓
west flash
↓
debug probe
↓
SoC
同时:
west debug
需要:
ELF
+
debug symbols
+
GDB
+
probe
所以:
Flash 和 Debug 不是"额外工具",而是 BSP 交付链的一部分。
十八、第十六阶段:BSP Validation
这就是第 35 篇的核心。
不能只测试:
blinky
而应该逐层验证。
Level 0 — Build
west build
检查:
CMake
Kconfig
Devicetree
compiler
linker
Level 1 — Boot
firmware
↓
reset
↓
main()
确认:
CPU 可以启动
Level 2 — Console
UART
↓
printk()
确认:
console 正常
Level 3 — GPIO
LED
Button
Level 4 — Interrupt
button IRQ
timer IRQ
uart IRQ
Level 5 — Communication
SPI
I2C
UART
Level 6 — Timing
Timer
PWM
counter
Level 7 — DMA
Peripheral
↓
DMA
↓
RAM
Level 8 — Power / Reset
clock gating
reset
sleep
wake-up
Level 9 — Stress
例如:
UART continuous RX
SPI continuous transfer
Timer high frequency
multiple interrupts
十九、第十七阶段:SoC Family
到这里,公司通常会遇到一个新的问题:
“下一颗 SoC 来了。”
例如:
CX1
CX2
CX3
它们可能:
CPU 相同
UART 相同
GPIO 相同
SPI 相同
只是:
Flash 不同
RAM 不同
peripheral 数量不同
clock tree 有差异
这时不能复制三份 BSP。
正确方式是:
company/
└── soc/
├── cx1/
├── cx2/
└── cx3/
共享:
common/
而差异:
cx1/
cx2/
cx3/
单独处理。
这就是第 39 篇:
SoC Family Architecture
二十、第十八阶段:Release
真正的公司 BSP 最后一定会进入版本管理。
例如:
BSP v1.0
↓
BSP v1.1
↓
BSP v2.0
同时对应:
SoC revision
Board revision
HAL version
Zephyr version
Toolchain version
例如:
Company BSP
│
├── Zephyr 4.2.x
├── Company HAL 2.1
├── CX1 Rev.B
└── DevKit Rev.2
这时候开始出现一个非常现实的问题:
如何保证升级 Zephyr 后 BSP 不坏?
答案就是:
CI
+
BSP Validation
+
Regression Test
二十一、最终形成 CI
例如公司服务器:
Git Push
↓
CI
↓
west build
↓
multiple boards
↓
multiple configurations
例如:
CX1 DevKit
CX1 EVB
CX2 DevKit
CX3 EVB
同时:
CONFIG_DEBUG=y
CONFIG_DEBUG=n
UART=y
SPI=y
I2C=y
最终形成矩阵:
CX1 CX2 CX3
DevKit ✓ ✓ ✓
EVB ✓ ✓ ✓
Debug ✓ ✓ ✓
Release ✓ ✓ ✓
这时候 BSP 才真正从:
“工程师电脑上的代码”
变成:
公司的可维护软件平台。
二十二、所以,一个 BSP 的完整生命周期其实是这个
把前面的全部压缩:
┌───────────────┐
│ SoC Spec │
└───────┬───────┘
↓
┌───────────────┐
│ Company HAL │
└───────┬───────┘
↓
┌───────────────┐
│ SoC Port │
└───────┬───────┘
↓
┌───────────────┐
│ CPU / Startup │
└───────┬───────┘
↓
┌───────────────┐
│ IRQ │
└───────┬───────┘
↓
┌───────────────┐
│ Clock / Reset │
└───────┬───────┘
↓
┌───────────────┐
│ Devicetree │
│ + Binding │
└───────┬───────┘
↓
┌───────────────┐
│ Kconfig │
└───────┬───────┘
↓
┌───────────────┐
│ Drivers │
└───────┬───────┘
↓
┌───────────────┐
│ CMake │
└───────┬───────┘
↓
┌───────────────┐
│ Linker │
└───────┬───────┘
↓
┌───────────────┐
│ Board │
└───────┬───────┘
↓
┌───────────────┐
│ Flash/Debug │
└───────┬───────┘
↓
┌───────────────┐
│ Validation │
└───────┬───────┘
↓
┌───────────────┐
│ Release │
└───────┬───────┘
↓
┌───────────────┐
│ CI │
└───────┬───────┘
↓
┌───────────────┐
│ Maintenance │
└───────┬───────┘
│
└───────────────→ 下一代 SoC
二十三、最重要的是:BSP 不是"一个目录"
这是整个 20~40 篇最值得记住的一句话:
BSP 是一条从 Silicon 到 Application 的软件供应链,而不是一个 boards/xxx/ 目录。
它至少包含:
SoC
HAL
Architecture
Startup
IRQ
Clock
Reset
Devicetree
Binding
Kconfig
Drivers
CMake
Linker
Board
Runner
Validation
CI
Release
Maintenance
二十四、公司真正需要维护的是"四条线"
以后你进入公司做 BSP,可以直接用这四条线检查。
① 硬件线
Silicon
↓
SoC
↓
Board
↓
Pin / Clock / IRQ / Memory
② 软件抽象线
Register
↓
HAL
↓
Zephyr Driver
↓
Zephyr API
↓
Application
③ 构建线
Kconfig
↓
Devicetree
↓
CMake
↓
Compiler
↓
Linker
↓
ELF
↓
Binary
④ 验证线
Build
↓
Boot
↓
Console
↓
Peripheral
↓
Interrupt
↓
Stress
↓
Regression
↓
CI
如果这四条线都能闭环:
Hardware
│
↓
HAL → SoC → Zephyr → Board
│
↓
Build System
│
↓
Firmware
│
↓
Validation
│
└────→ Release
那么你做的就已经不是一个"Zephyr demo"。
而是一个真正可以交给公司其他团队使用的 Zephyr BSP。
二十五、20~40 篇终于串起来了
你前面的学习路线其实正好形成了一条完整的工程路径:
20 理解 Zephyr BSP
↓
21 SoC Port Skeleton
↓
22 CPU / Architecture
↓
23 Startup
↓
24 Interrupt Controller
↓
25 Clock / Reset
↓
26 Devicetree
↓
27 Binding
↓
28 UART Driver
↓
29 GPIO / SPI / I2C / Timer
↓
30 Board
↓
31 Kconfig
↓
32 CMake
↓
33 Linker / Memory Map
↓
34 Flash / Debug / Runner
↓
35 BSP Validation
↓
36 Company HAL
↓
37 HAL / Zephyr Driver Boundary
↓
38 Multi-Board / Multi-Chip
↓
39 SoC Family Architecture
↓
40 BSP Lifecycle
所以从这里开始,你已经可以换一个视角:
不再是"我怎么学 Zephyr?"
而是:
“如果明天公司给我一颗自己的 SoC,我应该按照什么工程流程把它变成一个可交付的 Zephyr BSP?”
这就是 20~40 篇真正想建立起来的能力。
二十六、总结与四条线
回顾全文,一个公司 SoC 从芯片定义到 BSP 可交付,核心链路是:SoC Spec → Company HAL → Zephyr SoC Port → Startup → IRQ → Clock/Reset → Devicetree/Binding → Kconfig → Drivers → CMake → Linker → Board → Flash/Debug → Validation → SoC Family → Release → CI。这 18 个阶段环环相扣,任何一环缺失,BSP 都无法真正交付。而支撑这条链路的,正是公司需要长期维护的「四条线」。
① 硬件线
- 维护内容:SoC 规格、memory map、interrupt map、clock tree、pinmux,以及 Board 层的引脚分配与板载外设描述。
- 常见陷阱:芯片改版(revision)后寄存器地址或时序变化未同步到 DTS;把硬件细节硬编码进 driver,导致多板复用困难。
② 软件抽象线
- 维护内容:Company HAL 与 Zephyr Driver 的边界、struct device 与 driver API 的封装、DT 宏到 HAL 调用的映射。
- 常见陷阱:HAL 层混入 Zephyr 概念(如 DEVICE_DT_DEFINE),破坏分层;driver 直接操作寄存器,绕过 HAL 导致抽象失效。
③ 构建线
- 维护内容:Kconfig 配置项、Devicetree 节点、CMake 源文件选择、Linker 脚本与内存布局,确保 west build 产出正确的 firmware。
- 常见陷阱:Kconfig 依赖缺失导致外设未编译;linker script 错误导致「编译成功但上电 HardFault」。
④ 验证线
- 维护内容:Build → Boot → Console → Peripheral → Interrupt → Stress → Regression 的逐层验证,以及 CI 矩阵的自动化回归。
- 常见陷阱:只测 blinky 就宣称 BSP 完成;CI 本地通过、服务器失败(工具链版本不一致);偶发失败未标记 flaky 导致误报。
只有这四条线都能闭环,BSP 才真正从「工程师电脑上的代码」变成「公司可维护的软件平台」——这也是 20~40 篇最终想建立的能力。



