ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Zephyr BSP: 23-Zephyr 到底为什么会编译你的 SoC 代码?

Zephyr BSP: 23-Zephyr 到底为什么会编译你的 SoC 代码? Zephyr 启动流程摘要本文从芯片真正开始执行指令的那一刻出发追查 Zephyr 在 SoC 上电后的完整启动链路。核心结论是Zephyr 并不存在一个所有架构统一的第一行 C 代码真正最早执行的是 ARCH-specific 的 reset/startup 汇编代码。对 ARM Cortex-M 而言典型入口为arch/arm/core/cortex_m/reset.S中的z_arm_reset它负责把 CPU 从 Reset 状态带到可运行 C 代码的状态随后依次进入z_prep_c()与z_cstart()。文章详细拆解了 Reset Vector、soc_early_reset_hook()、soc_reset_hook()、z_prep_c()、z_cstart()的职责边界并给出 BSP 开发者拿到新 SoC 时的排查顺序与核心 Call Graph。StartupSoC 上电以后Zephyr 第一行代码在哪里这一篇非常关键。前面我们一直在追BOARD ↓ SOC ↓ ARCH ↓ CMake Target ↓ Zephyr Kernel现在反过来从芯片真正开始执行指令的那一刻追回来SoC 上电 / Reset ↓ CPU Reset Vector ↓ ARCH Reset Handler ↓ SoC Early Hook ↓ 设置 Stack / RAM / CPU ↓ z_prep_c()↓ z_cstart()↓ Kernel 初始化 ↓ main()最重要的结论先说Zephyr 并不存在一个所有架构统一的第一行 C 代码。真正最早执行的是ARCH-specific reset/startup code通常是汇编。对 ARM Cortex-M 来说典型入口就是arch/arm/core/cortex_m/reset.S ↓ z_arm_reset ↓ z_prep_c()↓ z_cstart()Zephyr 官方 Architecture Porting Guide 也明确把 early boot sequence 定义为从 CPU reset 状态进入能够运行 C 代码的状态然后进入 z_cstart()。1. 先不要从 main() 开始看很多刚接触 RTOS 的人会这样想intmain(void){...}是不是CPU ↓ main()当然不是。main() 已经非常晚了。实际上CPU Reset │ ▼ Reset Vector │ ▼ ARCH startup │ ▼ CPU / Stack / RAM 初始化 │ ▼ Zephyr kernel startup │ ▼ 创建/切换 main thread │ ▼ main()所以如果你正在做一个新的 SoC BSP真正应该研究的不是main.c而是ARCH reset/startup SOC early initialization Zephyr kernel startup2. 对 Cortex-MCPU 到底从哪里开始以你现在熟悉的STM32F303RE Cortex-M4为例。Cortex-M reset 后会从向量表读取两个非常重要的东西Vector Table -----------------------|0x00 Initial MSP|-----------------------|0x04 Reset_Handler|-----------------------|0x08 NMI_Handler|-----------------------|0x0C HardFault|-----------------------|...|-----------------------因此CPU Reset │ ├── 从 vector[0]得到初始 SP │ └── 从 vector[1]得到 Reset_Handler │ ▼ 开始执行代码这时候还谈不上main()甚至还没有进入 Zephyr 的普通 C 初始化。3. Zephyr 的 Reset Handler 在哪里以 ARM Cortex-M 为例Zephyr 当前源码zephyr/ └── arch/ └── arm/ └── core/ └── cortex_m/ └── reset.S里面定义了z_arm_reset:也就是 Zephyr Cortex-M 的 reset entry。官方源码把它描述为Reset handler that prepares the system for running C code.也就是说它的主要任务就是把 CPU 从刚 Reset 的状态带到可以运行 C的状态。4. 所以Zephyr 第一行代码到底是哪一行如果你问STM32 Cortex-M 上Zephyr 第一条真正执行的 Zephyr 指令在哪里可以先定位arch/arm/core/cortex_m/reset.S z_arm_reset但是注意这不是一行 C。它是 Assembly。大概流程Reset Vector │ ▼ z_arm_reset │ ├── SOC early reset hook │ ├── SOC reset hook │ ├── 初始化 ARM architecture │ ├── 设置 interrupt stack │ ├── 设置 PSP │ └── bl z_prep_c │ ▼ C 世界当前 Zephyr 的 Cortex-M reset.S 最后会调用bl z_prep_c5. z_arm_reset() 在干什么把源码压缩成人脑版本z_arm_reset: SOC EARLY RESET ↓ 设置/清理 CPU 状态 ↓ SOC RESET HOOK ↓ ARM architecture 初始化 ↓ 关闭/锁定 interrupts ↓ 初始化 stack ↓ 设置 PSP ↓ z_prep_c()例如当前 Cortex-M startup 中可以看到bl soc_early_reset_hook以及bl soc_reset_hook之后进行 architecture-specific 初始化、stack 设置等操作。这就是为什么SoC BSP 不只是 Kconfig DTS。SoC 真正启动的时候SOC 层还可能参与CPU Reset ↓ ARCH reset ↓ SOC early reset hook ↓ ARCH setup ↓ SOC reset hook6. soc_early_reset_hook() 是什么这个特别值得你记住。Zephyr 提供soc_early_reset_hook()它允许 SoC 在非常早的启动阶段插入自己的代码。但是这个阶段非常早。官方 architecture porting 文档特别强调这个 hook 甚至可能在正常 stack / RAM 初始化之前执行。所以不能把它当成soc_init()这种普通初始化函数。它更像CPU刚Reset ↓ ARCH刚接管 ↓ SOC_EARLY_RESET_HOOK ↓ 还不能假设完整 C runtime 已经准备好因此这里适合做非常早期的 SoC 硬件准备而不是复杂 driver 初始化7. 然后进入 z_prep_c()这是一个非常重要的分界线。前面reset.S主要是在解决CPU 怎么从 Reset 状态进入 C 世界然后z_prep_c()开始把事情交给更高层。对 Cortex-M 来说z_arm_reset()│ │ bl ▼ z_prep_c()然后再进入z_cstart()所以可以把它理解成Hardware/CPU world │ ▼ z_arm_reset()│ ▼ z_prep_c()│ ▼ Zephyr kernel world │ ▼ z_cstart()8. z_cstart() 才是 Zephyr Kernel Startup 的核心入口当前 Zephyr 的kernel/init.c中有void z_cstart(void)官方源码说明得很直接This routine is invoked when the system is ready to run C code.所以z_arm_reset()解决的是CPU → C而z_cstart()解决的是C → Zephyr Kernel9. z_cstart() 开始做什么简化以后void z_cstart(void){... z_sys_init_run_level(INIT_LEVEL_EARLY);arch_kernel_init();...... switch_to_main_thread(...);}也就是说从这里开始你真正进入Zephyr initialization当前源码中可以看到 z_cstart() 首先进入 early init然后进行 architecture-specific kernel initialization之后最终切换到 main thread。10. 这时候 main() 还没有执行这一点非常重要。整个过程不是z_cstart()↓ main()中间还有z_cstart()↓ kernel initialization ↓ device initialization ↓ thread initialization ↓ create main thread ↓ context switch ↓ main()例如实际调用链中可以看到z_arm_reset()↓ z_prep_c()↓ z_cstart()↓ switch_to_main_thread()↓ arch_switch_to_main_thread()↓ z_thread_entry()↓ main()Zephyr 的实际调试 backtrace 也能看到类似arch_switch_to_main_thread ↓ switch_to_main_thread ↓ z_cstart ↓ z_prep_c ↓ z_arm_reset11. 所以整个 Startup 可以画成这样这是你以后做 BSP 最值得记住的一张图SoC Power-On / Reset │ ▼ CPU Reset Vector │ ▼ ┌──────────────────────┐ │ ARCH startup │ │ │ │ reset.S │ │ │ │ z_arm_reset()│ └──────────┬───────────┘ │ ┌───────────┴───────────┐ │ │ ▼ ▼ soc_early_reset_hook()ARM HW setup │ ▼ soc_reset_hook()│ ▼ Stack / CPU setup │ ▼ z_prep_c()│ ▼ ┌─────────────────┐ │ Zephyr Kernel │ │ │ │ z_cstart()│ └────────┬────────┘ │ ▼ EARLY initialization │ ▼ ARCH kernel init │ ▼ PRE_KERNEL_1 │ ▼ PRE_KERNEL_2 │ ▼ POST_KERNEL │ ▼ APPLICATION │ ▼ main thread │ ▼ 下面是同一过程的 Mermaid 时序图按调用顺序标注了每个阶段所属的层级ARCH / SOC / Kernel mermaid sequenceDiagram autonumber participant HW asSoC 上电 / Resetparticipant ARCH asARCH 层participant SOC asSOC 层participant KERNEL asKernel 层HW-ARCH: CPU Reset Vector Note over ARCH: reset.S ARCH-ARCH: z_arm_reset()ARCH-SOC: soc_early_reset_hook()Note over SOC: 极早期 SoC 硬件准备br/stack/RAM 尚未就绪 SOC--ARCH: 返回 ARCH-ARCH: ARM HW setup / Stack / CPU 初始化 ARCH-SOC: soc_reset_hook()Note over SOC: SoC 复位后处理br/时钟 / 内存 / 安全等 SOC--ARCH: 返回 ARCH-ARCH: z_prep_c()Note over ARCH: CPU → C 世界 ARCH-KERNEL: z_cstart()Note over KERNEL: kernel/init.c KERNEL-KERNEL: EARLY / ARCH kernel init KERNEL-KERNEL: PRE_KERNEL_1 / PRE_KERNEL_2 / POST_KERNEL KERNEL-KERNEL: switch_to_main_thread()KERNEL-KERNEL: arch_switch_to_main_thread()KERNEL-KERNEL: z_thread_entry()KERNEL-KERNEL: main()main()**12\. 那 BOARD 在这里干什么** 这是我们前面 **25 — BOARD / SOC / ARCH** 的关键连接点。 启动时 bash BOARD不会直接执行。实际上是BOARD │ │ 选择 ▼ SOC │ │ 使用 ▼ ARCH │ │ 提供 ▼ Reset/startup code所以BOARD告诉 Zephyr“我要启动哪个具体硬件平台”例如my_board最终选择MY_SOC然后 MY_SOC 对应ARM Cortex-M于是最终进入arch/arm/core/cortex_m/reset.S13. 这就是为什么 ARCH 是 Startup 的真正主人你前面的第 22、25、26 篇已经开始接近这个结论了。三者职责可以这样理解层负责什么BOARD我是什么开发板SOC我是什么芯片/SoCARCHCPU 应该怎么启动KernelZephyr 怎么初始化Driver外设怎么工作所以BOARD ↓ SOC ↓ ARCH ↓ Reset Handler ↓ Kernel其中CPU Reset 以后第一步怎么走主要属于 ARCH。Zephyr 官方 Architecture Porting Guide 也把Early Boot Sequence明确列为 architecture port 的必需部分。14. 如果我们自己做一个新 SoC BSP这时候就非常有用了。假设你有一个新的AcmeSoC-X1CPUCortex-M4你已经有soc/arm/acme/x1/你可能会以为写 Kconfig 写 DTS 写 soc.c就结束了。实际上你首先要问这个 SoC 使用哪个 ARCH startup因为Cortex-M4已经存在arch/arm/core/cortex_m/所以通常AcmeSoC-X1 │ ▼ ARM Cortex-M │ ▼ Zephyr ARM Cortex-M startup │ ▼ z_arm_reset()你不需要自己重新写reset.S除非你的 SoC 有特殊启动要求。15. 那什么时候 SOC 要插手 Startup例如你的 SoC 有Clock Controller PLL Power Controller Memory Controller TCM Cache Security Controller需要在 Zephyr 正常 C/kernel 初始化之前完成某些动作。那么可能出现z_arm_reset()│ ▼ soc_early_reset_hook()│ ├── SoC special reset handling ├── early clock setup ├── memory setup └── security setup │ ▼ 继续 ARCH startup │ ▼ z_prep_c()但必须严格区分ARCH startup和SOC-specific early startup这也是做 BSP 时最容易混乱的地方。16. 为什么 Zephyr 不让 SOC 自己从 Reset 开始因为Cortex-M RISC-V ARC x86 ARM64的 CPU reset 机制完全不同。例如 Cortex-MVector Table ↓ Reset Handler而其他 architecture 的启动方式可能完全不同。因此 Zephyr 把CPU Reset → C这一段放到ARCH里面。官方文档明确说early boot sequence 是 architecture-specific 的不同架构需要完成不同的 reset 后处理然后进入共同的 z_cstart()。于是形成非常漂亮的架构z_cstart()▲ │ ┌───────────┼───────────┐ │ │ │ ARM RISC-V x86 │ │ │ reset.S startup.S startup │ │ │ └───────────┴───────────┘ │ Common Kernel17. 所以 Zephyr Startup 实际上分成两层你可以把它记成第一层Architecture BootReset ↓ ARCH reset ↓ CPU setup ↓ Stack ↓ Memory ↓ SOC early hooks ↓ z_prep_c()负责把 CPU 变成一个可以运行 Zephyr C 代码的 CPU。第二层Kernel Bootz_cstart()↓ EARLY ↓ ARCH kernel init ↓ PRE_KERNEL_1 ↓ PRE_KERNEL_2 ↓ POST_KERNEL ↓ APPLICATION ↓ main()负责把一个能运行 C 的 CPU变成一个正在运行 Zephyr 的系统。18. 对你正在学习的 BSP 来说最重要的一条线把前面的几篇全部串起来Build Time ──────────────────────────────────────── BOARD │ ▼ SOC │ ▼ ARCH │ ▼ CMake Target │ ▼ zephyr.elf Run Time ──────────────────────────────────────── SoC Reset │ ▼ ARCH Reset Vector │ ▼ z_arm_reset()│ ├── SOC early hook │ ├── CPU setup │ ├── Stack setup │ └── SOC reset hook │ ▼ z_prep_c()│ ▼ z_cstart()│ ▼ Kernel Init │ ▼ Device Init │ ▼ Scheduler │ ▼ main()这就把Build Time和Run Time真正连接起来了。19. 以后你拿到一个新 Zephyr SoC第一步怎么查不要先看 main.c。按照这个顺序# 1. 找 ARCHarch/# 2. 找 reset/startuparch/arch/core/# 3. 找 z_cstartkernel/init.c# 4. 找 SoC early hookgrep-Rsoc_early_reset_hooksoc/ arch/# 5. 找 SoC reset hookgrep-Rsoc_reset_hooksoc/ arch/然后重点建立这条 Call GraphReset Vector ↓lt;archgt;reset handler ↓ soc_early_reset_hook()↓lt;archgt;initialization ↓ soc_reset_hook()↓ z_prep_c()↓ z_cstart()↓ main thread ↓ main()20. 最后给你一个BSP 开发者视角的答案如果有人问你“Zephyr 在 SoC 上电以后第一行代码在哪里”不要简单回答kernel/init.c正确的思维应该是SoC Reset │ ▼ CPU Architecture │ ▼ ARCH Reset Handler │ ┌─────┴─────┐ │ │ ▼ ▼ SOC Hook CPU Setup │ │ └─────┬─────┘ ▼ z_prep_c()│ ▼ z_cstart()│ ▼ Zephyr Kernel │ ▼ main()所以第一行不是一个统一的 Zephyr C 函数而是由 ARCH 决定的 reset/startup entry。对于你目前重点研究的Cortex-M SoC BSPReset Vector ↓ arch/arm/core/cortex_m/reset.S ↓ z_arm_reset ↓ z_prep_c()↓ kernel/init.c ↓ z_cstart()这条线就是我们接下来真正进入“Zephyr BSP 启动代码”的入口。
返回列表