深入理解 Zephyr RTOS 启动流程:从上电复位到 main 函数的完整追踪(基于AI协助总结)

从按下复位键到 `main()` 函数被调用,Zephyr RTOS 经历了一段精密的初始化旅程。本文以 STM32H747I-DISCO 开发板为例,逐行解析从硬件复位向量到内核多线程切换的完整链路,涵盖 STM32H7 启动地址机制、向量表布局、`reset.S` 汇编初始化、`z_prep_c()` C 运行时准备、以及内核初始化等级的层层递进。

硬件平台: STM32H747I-DISCO | Zephyr 版本: v4.3.0-dev | 架构: ARM Cortex-M7 (ARMv7-M)

1. 为什么要理解启动流程

理解 Zephyr 的启动流程,意味着你能回答这些问题:

  • CPU 上电后第一条指令从哪里开始执行?0x08000000 这个地址是谁决定的?
  • 为什么有些日志”看不到”——实际上是因为打印太早、串口驱动还没初始化
  • 链接脚本里的 FLASHRAM 区域地址是怎么来的?DTS 的 chosen 节点如何影响链接结果?
  • main() 函数被调用之前,内核已经完成了哪些初始化?驱动的初始化顺序是谁定的?
  • 第一次上下文切换发生在哪个节点?假线程(dummy thread)存在的意义是什么?

本文从硬件复位开始,逐阶段追踪 Zephyr 的启动序列。


2. 全局鸟瞰:从上电到 main 的完整路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
CPU 上电


┌─────────────────────────────────────────────────────────────┐
│ 1. 硬件复位 │
│ 从向量表读取 MSP(主栈指针)和 Reset_Handler 地址 │
│ 向量表位置:0x08000000(STM32 Flash 起始地址) │
└─────────────┬───────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│ 2. z_arm_reset() ← reset.S(纯汇编) │
│ 设置 MSP/PSP,禁用中断,初始化 FPU/MPU │
│ 调用 soc_reset_hook()(SoC 专用初始化,如 TCM ECC 清零) │
└─────────────┬───────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│ 3. z_prep_c() ← prep_c.c(C 语言开始) │
│ 清零 .bss,拷贝 .data,初始化中断控制器 │
└─────────────┬───────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│ 4. z_cstart() ← kernel/init.c(内核核心初始化) │
│ EARLY 等级 → arch_kernel_init → soc_early_init_hook │
│ PRE_KERNEL_1/2 等级 → stack canary → 多线程切换 │
└─────────────┬───────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│ 5. prepare_multithreading() │
│ 初始化调度器,创建 main 线程 + idle 线程 │
└─────────────┬───────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│ 6. switch_to_main_thread() │
│ 首次上下文切换,跳转到 bg_thread_main() │
└─────────────┬───────────────────────────────────────────────┘


┌─────────────────────────────────────────────────────────────┐
│ 7. bg_thread_main() │
│ 执行 POST_KERNEL / APPLICATION 初始化等级 │
│ 调用 main() │
└─────────────────────────────────────────────────────────────┘

3. 第一步:硬件复位 — STM32H7 如何找到第一行代码

3.1 启动地址机制:Option Bytes 与 BOOT 引脚

STM32H747 是双核芯片(Cortex-M7 + Cortex-M4),启动行为由 Option Bytes(选项字节)控制,而非单一的 BOOT 引脚。

Option Byte 不是普通寄存器:存在于 Flash 的特殊区域,掉电保持,硬件上电时自动读取(此时软件尚未运行)。可通过 STM32CubeProgrammer 修改。

三个关键配置

BCM7 / BCM4 — 控制哪个核启动

BCM7 BCM4 行为
1 1 出厂默认:两个核都启动
1 0 只有 M7 启动(M4 可被 M7 软件唤醒)
0 1 只有 M4 启动
0 0 两个核都不自动启动

BCM7_ADD0 / ADD1 — M7 的启动地址

Option Byte 出厂默认值 含义
BCM7_ADD0 0x08000000 Flash(BOOT=0 时使用)
BCM7_ADD1 0x00000000 ITCM-RAM(BOOT=1 时使用)

BOOT 引脚 — 芯片上的物理引脚,选择用 ADD0 还是 ADD1

1
2
BOOT = 0(当前开发板设置,BOOT 脚接地)→ 使用 ADD0 = 0x08000000(从 Flash 启动)
BOOT = 1 → 使用 ADD1 = 0x00000000(从 ITCM-RAM 启动)

完整启动决策流程

1
2
3
4
5
6
7
上电

├─ ① 硬件读取 Option Byte: BCM7=1, BCM4=1 → M7 启动(M4 也启动但无固件)
├─ ② 硬件采样 BOOT 引脚 → BOOT=0 → 使用 BCM7_ADD0
├─ ③ 硬件读取 BCM7_ADD0 = 0x08000000
├─ ④ 硬件将 CPU 取指地址指向 0x08000000
└─ ⑤ 从 0x08000000 读 MSP,从 0x08000004 读 Reset_Handler → CPU 开始执行

3.2 上电时 VTOR 的状态

ARM Cortex-M 架构规定:复位后 VTOR = 0,CPU 理论上从 0x00000000 读取向量表。

STM32H7 的实现:不是简单地把 0x08000000 别名映射到 0x00000000,而是通过 BOOT 引脚 + Option Bytes 机制,硬件直接把 CPU 的初始取指地址指向 BCM7_ADD0 指定的地址(默认 0x08000000)。

1
2
3
4
上电时:硬件直接从 0x08000000 取 MSP 和 Reset_Handler(不经过 VTOR)

▼ z_prep_c() 中显式设置
运行时:SCB->VTOR = 0x08000000 ← 锁定向量表位置,确保后续中断可靠

3.3 向量表:Flash 开头的一张函数指针数组

向量表是 Flash 开头的一张函数指针数组,CPU 上电后第一条指令就从这里找:

1
2
3
4
5
6
7
8
9
10
11
12
Flash: 0x0800_0000
┌──────────────┐
│ 0x0800_0000 │ → MSP 初始值(主栈指针,指向栈顶)
├──────────────┤
│ 0x0800_0004 │ → Reset_Handler(复位入口 = z_arm_reset)
├──────────────┤
│ 0x0800_0008 │ → NMI_Handler
├──────────────┤
│ 0x0800_000C │ → HardFault_Handler
├──────────────┤
│ ... │ → 其他异常和中断
└──────────────┘

关键理解

  • CPU 硬件自动从 0x0800_0000 加载 MSP,从 0x0800_0004 加载 PC
  • STM32H7 通过 SCB->VTOR 寄存器可以在运行时重定位向量表

3.4 向量表地址 0x08000000 的完整传递链

0x08000000 从 DTS 到运行时的 5 层传递:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
① 设备树 DTS:
stm32h747Xi_m7.dtsi:
flash0: flash@8000000 { reg = <0x08000000 DT_SIZE_K(1024)>; }
stm32h747i_disco_m7.dts:
chosen { zephyr,flash = &flash0; }

② CMake 阶段(dts.cmake):
运行 gen_edt.py 解析所有 DTS 文件
→ 生成 edt.pickle(序列化的 Python EDT 对象)

③ Kconfig 函数(kconfigfunctions.py):
调用 dt_chosen_reg_addr_hex("zephyr,flash"):
① edt.chosen_node("zephyr,flash") → 找到 flash0 节点
② flash0.regs[0].addr → 提取 0x08000000
③ hex(0x08000000) → 返回 "0x8000000"

④ Kconfig 定义(arch/Kconfig):
config FLASH_BASE_ADDRESS
default $(dt_chosen_reg_addr_hex,zephyr,flash)
→ CONFIG_FLASH_BASE_ADDRESS = 0x8000000

⑤ 链接脚本(linker.cmd):
__rom_region_start = 0x08000000
_vector_start = .(在 FLASH 最开头)
→ prep_c.c 中: SCB->VTOR = _vector_start

关键机制:CMake 把 DTS 解析成 Python 对象(edt.pickle),Kconfig 求值时通过 Python 函数 dt_chosen_reg_addr_hex() 从中查找 zephyr,flash 指向的节点的 reg 属性,提取出 0x08000000

3.5 链接脚本中向量表段的详细分析

源码位置build/zephyr/linker.cmd

1
2
3
4
5
6
7
8
9
10
11
12
13
14
__rom_region_start = (0x8000000 + 0x0);
rom_start :
{
. = ALIGN(4);
. = ALIGN( 1 << LOG2CEIL(4 * 32) ); ← VTOR 对齐:128 字节
. = ALIGN( 1 << LOG2CEIL(4 * (16 + 150)) ); ← 向量表大小对齐:1024 字节
_vector_start = .;
KEEP(*(.exc_vector_table)) ← 系统异常向量(编译时固定)
KEEP(*(".exc_vector_table.*"))
KEEP(*(.vectors))
_vector_end = .;
KEEP(*(.gnu.linkonce.irq_vector_table*)) ← IRQ 向量(指向 _isr_wrapper)
_vector_end = .;
} > FLASH

ALIGN 为什么不移动位置?

0x08000000 = 二进制末尾有 27 个零,天然满足所有对齐要求:

1
2
3
4
. = ALIGN(4)    → 需要 2 位对齐   ✓ 不动
. = ALIGN(128) → 需要 7 位对齐 ✓ 不动
. = ALIGN(1024) → 需要 10 位对齐 ✓ 不动
→ _vector_start = 0x08000000(精确在 Flash 起始位置)

ALIGN 是安全网——如果起始地址不是 0x08000000(比如从 SRAM 启动),对齐操作会自动调整。

3.6 向量表的实际内容

源码位置zephyr/arch/arm/core/cortex_m/vector_table.S

STM32H7 M7 核是 ARMv7-M Mainline 架构,16 个系统异常全都有

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
偏移    内容
[0] +0x00 z_main_stack + CONFIG_MAIN_STACK_SIZE ← MSP 初始值
[1] +0x04 z_arm_reset ← Reset Handler
[2] +0x08 z_arm_nmi ← NMI
[3] +0x0C z_arm_hard_fault ← HardFault
[4] +0x10 z_arm_mpu_fault ← MemManage
[5] +0x14 z_arm_bus_fault ← BusFault
[6] +0x18 z_arm_usage_fault ← UsageFault
[7] +0x1C 0(SecureFault,需 TrustZone)
[8]-[10] 0(保留)
[11]+0x2C z_arm_svc ← SVCall
[12]+0x30 z_arm_debug_monitor ← Debug Monitor
[13]+0x34 0(保留)
[14]+0x38 z_arm_pendsv ← PendSV(上下文切换)
[15]+0x3C sys_clock_isr ← SysTick(系统时钟)
[16]+0x40 外设 IRQ 0 ... ← 150 个外设中断

3.7 静态向量表 vs 动态 ISR 表

链接脚本中有两种向量段,对应不同的中断处理方式:

段名 内容 可变性
.exc_vector_table 系统异常(Reset, NMI, Fault…) 编译时固定
.gnu.linkonce.irq_vector_table 外设中断(IRQ 0~149) 全部指向 _isr_wrapper

动态分发机制

flowchart LR
    subgraph VT["向量表 (Flash, 固定)"]
        direction TB
        V0["IRQ 0"]
        V1["IRQ 1"]
        V2["IRQ 2"]
        VN["..."]
        V149["IRQ 149"]
    end

    subgraph ISR["ISR 软件表 (RAM, 运行时可改)"]
        direction TB
        S0["[0] spi1_isr"]
        S1["[1] uart1_isr"]
        S2["[2] gpio_isr"]
        SN["..."]
    end

    W["_isr_wrapper()"]
    C["IRQ_CONNECT() 驱动注册"]

    VT --> W
    W -- "查表 _irq_table[irq_num]" --> ISR
    C -. "运行时填充" .-> ISR

动态分发的意义:驱动在运行时用 IRQ_CONNECT() 注册 ISR,而不是编译时写死。这样同一中断号可以接不同驱动,支持动态加载。

3.8 向量表的两级源码实现

vector_table.S 中只有 16 个系统异常,外设中断(IRQ 0~149)的定义在 zephyr/arch/common/isr_tables.c 中:

1
2
3
4
5
6
7
8
9
10
11
// zephyr/arch/common/isr_tables.c:59-83
#ifdef CONFIG_GEN_SW_ISR_TABLE
#define IRQ_VECTOR_TABLE_DEFAULT_ISR _isr_wrapper // 有软件 ISR 表 → 统一指向 _isr_wrapper
#else
#define IRQ_VECTOR_TABLE_DEFAULT_ISR z_irq_spurious // 无软件 ISR 表 → 指向空处理
#endif

// 外设中断向量表:所有 IRQ 统一填充为同一个入口
const uintptr_t __irq_vector_table _irq_vector_table[IRQ_TABLE_SIZE] = {
[0 ...(IRQ_TABLE_SIZE - 1)] = (uintptr_t)&IRQ_VECTOR_TABLE_DEFAULT_ISR,
};

两级向量表的源码分工

源文件 段名 内容 原因
vector_table.S .exc_vector_table 16 个系统异常 每个异常有独立处理函数(hard_fault、svc、pendsv 等),必须逐个定义
isr_tables.c .gnu.linkonce.irq_vector_table 全部 IRQ → _isr_wrapper 本质是同值数组,用 C 的 range initializer [0 ...(N-1)] 一行填充

链接脚本将两个段拼在一起,形成完整的向量表:

1
2
3
KEEP(*(.exc_vector_table))                         ← 系统异常(vector_table.S)
KEEP(*(.vectors))
KEEP(*(.gnu.linkonce.irq_vector_table*)) ← 外设中断(isr_tables.c)

最终 ELF 中是一段连续的函数指针数组:16 个系统异常入口 + 150 个 _isr_wrapper


4. 第二步:z_arm_reset() — 汇编级逐行深度解析

源码位置zephyr/arch/arm/core/cortex_m/reset.S

这是 CPU 执行的第一段用户代码(纯汇编)。以下按 STM32H7(ARMv7-M Mainline)实际执行的路径,逐行详解每条汇编和涉及的系统寄存器。

4.1 完整执行时间线(STM32H7 实际路径)

在逐行分析前,先看 z_arm_reset() 的完整执行流程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
z_arm_reset:

├─ movs r0, #0; msr CONTROL, r0; isb ← 重置 CONTROL
│ 此时:CONTROL=0, 使用MSP, 特权模式

├─ ldr r0, =z_main_stack+size; msr msp, r0 ← 设置 MSP
│ 此时:MSP = 主栈顶部

├─ bl soc_reset_hook ← 清零 TCM(防 ECC)

├─ movs r0, #0; str r0, [MPU_CTRL]; dsb ← 禁用 MPU

├─ bl z_arm_init_arch_hw_at_boot ← 初始化 NVIC、Cache

├─ movs r0, #prio; msr BASEPRI, r0 ← 屏蔽普通中断
│ 此时:中断全禁,只有 NMI/Fault 能触发

├─ ldr r0, =z_interrupt_stacks+size ← 设置 PSP
│ msr PSP, r0
├─ mrs r0, CONTROL; orrs r0, #2; msr CONTROL, r0; isb
│ 此时:CONTROL.SPSEL=1, CPU 使用 PSP

└─ bl z_prep_c ← 跳转至 C 运行时准备

4.2 涉及的 ARM Cortex-M 系统寄存器速查

寄存器 访问方式 位域 作用
CONTROL 特殊寄存器(MRS/MSR) bit[2] FPCA: FP 上下文活跃
bit[1] SPSEL: 0=MSP, 1=PSP
bit[0] nPRIV: 0=特权, 1=非特权
控制栈选择和特权级别
MSP 特殊寄存器(MRS/MSR) 32bit 主栈指针(复位后默认使用)
PSP 特殊寄存器(MRS/MSR) 32bit 进程栈指针(线程使用)
BASEPRI 特殊寄存器(MRS/MSR) bit[7:0] 优先级屏蔽阈值(≥此值的中断被屏蔽)
FAULTMASK 特殊寄存器(MRS/MSR) bit[0] 异常屏蔽(=1 时屏蔽所有可编程异常)
MPU_CTRL 0xE000ED94(存储器映射) bit[0] ENABLE, bit[1] HFNMIENA, bit[2] PRIVDEFENA MPU 总开关

4.3 逐行详解

阶段 1:重置 CONTROL 寄存器

1
2
3
4
/* 第 93-97 行:重置 CONTROL 为默认值 */
movs.n r0, #0 /* r0 = 0 */
msr CONTROL, r0 /* CONTROL = 0 → 使用 MSP,特权模式 */
isb /* 指令同步屏障:确保 CONTROL 写入生效 */

CONTROL 寄存器详解(ARMv7-M,Cortex-M7 有效位域):

1
2
3
4
CONTROL 寄存器(特殊寄存器,通过 MRS/MSR 访问):
bit[0] nPRIV = 0 → 特权模式(可访问所有资源)
bit[1] SPSEL = 0 → 使用 MSP(主栈指针)
bit[2] FPCA = 0 → 无活跃的浮点上下文

重置的原因:这段代码可能被 bootloader 链式加载(chain-loading),此时 CONTROL 可能不是默认值。重置确保从已知状态开始。

isb 是什么? Instruction Synchronization Barrier(指令同步屏障)。写 CONTROL 后必须跟 isb,否则 CPU 流水线可能还在用旧的栈指针。ARM 架构要求:修改 CONTROL 或栈指针后,必须用 isb 确保生效

阶段 2:清零栈限制寄存器

1
2
3
4
/* 第 98-103 行:清零 SPLIM(如果 CPU 支持) */
movs.n r0, #0 /* r0 = 0 */
msr MSPLIM, r0 /* MSP 栈下限 = 0(不限制) */
msr PSPLIM, r0 /* PSP 栈下限 = 0(不限制) */

MSPLIM / PSPLIM 是什么? ARMv8-M 引入的栈溢出检测寄存器。如果 MSP < MSPLIM,触发 Stack Overflow 异常。启动阶段清零 = 暂不检测。

STM32H7 的 Cortex-M7 是 ARMv7-M 架构,没有 SPLIM 寄存器,所以这段代码实际不编译(被 #if defined(CONFIG_CPU_CORTEX_M_HAS_SPLIM) 排除)。ARMv8-M 架构的处理器(如 Cortex-M33/M55)才有此寄存器。

阶段 3:设置 MSP 到主栈

1
2
3
/* 第 147-148 行:设置 MSP */
ldr r0, =z_main_stack + CONFIG_MAIN_STACK_SIZE /* r0 = 主栈顶部地址 */
msr msp, r0 /* MSP = 主栈顶部 */

ldr r0, =xxx 是伪指令,把 32 位常量加载到 r0。z_main_stack 是链接脚本中定义的符号,CONFIG_MAIN_STACK_SIZE 通常为 1024 或 2048。

使用 MSP 的原因:此时 CONTROL.SPSEL=0,CPU 还在使用 MSP。后续代码需要 MSP 来执行 soc_reset_hook() 等函数调用(函数调用需要栈来保存返回地址)。

阶段 4:调用 SoC 复位钩子

1
2
/* 第 162-164 行:调用 STM32H7 专用复位函数 */
bl soc_reset_hook /* 跳转并保存返回地址到 LR */

bl = Branch with Link:跳转到 soc_reset_hook,同时把下一条指令的地址保存到 LR(R14),函数返回时 bx lr 跳回来。

soc_reset_hook() 做了什么? 以 STM32H7 为例,清零 TCM 内存(防 ECC 错误):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// zephyr/soc/st/stm32/stm32h7x/soc_m7.c
// 地址和大小从 Device Tree chosen 节点获取,非硬编码
#define ITCM_BASE DT_REG_ADDR(DT_CHOSEN(zephyr_itcm)) // → 0x00000000
#define ITCM_END (ITCM_BASE + DT_REG_SIZE(DT_CHOSEN(zephyr_itcm))) // → 0x00010000 (64KB)
#define DTCM_BASE DT_REG_ADDR(DT_CHOSEN(zephyr_dtcm)) // → 0x20000000
#define DTCM_END (DTCM_BASE + DT_REG_SIZE(DT_CHOSEN(zephyr_dtcm))) // → 0x20020000 (128KB)

void soc_reset_hook(void)
{
// 清零 ITCM — volatile 64位对齐写入(防止编译器优化掉清零操作)
#ifdef ITCM_BASE
BUILD_ASSERT(ITCM_BASE % sizeof(uint64_t) == 0, "ITCM must be 64-bit aligned");
for (volatile uint64_t *p = (volatile uint64_t *)ITCM_BASE;
p < (volatile uint64_t *)ITCM_END; p++) { *p = 0; }
#endif

// 清零 DTCM — volatile 32位对齐写入
#ifdef DTCM_BASE
BUILD_ASSERT(DTCM_BASE % sizeof(uint32_t) == 0, "DTCM must be 32-bit aligned");
for (volatile uint32_t *p = (volatile uint32_t *)DTCM_BASE;
p < (volatile uint32_t *)DTCM_END; p++) { *p = 0; }
#endif
}

关键设计:地址和大小从 Device Tree 的 zephyr_itcm / zephyr_dtcm chosen 节点获取,同一 SoC 的不同变体(ITCM 大小不同)无需改代码。BUILD_ASSERT 在编译时验证对齐要求。volatile 防止编译器将清零循环优化掉。

STM32H7 需要清零 TCM 是因为其上电后内容未定义,访问会触发 ECC 错误 → HardFault(参考 ST AN5342 Rev.7 §3.1.1)。其他 SoC 可能在此执行 PLL 预配置、看门狗关闭等操作。

阶段 5:禁用 MPU

1
2
3
4
5
/* 第 167-173 行:关闭 MPU */
movs.n r0, #0 /* r0 = 0 */
ldr r1, =_SCS_MPU_CTRL /* r1 = 0xE000ED94 (MPU_CTRL 地址) */
str r0, [r1] /* MPU_CTRL = 0 → 禁用 MPU */
dsb /* 数据同步屏障 */

MPU_CTRL 寄存器详解

1
2
3
4
MPU_CTRL (0xE000ED94):
bit[0] ENABLE = 0 → MPU 禁用
bit[1] HFNMIENA = 0 → HardFault/NMI 时禁用 MPU
bit[2] PRIVDEFENA = 0 → 特权模式默认映射禁用

先禁用 MPU 的原因:后续要设置栈指针、拷贝数据,如果 MPU 配置了内存保护,可能会阻止这些操作。先禁用,等内核初始化完成后再配置并启用。

dsb 是什么? Data Synchronization Barrier(数据同步屏障)。确保 MPU_CTRL 写入到达硬件后再继续。

阶段 6:初始化架构硬件

1
2
/* 第 176 行 */
bl z_arm_init_arch_hw_at_boot /* 初始化 NVIC、MPU、Cache */

源码位置zephyr/arch/arm/core/cortex_m/scb.c(93-155 行)

这个函数把 CPU 内部所有硬件状态重置到已知值,防止前一个固件(bootloader)留下残余配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
void z_arm_init_arch_hw_at_boot(void)
{
__disable_irq(); // 1. 禁用全局中断
__set_FAULTMASK(0); // 2. 清除 Fault 屏蔽

z_arm_clear_arm_mpu_config(); // 3. 清除所有 MPU 区域配置

// 4. 禁用所有 NVIC 中断
for (i = 0; i < ARRAY_SIZE(NVIC->ICER); i++)
NVIC->ICER[i] = 0xFFFFFFFF; // ICER: Interrupt Clear Enable Register

// 5. 清除所有 NVIC 挂起中断
for (i = 0; i < ARRAY_SIZE(NVIC->ICPR); i++)
NVIC->ICPR[i] = 0xFFFFFFFF; // ICPR: Interrupt Clear Pending Register

// 6. 处理 Cache 状态
if (SCB->CCR & SCB_CCR_DC_Msk)
SCB_DisableDCache(); // D-Cache 已启用 → 先清空再禁用
else
SCB_InvalidateDCache(); // D-Cache 未启用 → 直接无效化
SCB_DisableICache(); // 禁用 I-Cache

__enable_irq(); // 7. 恢复中断
barrier_dsync_fence_full(); // 8. 数据屏障
barrier_isync_fence_full(); // 9. 指令屏障
}

涉及的寄存器详解

步骤 寄存器 访问方式 操作 目的
2 FAULTMASK 特殊寄存器(MRS/MSR) 写 0 允许所有 Fault 触发
3 MPU_RNR/RBAR/RASR 0xE000ED98..(存储器映射) 清除所有区域 MPU 回到空白状态
4 NVIC->ICER[n] 0xE000E180..(存储器映射) 写全 1 禁用所有外设中断
5 NVIC->ICPR[n] 0xE000E280..(存储器映射) 写全 1 清除所有挂起中断
6 SCB->CCR 0xE000ED14(存储器映射) 检查 DC 位 根据当前状态决定如何处理 Cache

NVIC 寄存器特性ICER(Interrupt Clear Enable)写 1 禁用对应中断,ICPR(Interrupt Clear Pending)写 1 清除挂起状态。这是 ARM 的”写 1 清除/设置”模式——读和写的含义不同。

阶段 7:禁用普通中断(保留 Fault)

1
2
3
/* 第 182-184 行(ARMv7-M 路径) */
movs.n r0, #_EXC_IRQ_DEFAULT_PRIO /* r0 = 优先级阈值 */
msr BASEPRI, r0 /* BASEPRI = 阈值 → 屏蔽普通中断 */

ARM Cortex-M 三级优先级体系

层级 优先级 异常 BASEPRI 能屏蔽?
固定(不可屏蔽) -3 Reset 永远不能
固定(不可屏蔽) -2 NMI(不可屏蔽中断) 永远不能
固定(不可屏蔽) -1 HardFault 永远不能
可编程 Fault 0x00(默认) MemManage / BusFault / UsageFault 不屏蔽(< 0x10)
可编程系统 0x00(默认) SVCall 不屏蔽(< 0x10)
普通外设 0x10~0xF0 UART, SPI, DMA, TIM… 全部屏蔽
系统 0xFF PendSV, SysTick 全部屏蔽

BASEPRI 寄存器详解(特殊寄存器,通过 MRS/MSR 访问):

1
2
3
规则:屏蔽所有优先级 ≥ 写入值的中断
写入 0x10 → 屏蔽 ≥ 0x10 的中断,保留 < 0x10 的
写入 0x00 → 不屏蔽任何中断(全部允许)

_EXC_IRQ_DEFAULT_PRIO 的推导

1
2
3
4
5
6
7
8
9
10
11
// DTS 定义: stm32h7.dtsi → NUM_IRQ_PRIO_BITS = 4(STM32H7 用 4 位优先级)

// exception.h 计算链:
_EXCEPTION_RESERVED_PRIO = 1 // ARMv7-M 为 Fault 保留 1 级
_EXC_SVC_PRIO = 0 // 未启用 ZERO_LATENCY_IRQS
_IRQ_PRIO_OFFSET = 1 + 0 = 1

Z_EXC_PRIO(pri) = (pri << (8 - NUM_IRQ_PRIO_BITS)) & 0xFF
= (pri << 4) & 0xFF // 左移 4 位,填到高半字节

_EXC_IRQ_DEFAULT_PRIO = Z_EXC_PRIO(1) = (1 << 4) = 0x10

优先级寄存器的位布局:STM32H7 只有 4 位有效优先级,放在 8 位寄存器的高 4 位:

1
2
3
4
5
6
7
8
硬件 8 位优先级寄存器:
bit[7:4] = 有效位 bit[3:0] = 始终为 0

逻辑优先级 0 → 0b0001_0000 = 0x10 ← Fault 用
逻辑优先级 1 → 0b0001_0000 = 0x10 ← _EXC_IRQ_DEFAULT_PRIO
逻辑优先级 2 → 0b0010_0000 = 0x20
...
逻辑优先级 15 → 0b1111_0000 = 0xF0 ← 最低

BASEPRI=0x10 后的效果

1
2
3
4
5
6
7
能触发: Reset, NMI, HardFault(固定优先级,BASEPRI 管不了)
能触发: MemManage, BusFault, UsageFault, SVC(优先级 0x00 < 0x10)
被屏蔽: 所有外设中断(UART, SPI, TIM...)、PendSV、SysTick(≥ 0x10)

设计原因: 启动阶段中断全关,但 Fault 必须能触发
如果启动代码有 bug(如非法地址访问),Fault 能捕获
否则系统会无声死锁

阶段 8:切换到 PSP(进程栈指针)

1
2
3
4
5
/* 第 213-226 行 */
ldr r0, =z_interrupt_stacks /* r0 = 中断栈起始 */
ldr r1, =CONFIG_ISR_STACK_SIZE + MPU_GUARD_ALIGN_AND_SIZE /* r1 = 栈大小 */
adds r0, r0, r1 /* r0 = 中断栈顶部 */
msr PSP, r0 /* PSP = 中断栈顶部 */
1
2
3
4
5
6
7
mrs r0, CONTROL                    /* 读取 CONTROL 当前值 */
movs r1, #(2 | CONTROL_ARM_PAC_MASK | CONTROL_ARM_BTI_MASK)
/* Cortex-M7: PAC/BTI 未启用,两个 MASK = 0 */
/* 实际等价于 movs r1, #2(只设 SPSEL 位) */
orrs r0, r1 /* r0 |= 0x02 → 设置 SPSEL 位 */
msr CONTROL, r0 /* CONTROL.SPSEL = 1 → 切换到 PSP */
isb /* 同步屏障:确保栈切换生效 */

CONTROL.SPSEL 的含义

1
2
3
4
5
6
7
CONTROL 寄存器:
bit[1] SPSEL = 0 → CPU 使用 MSP(复位默认)
bit[1] SPSEL = 1 → CPU 使用 PSP(线程模式)

这一步之后:
MSP → 空出来,后续设为中断栈
PSP → 当前代码使用,指向 interrupt_stack 顶部

mrs / msr 而非 movs 的原因:CONTROL 是特殊寄存器,必须通过 mrs(读)和 msr(写)访问,不能用普通 mov 指令。

阶段 9:跳转到 C 语言

1
2
/* 第 233 行 */
bl z_prep_c /* 跳转到 C 运行时准备函数 */

bl = Branch with Link,跳转并把返回地址存入 LR。但 z_prep_c() 声明为 FUNC_NORETURN,永远不会返回。

4.4 三个屏障指令的区别

指令 全称 作用 何时使用
isb Instruction Synchronization Barrier 刷新流水线,确保后续指令看到之前的系统寄存器修改 修改 CONTROL、栈指针后
dsb Data Synchronization Barrier 等待所有显式内存访问完成 修改 MPU 配置后
dmb Data Memory Barrier 确保内存操作顺序 多核共享内存时

区分要点isb 作用于 CPU 流水线,dsb 等待内存总线写入完成,dmb 保证内存操作顺序。


5. 第三步:z_prep_c() — C 运行时环境准备

源码位置zephyr/arch/arm/core/cortex_m/prep_c.c

进入 C 语言世界前,需要建立 C 程序的基本假设:.bss 清零、.data 拷贝、中断控制器就绪。

1
2
3
4
5
6
7
8
9
10
11
z_prep_c()

├─ 1. soc_prep_hook() ← SoC 级预备(可选)
├─ 2. relocate_vector_table() ← 设置 SCB->VTOR
├─ 3. z_arm_floating_point_init() ← FPU 初始化
├─ 4. arch_bss_zero() ← 清零 .bss 段
├─ 5. arch_data_copy() ← 拷贝 .data 段(Flash → RAM)
├─ 6. z_arm_interrupt_init() ← NVIC 优先级初始化
├─ 7. arch_cache_init() ← Cache 初始化(Cortex-M 为空函数)
├─ 8. [可选] 空指针检测 ← DWT 空指针异常检测(默认关闭)
└─ 9. z_cstart() ← 进入内核核心初始化

5.1 relocate_vector_table() — 向量表重定位

1
2
3
4
5
6
7
8
9
10
11
12
void __weak relocate_vector_table(void)
{
#ifdef CONFIG_SRAM_VECTOR_TABLE
// 如果配置了 SRAM 向量表:拷贝向量表到 RAM(更快响应中断)
size_t vector_size = (size_t)_vector_end - (size_t)_vector_start;
arch_early_memcpy(_sram_vector_start, _vector_start, vector_size);
#endif
// 核心操作:设置 VTOR 指向向量表地址
SCB->VTOR = VECTOR_ADDRESS & VTOR_MASK; // = 0x08000000
barrier_dsync_fence_full(); // 确保写入到达硬件
barrier_isync_fence_full(); // 刷新流水线
}

__weak 的含义:弱符号——SoC 可以提供一个强符号版本来覆盖此函数。

VTOR 寄存器0xE000ED08):

1
2
3
bit[31:7]  = 向量表基地址(0x08000000 对齐到 128 字节)
bit[6:0] = 保留(必须为 0)
VTOR_MASK = SCB_VTOR_TBLOFF_Msk ← 屏蔽掉低 7 位

5.2 z_arm_floating_point_init() — FPU 初始化

STM32H7 有双精度浮点单元(FPU),需要配置 CPACR 和 FPCCR 两个寄存器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static inline void z_arm_floating_point_init(void)
{
// 1. 先禁用 FPU 访问(清除之前的配置)
SCB->CPACR &= (~(CPACR_CP10_Msk | CPACR_CP11_Msk));

#if defined(CONFIG_FPU) // 项目中启用了 FPU
// 2. 启用 CP10/CP11 协处理器(FPU 寄存器访问)
SCB->CPACR |= CPACR_CP10_PRIV_ACCESS | CPACR_CP11_PRIV_ACCESS;

// 3. 配置浮点上下文保存策略
FPU->FPCCR = FPU_FPCCR_ASPEN_Msk | FPU_FPCCR_LSPEN_Msk;
// ASPEN = 自动保存:使用 FP 指令时自动设 FPCA 位
// LSPEN = 延迟保存:中断进入时不立即保存 FP 寄存器,用到时才保存
// → 减少中断延迟,节省栈空间

__set_FPSCR(0);
#endif
}

延迟保存(Lazy Stacking)的意义

Cortex-M7 FPU 有 S0-S31 共 32 个单精度寄存器和 1 个 FPSCR。但硬件异常入口只保存其中一半:

1
2
3
4
5
6
7
硬件自动保存: S0-S15 + FPSCR + reserved = 18 words (72 字节)
软件保存: S16-S31(callee-saved,仅上下文切换时由软件保存)

无延迟保存: 中断进入时硬件立即写入 S0-S15 + FPSCR = 72 字节,需要数十个时钟周期
有延迟保存: 中断进入时硬件只预留空间,不实际写入
→ 如果 ISR 不用 FP,完全省去保存开销
→ 如果 ISR 用 FP,在第一次 FP 指令时才触发实际保存

5.3 arch_bss_zero() — .bss 段清零

源码位置zephyr/arch/common/init.c(50-81 行)

1
2
3
4
5
6
7
8
9
10
11
__boot_func void arch_bss_zero(void)
{
// 主 .bss 段清零(AXI SRAM 中)
arch_early_memset(__bss_start, 0, __bss_end - __bss_start);

// DTCM .bss 段清零(如果有)
#if DT_NODE_HAS_STATUS_OKAY(DT_CHOSEN(zephyr_dtcm))
arch_early_memset(&__dtcm_bss_start, 0,
(uintptr_t)&__dtcm_bss_end - (uintptr_t)&__dtcm_bss_start);
#endif
}

__bss_start__bss_end 是链接脚本提供的符号,标记 .bss 段的起止地址。

5.4 arch_data_copy() — .data 段拷贝

1
2
3
4
5
6
7
8
9
10
11
12
void arch_data_copy(void)
{
// 主 .data 段:从 Flash 拷贝到 RAM
arch_early_memcpy(&__data_region_start, &__data_region_load_start,
__data_region_end - __data_region_start);

// DTCM 数据段(如果有)
#if DT_NODE_HAS_STATUS_OKAY(DT_CHOSEN(zephyr_dtcm))
arch_early_memcpy(&__dtcm_data_start, &__dtcm_data_load_start,
__dtcm_data_end - __dtcm_data_start);
#endif
}

涉及的符号(全部由链接脚本定义):

符号 含义
__data_region_start RAM 中 .data 段的起始地址(目标)
__data_region_load_start Flash 中 .data 段初始值的起始地址(源)
__data_region_end RAM 中 .data 段的结束地址
1
2
3
4
5
6
Flash                        RAM
┌─────────────────┐ ┌─────────────────┐
│ __data_load_start│ ──copy→ │ __data_start │
│ (.data 初始值) │ │ (.data 运行时) │
│ │ │ __data_end │
└─────────────────┘ └─────────────────┘

5.5 z_arm_interrupt_init() — NVIC 优先级初始化

1
2
3
4
5
6
7
8
9
10
void z_arm_interrupt_init(void)
{
int irq = 0;
// 遍历所有 IRQ,设置默认优先级
for (; irq < CONFIG_NUM_IRQS; irq++) {
NVIC_SetPriority((IRQn_Type)irq, _IRQ_PRIO_OFFSET);
// _IRQ_PRIO_OFFSET = 1(逻辑优先级)
// 经 Z_EXC_PRIO 转换后 = 0x10(硬件值)
}
}

效果:所有 150 个外设中断的默认优先级设为 0x10(第二高优先级)。驱动初始化时可通过 IRQ_CONNECT() 覆盖。


6. 第四步:z_cstart() — 内核核心初始化

源码位置zephyr/kernel/init.c(538-610 行)

这是 Zephyr 内核真正的”启动管理器”,负责按优先级执行所有初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
z_cstart()                                         ← kernel/init.c:538

├─ 1. gcov_static_init() ← 代码覆盖率初始化(调试用)

├─ 2. z_sys_init_run_level(INIT_LEVEL_EARLY) ← 最早期的设备初始化

├─ 3. arch_kernel_init() ← 架构相关初始化(MPU 等)

├─ 4. LOG_CORE_INIT() ← 日志子系统核心初始化

├─ 5. z_dummy_thread_init() ← 创建"假线程"(启动期占位)

├─ 6. z_device_state_init() ← 静态设备状态初始化

├─ 7. soc_early_init_hook() ← SoC 早期初始化(Cache、电源)

├─ 8. board_early_init_hook() ← 板级早期初始化(可选)

├─ 9. z_sys_init_run_level(INIT_LEVEL_PRE_KERNEL_1) ← 内核前初始化 第一阶段

├─ 10. z_sys_init_run_level(INIT_LEVEL_PRE_KERNEL_2) ← 内核前初始化 第二阶段

├─ 11. stack canary 初始化 ← 栈溢出保护(随机数填充)

├─ 12. [可选] timing_init() + timing_start() ← 性能计时初始化

└─ 13. switch_to_main_thread(prepare_multithreading()) ← 多线程就绪

└─ 首次上下文切换 → bg_thread_main()

6.1 gcov_static_init() — 代码覆盖率初始化

1
2
/* init.c:541 */
gcov_static_init();

遍历 GCC 的 __init_array 段(存放全局构造函数指针),逐个调用。仅在启用 CONFIG_COVERAGE 时有实际代码,正常构建中是空内联函数,编译器直接优化掉,零开销

6.2 z_sys_init_run_level() — 初始化等级执行引擎

这是 z_cstart 中被调用多次的核心函数,也是理解 Zephyr 驱动初始化秩序的关键。

6.2.1 init_entry 结构体与两条注册路径

1
2
3
4
5
// zephyr/include/zephyr/init.h
struct init_entry {
int (*init_fn)(void); // 初始化函数指针(SYS_INIT 时有值)
const struct device *dev; // 关联的设备(DEVICE_DT_DEFINE 时有值)
};

两个字段互斥:要么 init_fn != NULL(纯函数),要么 dev != NULL(设备驱动)。

路径一:设备驱动 — DEVICE_DT_DEFINE()

每个 DTS 中 status = "okay" 的设备节点,驱动用此宏注册:

1
2
3
4
5
6
7
8
9
10
11
12
// 驱动源码示例:zephyr/drivers/interrupt_controller/intc_exti_stm32.c
DEVICE_DT_DEFINE(STM32_EXTI_INTCELL,
stm32_exti_gpio_intc_init, // init 函数
NULL, NULL, &exti_config,
PRE_KERNEL_1, // ← 等级
CONFIG_INTC_INIT_PRIORITY, // ← 优先级
NULL);

// 宏展开后放入命名段:
static const struct init_entry __init___device_dts_ord_100
__attribute__((section(".z_init_PRE_KERNEL_1_P_40_SUB_0_")))
__used = { .init_fn = NULL, .dev = &__device_dts_ord_100 };

路径二:纯函数 — SYS_INIT()

不需要设备结构体的初始化,直接注册函数:

1
2
3
4
5
6
7
// 驱动源码示例:zephyr/drivers/console/uart_console.c
SYS_INIT(uart_console_init, PRE_KERNEL_1, CONFIG_UART_CONSOLE_INIT_PRIORITY);

// 宏展开后:
static const struct init_entry __init_uart_console_init
__attribute__((section(".z_init_PRE_KERNEL_1_P_60_SUB_0_")))
__used = { .init_fn = uart_console_init, .dev = NULL };

6.2.2 核心机制:编译时排序 + 运行时遍历

1
2
3
4
5
6
7
8
9
源码阶段                          链接阶段                     运行时
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 驱动 A: │ │ 链接脚本按段 │ │ z_sys_init │
│ SYS_INIT() │ ──编译器──→ │ 名排序: │ ──运行──→ │ _run_level() │
│ 或 │ 放入命名段 │ │ 遍历数组 │ 逐个调用 │
│ 驱动 B: │ │ .z_init_ │ │ │
│ DEVICE_ │ │ PRE_KERNEL_1│ │ entry[0]() │
│ DT_DEFINE() │ │ _P_40_SUB_0 │ │ entry[1]() │
└──────────────┘ └──────────────┘ └──────────────┘

运行时只是简单的数组遍历,所有排序逻辑在编译/链接时已完成

6.2.3 链接脚本排序原理

段名 .z_init_{LEVEL}_P_{PRIO}_SUB_{SUB}_ 中的数字决定了排序:

1
2
3
4
5
.z_init_PRE_KERNEL_1_P_0_SUB_0_    ← RCC 时钟(优先级 0)
.z_init_PRE_KERNEL_1_P_40_SUB_0_ ← EXTI(优先级 40)
.z_init_PRE_KERNEL_1_P_60_SUB_0_ ← UART 控制台(优先级 60)
.z_init_PRE_KERNEL_2_P_0_SUB_0_ ← 系统时钟驱动
.z_init_POST_KERNEL_P_70_SUB_0_ ← I2C(POST_KERNEL,优先级 70)

链接脚本用 SORT 按段名字母顺序排列。因为段名中嵌入了等级和优先级数字,排序后的物理内存布局自然就是正确的执行顺序。

6.2.4 运行时执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static void z_sys_init_run_level(enum init_level level)
{
// levels[] 存放每个等级的起始地址(链接脚本定义的边界符号)
static const struct init_entry *levels[] = {
__init_EARLY_start,
__init_PRE_KERNEL_1_start,
__init_PRE_KERNEL_2_start,
__init_POST_KERNEL_start,
__init_APPLICATION_start,
__init_end,
};

// 遍历 [levels[level], levels[level+1]) 区间
for (entry = levels[level]; entry < levels[level+1]; entry++) {
if (entry->dev != NULL) {
// 设备驱动路径 → 调用 dev->config->init_fn(dev)
result = do_device_init(entry->dev);
} else {
// 纯函数路径 → 直接调用
result = entry->init_fn();
}
}
}

运行时逻辑极简:根据等级找到对应的数组区间,逐个调用。区分设备驱动和纯函数两条路径。

6.2.5 STM32H7 的典型初始化序列

以 STM32H747I-DISCO 板为例,通过 nm build/zephyr/zephyr.elf | grep __init_ | sort 可以看到各等级的初始化内容:

EARLY:通常为空(极少需要如此早期的初始化)

PRE_KERNEL_1:基础硬件

设备 说明
RCC 时钟控制器 所有外设的时钟源,必须最先初始化
STM32 时钟配置 配置 PLL、系统时钟树
内存 slab 内核内存管理基础设施
Reset Controller 硬件复位控制
GPIOA/B/C/… 引脚控制器(I2C/SPI/UART 等外设需要 GPIO)
EXTI 中断控制器 外部中断路由
DMA 直接内存访问
EXTI GPIO 中断 GPIO 到 EXTI 的中断映射
UART 串口 调试串口
串口控制台 printk 输出从此开始可用

PRE_KERNEL_2:系统时钟

设备 说明
sys_clock_driver 系统节拍定时器(Cortex-M SysTick 或 STM32 TIM)

POST_KERNEL:外设驱动

设备 说明
日志后端 日志输出初始化
系统工作队列 内核异步工作队列
SPI/I2C/USB 等总线驱动 具体外设取决于板级配置
磁盘/Flash 存储设备

依赖关系示例

1
2
3
4
5
6
RCC 时钟 (prio 0)
└→ GPIO (prio 高于 EXTI)
└→ EXTI
└→ DMA
└→ UART
└→ 串口控制台 (prio 最低)

RCC 必须最先:所有外设寄存器需要时钟才能响应。UART 在控制台之前:控制台依赖 UART 驱动就绪。

日志”消失”的原因:PRE_KERNEL_1 阶段初期,串口控制台还没初始化,printk() 输出被丢弃。串口控制台初始化之后,printk() 才真正输出到串口。

6.3 arch_kernel_init() — ARM Cortex-M 架构初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
static ALWAYS_INLINE void arch_kernel_init(void)
{
z_arm_interrupt_stack_setup(); // 1. 设置中断栈(MSP → interrupt_stack)
z_arm_exc_setup(); // 2. 配置异常优先级
z_arm_fault_init(); // 3. 启用 Fault 异常
z_arm_cpu_idle_init(); // 4. 配置低功耗行为
z_arm_clear_faults(); // 5. 清除残留 Fault 状态
#if defined(CONFIG_ARM_MPU)
z_arm_mpu_init(); // 6. 初始化 MPU
z_arm_configure_static_mpu_regions(); // 7. 配置静态 MPU 区域
#endif
soc_per_core_init_hook(); // 8. SoC 每核初始化钩子
}
步骤 函数 做了什么 为什么
1 z_arm_interrupt_stack_setup() MSP = interrupt_stack 栈顶 让中断处理使用专用栈(与线程栈分离)
2 z_arm_exc_setup() 设置 PendSV=0xFF(最低优先级),SVC=0x00;设置 Fault 优先级(MemManage/BusFault/UsageFault=0);SHCSR 启用这三个 Fault PendSV 用于上下文切换必须在最低优先级;Fault 必须能捕获启动阶段的错误
3 z_arm_fault_init() CCR 寄存器:启用 DIV_0_TRP(除零陷阱);配置 UNALIGN_TRP(非对齐访问) 让 CPU 捕获除零和非对齐等编程错误
4 z_arm_cpu_idle_init() SCB->SCR = SCB_SCR_SEVONPEND_Msk(赋值,非 OR) 中断挂起时产生事件,唤醒 WFE 等待的 CPU
5 z_arm_clear_faults() 清除 CFSR、HFSR 等 Fault 状态寄存器(写 1 清除) 防止启动阶段的残余 Fault 标志影响运行时
6-7 MPU 初始化 配置 MPU 区域(Flash、RAM、外设等)(#ifdef CONFIG_ARM_MPU 内存保护
8 soc_per_core_init_hook() SoC 每核初始化钩子(可选,默认空操作) 多核场景下每个核的独立初始化

6.4 LOG_CORE_INIT() — 日志子系统核心初始化

初始化日志缓冲区、默认时间戳函数、运行时过滤级别、并遍历初始化所有后端(UART、RTT 等)。放在 arch_kernel_init() 之后(需要硬件计数器支持时间戳)、PRE_KERNEL_1 之前(驱动初始化过程中需要用 LOG_INF() 等宏)。

6.5 z_dummy_thread_init() — 假线程占位

内核中大量代码通过 _current(当前线程指针)获取上下文信息。此时调度器尚未就绪,没有真正的线程,但 _current 不能为 NULL(解引用会触发 HardFault)。假线程将 _current 指向一个标记为 _THREAD_DUMMY 的结构体,作为安全占位指针。

假线程从 z_cstart() 创建,到 switch_to_main_thread() 首次上下文切换后丢弃,永远不会被调度。详见第 7.3 节。

6.6 z_device_state_init() — 设备对象初始化

1
2
3
4
5
6
static void z_device_state_init(void)
{
STRUCT_SECTION_FOREACH(device, dev) {
k_object_init(dev);
}
}

遍历所有静态定义的 device 结构体(由驱动 DEVICE_DT_DEFINE() 宏创建),标记为已注册。在驱动实际初始化(PRE_KERNEL_1 阶段)之前完成,确保 device_is_ready() 等函数可用。

6.7 soc_early_init_hook() — STM32H7 SoC 早期初始化

源码位置zephyr/soc/st/stm32/stm32h7x/soc_m7.c(150-171 行)

调用位置z_cstart()init.c:557(在 EARLY 等级之后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void soc_early_init_hook(void)
{
// 1. 启用 I-Cache 和 D-Cache
sys_cache_instr_enable(); // I-Cache 开启(代码执行加速)
sys_cache_data_enable(); // D-Cache 开启(数据访问加速)

// 2. 设置系统时钟默认值
SystemCoreClock = 64000000; // HSI 默认 64MHz

// 3. 配置电源方案
LL_PWR_ConfigSupply(SELECTED_POWER_SUPPLY); // LDO 或 SMPS

// 4. Errata 修复(RevY 芯片的 AXI SRAM 读错误)
if (LL_DBGMCU_GetRevisionID() == 0x1003) {
stm32_reg_set_bits(&GPV->AXI_TARG7_FN_MOD, 0x1);
}
}

时序约束:必须放在 EARLY 之后、PRE_KERNEL_1 之前。

1
2
3
4
5
6
7
时序设计:
z_arm_init_arch_hw_at_boot() ← Cache 被禁用
arch_data_copy() ← Flash → RAM 拷贝(需要 Cache 禁用)
z_sys_init_run_level(EARLY) ← 极少数早期驱动
soc_early_init_hook() ← Cache 在此启用
z_sys_init_run_level(PRE_KERNEL_1) ← 后续驱动使用 Cache 加速
z_sys_init_run_level(PRE_KERNEL_1) ← 后续驱动享受 Cache 加速

Cache 必须在 .data 拷贝和 .bss 清零之后才能启用,原因如下:

如果 D-Cache 在 .data 拷贝之前就启用:

1
2
3
4
5
6
1. .data 区域 RAM 中还是上电垃圾值
2. 任何代码读了该地址 → D-Cache 把垃圾缓存进来
3. .data 拷贝写入正确值 → 走 Cache write-back 路径
CPU 写入 → Cache 中更新 ✓,但 RAM 中还是旧值

结果:Cache [正确值] RAM [垃圾值] ← 数据不一致

D-Cache 关闭时执行拷贝,CPU 写入直达 RAM(无 Cache 中间层)。完成后启用 D-Cache,Cache 从空开始,只缓存正确的数据。

6.8 board_early_init_hook() — 板级早期初始化

默认行为:空宏(do { } while (0)),需要板级代码定义 CONFIG_BOARD_EARLY_INIT_HOOK 并实现函数才会生效。典型用途:配置板级特定的电压调节器、时钟树、GPIO 引脚复用等。

soc_early_init_hook 的区别:

soc_early_init_hook board_early_init_hook
实现者 SoC 系列(所有同 SoC 的板共用) 具体板级(同一 SoC 的不同板可不同)
位置 soc/st/stm32/stm32h7x/soc_m7.c boards/st/stm32h747i_disco/
典型内容 Cache、电源方案、Errata 板级电源芯片、特殊 GPIO 配置

6.9 Stack Canary 初始化 — 栈溢出保护

1
2
3
4
5
6
7
/* init.c:568-574 */
#ifdef CONFIG_REQUIRES_STACK_CANARIES
uintptr_t stack_guard;
z_early_rand_get((uint8_t *)&stack_guard, sizeof(stack_guard));
__stack_chk_guard = stack_guard;
__stack_chk_guard <<= 8;
#endif

启用传递链prj.conf: CONFIG_STACK_CANARIES=y → CMake → GCC -fstack-protector 编译选项。

机制:编译器在受保护函数的栈帧中插入”金丝雀”值(canary),函数返回前比较栈上副本与全局变量 __stack_chk_guard 中的原件。栈溢出会覆盖栈上副本,比较失败时触发 __stack_chk_fail() → 系统终止。

1
2
3
4
5
6
7
8
9
10
11
12
13
栈帧布局(受保护函数):
高地址
┌──────────────────────┐
│ 返回地址 (LR) │
├──────────────────────┤
│ 保存的 R7 │
├──────────────────────┤
│ CANARY 副本 │ ← 函数入口从 __stack_chk_guard 复制
│ (0xB2C1D500) │ 溢出的必经之路
├──────────────────────┤
│ buffer[0..7] │
└──────────────────────┘
低地址 (SP)

为什么 <<= 8 最低字节变为 \0。攻击者通过 strcpy() 溢出时,payload 中的 \0 会导致字符串截断,无法传递完整的伪造 canary 值来绕过检查。

随机数来源z_early_rand_get() 优先使用硬件熵源(STM32H7 的 RNG 外设),回退到基于 CPU 周期计数器的伪随机数。


7. 第五步:准备多线程与首次上下文切换

7.1 prepare_multithreading() — 创建 main 线程和 idle 线程

源码位置zephyr/kernel/init.c(442-473 行)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static char *prepare_multithreading(void)
{
char *stack_ptr;

z_sched_init(); // 1. 初始化调度器

#ifndef CONFIG_SMP
_kernel.ready_q.cache = &z_main_thread; // 2. 预热调度缓存
#endif

// 3. 创建 main 线程(入口函数 = bg_thread_main)
stack_ptr = z_setup_new_thread(&z_main_thread, z_main_stack,
K_THREAD_STACK_SIZEOF(z_main_stack),
bg_thread_main,
NULL, NULL, NULL,
CONFIG_MAIN_THREAD_PRIORITY,
K_ESSENTIAL, "main");
z_mark_thread_as_not_sleeping(&z_main_thread);
z_ready_thread(&z_main_thread); // 4. 加入就绪队列

z_init_cpu(0); // 5. 初始化 CPU 0(含 idle 线程)

return stack_ptr;
}

执行步骤详解

步骤 函数 做了什么
1 z_sched_init() 初始化调度器内部数据结构(就绪队列、优先级位图)
2 _kernel.ready_q.cache = &z_main_thread 调度缓存预热:缓存不能为 NULL,且此时只有 main 线程,直接填入避免调度器空转
3 z_setup_new_thread() 创建 main 线程:分配栈空间,初始化线程结构体,入口设为 bg_thread_main,优先级为 CONFIG_MAIN_THREAD_PRIORITY,标记 K_ESSENTIAL(线程退出时系统 panic)
4 z_ready_thread() 将 main 线程加入就绪队列
5 z_init_cpu(0) 初始化 CPU 0 数据结构:创建 idle 线程init_idle_thread(0))、设置 CPU 中断栈、CPU ID 映射

idle 线程的创建藏在 z_init_cpu(0)init_idle_thread(0) 中:

1
2
3
4
5
6
7
8
9
10
11
static void init_idle_thread(int i)
{
struct k_thread *thread = &z_idle_threads[i];
k_thread_stack_t *stack = z_idle_stacks[i];
size_t stack_size = K_KERNEL_STACK_SIZEOF(z_idle_stacks[i]);

z_setup_new_thread(thread, stack, stack_size,
idle, &_kernel.cpus[i], // 入口函数 = idle()
NULL, NULL, K_IDLE_PRIO, K_ESSENTIAL, "idle");
z_mark_thread_as_not_sleeping(thread);
}

idle 线程优先级 K_IDLE_PRIO 是最低优先级,当没有其他线程就绪时运行,执行 WFI 等待中断。

7.2 switch_to_main_thread() — 首次上下文切换

源码位置zephyr/kernel/init.c(476-490 行)

1
2
3
4
5
6
7
8
9
static FUNC_NORETURN void switch_to_main_thread(char *stack_ptr)
{
#ifdef CONFIG_ARCH_HAS_CUSTOM_SWAP_TO_MAIN
arch_switch_to_main_thread(&z_main_thread, stack_ptr, bg_thread_main);
#else
z_swap_unlocked();
#endif
CODE_UNREACHABLE;
}

两条路径

1
2
3
4
5
6
7
switch_to_main_thread(stack_ptr)

├─ CONFIG_ARCH_HAS_CUSTOM_SWAP_TO_MAIN=y(ARM Cortex-M)★ 当前项目
│ → arch_switch_to_main_thread(&z_main_thread, stack_ptr, bg_thread_main)

└─ 其他架构
→ z_swap_unlocked() ← 通用上下文切换(保存假线程状态,然后丢弃)

通用路径 z_swap_unlocked() 会保存假线程的状态再丢弃,浪费一次上下文保存。ARM Cortex-M 有专门的优化路径,完全绕过这个无意义操作。

7.2.1 arch_switch_to_main_thread() 完整流程

源码位置arch/arm/core/cortex_m/thread.c(564-656 行)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
void arch_switch_to_main_thread(struct k_thread *main_thread, char *stack_ptr,
k_thread_entry_t _main)
{
// ① FPU 准备
z_arm_prepare_switch_to_main();

// ② 直接设置 _current = main 线程(绕过 do_swap)
z_current_thread_set(main_thread);

// ③ TLS 指针初始化(如果启用了线程本地存储)
#if defined(CONFIG_THREAD_LOCAL_STORAGE)
z_arm_tls_ptr = main_thread->tls;
#endif

// ④ MPU 栈保护(如果启用)
#if defined(CONFIG_MPU_STACK_GUARD) || defined(CONFIG_USERSPACE)
z_arm_configure_dynamic_mpu_regions(main_thread);
#endif

// ⑤ PSPLIM 栈底保护(ARMv8-M,STM32H7 不支持)
#if defined(CONFIG_BUILTIN_STACK_GUARD) && defined(CONFIG_CPU_CORTEX_M_HAS_SPLIM)
__set_PSPLIM(main_thread->stack_info.start);
#endif

// ⑥ 内联汇编:切换栈 + 开中断 + 跳转到 z_thread_entry
__asm__ volatile(
"mov r4, %0\n" // 保存 _main 到 r4(callee-saved)
"msr PSP, %1\n" // 切换到 main 线程的栈
"movs r0, #0\n" // key = 0(开中断)
"blx r3\n" // arch_irq_unlock(0) → 开中断
"mov r0, r4\n" // R0 = bg_thread_main
"movs r1, #0\n" // p1 = NULL
"movs r2, #0\n" // p2 = NULL
"movs r3, #0\n" // p3 = NULL
"bx r4\n" // 跳转到 z_thread_entry,永不返回
);
}

7.2.2 逐步详解

**① z_arm_prepare_switch_to_main()**:清零 FPSCR(浮点状态控制寄存器),清除 CONTROL.FPCA 位。确保没有残留的浮点标志影响后续线程。

**② z_current_thread_set(main_thread)**:将 _current 从假线程切换到 main 线程。从此刻起,_current 指向 z_main_thread

③ TLS 指针初始化z_arm_tls_ptr = main_thread->tls。这是线程本地存储的关键一步。

TLS 知识点详解

什么是 TLS?__thread 关键字修饰的变量,每个线程有独立的副本:

1
2
3
4
5
static __thread int counter;  // 每个线程有自己的 counter

void thread_fn(void) {
counter++; // 只修改当前线程的副本,不影响其他线程
}

ARM Cortex-M 的困境:ARM Cortex-A 有专用寄存器 TPIDRURO 存放 TLS 基地址。Cortex-M 没有这个寄存器,Zephyr 用全局变量 z_arm_tls_ptr 替代。

1
2
3
4
5
6
Cortex-A(硬件支持):              Cortex-M(软件模拟):
┌────────────────────┐ ┌────────────────────┐
│ TPIDRURO 寄存器 │ │ z_arm_tls_ptr 全局变量│
│ → TLS 基地址 │ │ → TLS 基地址 │
└────────────────────┘ └────────────────────┘
↑ 上下文切换时写入 ↑ 上下文切换时写入

编译器遇到 __thread 变量时,调用 __aeabi_read_tp()(ARM EABI 标准函数)获取 TLS 基地址,再加偏移量访问变量。每个线程的 TLS 区域在线程栈顶分配。

为什么必须手动设置? 正常上下文切换由 PendSV 汇编自动更新 z_arm_tls_ptr。但 arch_switch_to_main_thread() 绕过了 PendSV 路径,如果不手动设置,z_arm_tls_ptr 为 0,main 线程中任何 __thread 变量访问都会解引用地址 0 → HardFault。

④ MPU 栈保护:为 main 线程配置 MPU 栈守卫区域。在线程栈底前方(低地址方向)放一个 guard 区域,设为特权只读。栈溢出时 PUSH 写入 guard → MPU 拦截 → MemManage 异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
高地址
┌──────────────────┐
│ │
│ 线程栈 │ ← 初始 SP,读/写
│ SP 向下增长 ↓ │
│ │
├──────────────────┤ ← stack_info.start(栈底,0x24010040)
│ Guard 区域 │ 只读(AP=101)
│ 64 字节 │ 写入 → MemManage!
├──────────────────┤ ← guard_start(0x24010000)
│ 其他数据 │ 读/写
└──────────────────┘
低地址

Guard 区域的 MPU 配置源码:

1
2
3
4
5
// arch/arm/core/mpu/arm_core_mpu.c
guard_start = thread->stack_info.start - guard_size; // 栈底 - 64
dynamic_regions[region_num].start = guard_start;
dynamic_regions[region_num].size = guard_size;
dynamic_regions[region_num].attr = K_MEM_PARTITION_P_RO_U_NA; // 特权只读
Guard 为什么是 64 字节?

Guard 的大小不是随意选择的,它要保证溢出被可靠检测。核心约束:

1
2
3
Guard 大小 ≥ 单次最大 PUSH + 异常帧大小
= 32 字节 + 32 字节
= 64 字节

用 32 字节 guard 和 64 字节 guard 的最坏情况对比(SP 刚好从栈底溢出)。

Cortex-M PUSH 指令的关键行为:PUSH 分两步——① 先减 SP(SP = SP - 4×N)② 再从新 SP 写入内存。MPU fault 发生在第 ② 步写内存时,此时 SP 已经减完了,不会回滚

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
【32 字节 Guard — 异常帧可能逃逸】

├──────────────────┤ ← 0x24010040 stack_info.start(栈底)
SP = │ Guard (32B) │ 只读
├──────────────────┤ ← 0x24010020 guard_start
│ 其他数据 │ 读/写
└──────────────────┘

① 线程 PUSH {R4-R11}(32 字节),SP = 0x24010040
→ SP 先减:SP = 0x24010040 - 32 = 0x24010020(已进入 guard)
→ 写入 [0x24010020, 0x2401003F] → guard 区域,只读 → MemManage 触发

② CPU 处理 fault,压异常帧(32 字节),从当前 SP = 0x24010020
→ 写入 [0x24010000, 0x2401001F] → 全部在 guard 下方!
→ 合法内存,push 成功 → 异常帧破坏了其他线程的数据 ✗
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
【64 字节 Guard — 异常帧仍在 guard 内】

├──────────────────┤ ← 0x24010040 stack_info.start(栈底)
│ Guard 上半 (32B) │ 只读
SP = ├──────────────────┤ ← 0x24010020
│ Guard 下半 (32B) │ 只读
├──────────────────┤ ← 0x24010000 guard_start
│ 其他数据 │ 读/写
└──────────────────┘

① 线程 PUSH {R4-R11}(32 字节),SP = 0x24010040
→ SP 先减:SP = 0x24010040 - 32 = 0x24010020(进入 guard 中间)
→ 写入 [0x24010020, 0x2401003F] → guard 区域 → MemManage 触发

② CPU 处理 fault,压异常帧(32 字节),从当前 SP = 0x24010020
→ 写入 [0x24010000, 0x2401001F] → 仍在 guard 内!
→ 又一次写只读区域 → 二次 MemManage → 升级 HardFault → 安全终止 ✓

64 字节 guard 的关键:SP 先减进入 guard 后,异常帧 push(32 字节)仍然落在 guard 范围内。Guard 大小 = 单次最大 PUSH(32B) + 异常帧(32B) = 64 字节,缺一不可。

MPU 栈保护的局限:只能防渐进式溢出(小 PUSH 一步步进入 guard),防不住跳跃式溢出——int buf[1000] 编译器生成 SUB SP, SP, #4000,只改 SP 寄存器不访问内存,MPU 完全无感,SP 直接跳过 guard。

保护方式 机制 防跳跃溢出 STM32H7
MPU guard 检查内存访问地址 不能 支持
PSPLIM 硬件寄存器 检查 SP 值 不支持(ARMv8-M)
-fstack-clash-protection 编译器插入探测 缓解 Zephyr 未启用

⑤ PSPLIM(ARMv8-M):直接检查 SP 值,SP < PSPLIM 立即触发异常。STM32H7 的 Cortex-M7 是 ARMv7E-M,没有此寄存器,所以只能依赖 MPU guard。

7.2.3 寄存器状态追溯:从上电到内联汇编执行前

以下通过源码追溯验证了每个阶段 PSP/MSP/CONTROL/BASEPRI 的变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
阶段 0:硬件复位
MSP = 向量表[0](初始栈顶)
PSP = 未定义
CONTROL.SPSEL = 0(使用 MSP)
BASEPRI = 0

阶段 1:reset.S — z_arm_reset()
MSP = z_main_stack 栈顶(临时供早期启动代码)
PSP = z_interrupt_stacks 栈顶
CONTROL.SPSEL = 1 ← 关键转折!从此 CPU 使用 PSP
BASEPRI = 0x10

阶段 2:z_prep_c() → z_cstart()
无 MSP/PSP/CONTROL/BASEPRI 修改
所有内核初始化代码运行在 PSP 上!

阶段 3:arch_kernel_init() → z_arm_interrupt_stack_setup()
MSP = z_interrupt_stacks[0] 栈顶(正式设为中断/异常专用栈)
PSP 不变(仍在中断栈内某处)
CONTROL.SPSEL = 1(未改变)

阶段 4:z_arm_prepare_switch_to_main()
FPSCR = 0, CONTROL.FPCA = 0
不修改 SPSEL

执行内联汇编前的最终状态

1
2
3
4
5
MSP            = z_interrupt_stacks[0] 栈顶(异常 handler 用)
PSP = 中断栈内某处 ← 当前活跃栈指针!
CONTROL.SPSEL = 1(线程模式使用 PSP,从 reset.S 起从未改变)
CONTROL.FPCA = 0(FPU 未激活)
BASEPRI = 0x10(中断关闭)

纠正常见误解:所有内核初始化代码(z_prep_cz_cstart、所有初始化函数)都运行在 PSP 上,不是 MSP。MSP 从 z_arm_interrupt_stack_setup() 后就留给异常/中断 handler 专用。

7.2.4 内联汇编逐条指令解析

1
2
/* 第 1 步:保存 _main 到 callee-saved 寄存器 */
mov r4, %0 /* r4 = _main (bg_thread_main 的地址) */

GCC 内联汇编占位符 %0 对应输入约束 "r"(_main)。保存到 r4 因为后面调用 arch_irq_unlock 会覆盖 r0-r3(caller-saved),而 r4-r11 是 callee-saved——被调用函数必须保存恢复,调用返回后值不变。_main 在函数调用之后还要用,必须放在 callee-saved 寄存器中。

1
2
/* 第 2 步:切换活跃栈(中断栈 → main 线程栈) */
msr PSP, %1 /* PSP = stack_ptr (main 线程栈顶) */

直接改变活跃栈指针! 由于 CONTROL.SPSEL = 1,PSP 就是当前活跃栈。msr PSP, stack_ptr 执行后,所有后续的 PUSH/CALL 立刻使用 main 线程的栈。必须在开中断之前完成——否则 PendSV 可能在 PSP 还指向中断栈时触发,异常帧压错位置。

1
2
3
4
5
6
7
8
/* 第 3 步:准备开中断参数 */
movs r0, #0 /* r0 = 0(开中断的 key) */

/* 第 4 步:加载函数地址 */
ldr r3, =arch_irq_unlock_outlined

/* 第 5 步:调用开中断 */
blx r3 /* arch_irq_unlock(0) → BASEPRI=0,中断开启 */

blx r3 跳转并保存返回地址到 lr。但 arch_switch_to_main_thread 标记 FUNC_NORETURNlr 的值毫无意义——注释说 “LR is lost which is fine”。r4 中的 bg_thread_main 地址在函数调用后仍然保留(callee-saved)。

1
2
3
4
5
6
7
8
9
10
11
/* 第 6 步:准备 z_thread_entry 参数 */
mov r0, r4 /* r0 = bg_thread_main(第 1 步保存在 r4 中) */
movs r1, #0 /* p1 = NULL */
movs r2, #0 /* p2 = NULL */
movs r3, #0 /* p3 = NULL */

/* 第 7 步:加载 z_thread_entry 地址 */
ldr r4, =z_thread_entry /* r4 可复用(旧值已复制到 r0) */

/* 第 8 步:跳转,永不返回 */
bx r4 /* 跳转到 z_thread_entry,不保存返回地址 */

bx(Branch and Exchange)与 blx 的关键区别:不保存返回地址到 lr。这是一次单向跳转:

  • lr 已被 arch_irq_unlock 调用覆盖,是垃圾值
  • arch_switch_to_main_thread 标记 FUNC_NORETURN
  • z_thread_entry 本身也从不返回

7.2.5 完整执行流程图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
执行前:
MSP ──► 中断栈顶(异常 handler 用)
PSP ──► 中断栈内某处(活跃栈!内核初始化一直运行在 PSP 上)
BASEPRI = 0x10(中断关闭)
CONTROL.SPSEL = 1


▼ mov r4, _main
r4 = bg_thread_main


▼ msr PSP, stack_ptr
PSP = main 线程栈顶(活跃栈切换!后续运行在 main 线程栈上)


▼ blx arch_irq_unlock(0)
BASEPRI = 0,中断开启
r4 = bg_thread_main(callee-saved,被恢复)


▼ mov r0, r4; movs r1-3, #0
r0 = bg_thread_main, r1=r2=r3 = NULL


▼ bx z_thread_entry
跳转到 z_thread_entry(bg_thread_main, NULL, NULL, NULL)
永不返回

z_thread_entry 内部:
调用 bg_thread_main(unused1, unused2, unused3)
调用 main()
→ 你的代码开始执行 ✓
7.2.5.1 z_thread_entry — 线程的真正入口

main 线程的真正执行从 z_thread_entry 内部开始。它不是架构相关的汇编代码,而是一个通用的 C 函数lib/os/thread_entry.c),所有架构、所有线程都经过它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// lib/os/thread_entry.c
FUNC_NORETURN void z_thread_entry(k_thread_entry_t entry,
void *p1, void *p2, void *p3)
{
// 1. 标记返回地址未定义(防止调试器回溯到垃圾地址)
ARCH_CFI_UNDEFINED_RETURN_ADDRESS();

// 2. TLS 初始化(如果启用了 CONFIG_CURRENT_THREAD_USE_TLS)
#ifdef CONFIG_CURRENT_THREAD_USE_TLS
z_tls_current = k_sched_current_thread_query();
#endif

// 3. TLS 版 Stack Canary(每个线程独立的 canary 值)
#ifdef CONFIG_STACK_CANARIES_TLS
sys_rand_get((uint8_t *)&stack_guard, sizeof(stack_guard));
__stack_chk_guard = stack_guard;
__stack_chk_guard <<= 8; // 最低字节清零,防 strcpy 绕过
#endif

// 4. 调用线程入口函数(对于 main 线程 = bg_thread_main)
entry(p1, p2, p3);

// 5. 入口函数返回 → 终止线程(不能让线程"掉出"到随机地址)
k_thread_abort(k_current_get());

CODE_UNREACHABLE;
}

为什么需要这个包装层? 直接让 bx 跳转到 bg_thread_main 不行吗?不行,原因有三个:

  1. 线程安全终止:如果线程入口函数(如 main())返回,不能让执行流”掉出”到随机地址。z_thread_entryentry() 返回后调用 k_thread_abort() 清理线程资源。

  2. TLS 初始化:每个线程创建后需要初始化自己的 TLS 变量(如 z_tls_current)和线程独立的 stack canary 值。这必须在 entry() 调用之前完成。

  3. CFI 标记ARCH_CFI_UNDEFINED_RETURN_ADDRESS() 告诉调试器”此函数没有有效的返回地址”,防止 backtrace 工具回溯到垃圾地址导致 HardFault。

调用链全景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
arch_switch_to_main_thread() 内联汇编
│ bx z_thread_entry

z_thread_entry(entry=bg_thread_main, p1=NULL, p2=NULL, p3=NULL)
│ ARCH_CFI_UNDEFINED_RETURN_ADDRESS() ← 标记无返回地址
│ [TLS 初始化]
│ [Stack Canary 初始化]
│ entry(p1, p2, p3) ← 调用 bg_thread_main

bg_thread_main(NULL, NULL, NULL)
│ z_sys_init_run_level(POST_KERNEL) ← 外设驱动初始化
│ z_sys_init_run_level(APPLICATION) ← 应用层初始化
│ z_init_static_threads() ← 静态线程就绪
│ main() ← 用户代码
│ ...(main 返回)

k_thread_abort(k_current_get()) ← main 线程终止

7.2.6 栈使用时间线

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
硬件复位
│ MSP 活跃(向量表初始值)

▼ reset.S: CONTROL.SPSEL = 1
│ PSP 活跃(z_interrupt_stacks,所有内核初始化代码运行于此)

▼ z_arm_interrupt_stack_setup(): MSP = 中断栈顶
│ MSP 供异常 handler 使用
│ PSP 仍活跃(内核初始化继续在中断栈上运行)

▼ 内联汇编 msr PSP, stack_ptr
│ PSP 活跃(main 线程栈,从此开始用 main 线程自己的栈)

▼ bx z_thread_entry → bg_thread_main → main()
全程运行在 PSP 上
异常发生时硬件自动切到 MSP,异常返回切回 PSP

7.2.7 为什么必须用内联汇编而不是 C 代码?

  1. 栈切换msr PSP, stack_ptr 切换栈指针。C 函数的局部变量和返回地址在旧栈上,切换后旧栈虽仍在内存但语义上已”不属于当前线程”。汇编精确控制哪些值在寄存器中(r4 保存 _main),不依赖栈。

  2. 不返回bx r4 没有”返回地址”(不是 blx),旧栈上的内容被丢弃。C 函数必须有返回路径,无法表达”永不返回”的跳转。

  3. 中断时序msr PSP 改变活跃栈指针,必须在开中断之前完成。如果先开中断,PendSV 可能在 PSP 还指向中断栈时触发 → 异常帧压错位置 → 上下文混乱。汇编精确控制 msr PSPblx arch_irq_unlock 之前。

7.3 假线程存在的意义

内核中大量代码通过 _current(当前线程指针)来获取上下文信息。无假线程时 _current 为 NULL,任何解引用都会触发 HardFault。

假线程的状态 _THREAD_DUMMY 确保它永远不会被调度,只是一个安全的占位指针。

内核中引用 _current 的典型场景

场景 代码 无假线程会怎样
调度器加锁 _current->base.sched_locked++ NULL 解引用 → HardFault
中断处理 z_get_next_ready_thread() 依赖当前线程上下文 优先级比较失败
内核对象权限(USERSPACE) k_object_access_grant(obj, _current) 授权给 NULL

8. 第六步:bg_thread_main() — 应用初始化的最后一步

源码位置zephyr/kernel/init.c(283-359 行)

bg_thread_main() 在首次上下文切换后运行于 main 线程中,完成剩余初始化并调用 main()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
static void bg_thread_main(void *unused1, void *unused2, void *unused3)
{
z_sys_post_kernel = true; // 标记内核初始化完成

#if CONFIG_IRQ_OFFLOAD
arch_irq_offload_init(); // IRQ 卸载初始化
#endif

z_sys_init_run_level(INIT_LEVEL_POST_KERNEL); // POST_KERNEL 外设驱动

soc_late_init_hook(); // SoC 后期初始化(可选)
board_late_init_hook(); // 板级后期初始化(可选)

boot_banner(); // 打印启动 banner

z_sys_init_run_level(INIT_LEVEL_APPLICATION); // APPLICATION 应用初始化

z_init_static_threads(); // 初始化用户定义的静态线程

// 调用用户的 main 函数
main();

// main() 返回后:清除 ESSENTIAL 标记,线程可被调度器回收
z_thread_essential_clear(&z_main_thread);
}

POST_KERNEL 阶段初始化的设备(STM32H747I-DISCO 典型序列):

设备 说明
FMC/SDRAM 控制器 外部内存
日志后端 日志输出到串口/RTT
malloc 堆 动态内存分配
SPI/QSPI Flash 外部存储
I2C 总线驱动
USB OTG USB 设备/主机
磁盘/SDMMC SD 卡/Flash 存储
FAT 文件系统 文件系统挂载

**soc_late_init_hook() vs soc_early_init_hook()**:early 在 PRE_KERNEL_1 之前(Cache 启用、电源配置),late 在 POST_KERNEL 之后(此时所有驱动已就绪,可执行依赖驱动的 SoC 级操作)。

**boot_banner()**:打印启动信息(如 *** Booting Zephyr OS build xxx ***),出现在串口上的第一行内核输出。

**z_init_static_threads()**:初始化用户代码中通过 K_THREAD_DEFINE() 定义的静态线程。这些线程在 main() 之前就绪,main() 返回后调度器继续运行它们。

至此,main() 执行时,所有初始化等级均已完成。


9. 总结

回顾整个启动流程,从硬件复位到 main() 的数据与控制传递:

flowchart TD
    subgraph 硬件["硬件"]
        direction LR
        A["CPU 复位"] --> B["向量表 0x08000000"] --> C["MSP + Reset_Handler"]
    end

    subgraph reset["reset.S"]
        direction LR
        D["z_arm_reset()"] --> E["soc_reset_hook()"] --> F["init_arch_hw()"] --> G["BASEPRI=0x10"] --> H["切换 PSP"]
    end

    subgraph prep["prep_c.c"]
        direction LR
        I["z_prep_c()"] --> J["VTOR"] --> K["清 .bss"] --> L["拷 .data"] --> M["NVIC 初始化"]
    end

    subgraph kernel["kernel/init.c"]
        direction LR
        N["z_cstart()"] --> O["Cache"] --> P["PRE_KERNEL_1"] --> Q["PRE_KERNEL_2"] --> R["canary"] --> S["多线程准备"] --> T["首次切换"]
    end

    subgraph app["应用"]
        direction LR
        U["z_thread_entry()"] --> V["bg_thread_main()"] --> W["POST_KERNEL"] --> X["main()"]
    end

    硬件 --> reset --> prep --> kernel --> app

    style 硬件 fill:#e3f2fd,stroke:#1565c0,color:#000
    style reset fill:#fff3e0,stroke:#ef6c00,color:#000
    style prep fill:#e8f5e9,stroke:#2e7d32,color:#000
    style kernel fill:#fce4ec,stroke:#c62828,color:#000
    style app fill:#e1f5fe,stroke:#0277bd,color:#000

五个关键要点

  1. 硬件向量表地址由 Option Bytes + BOOT 引脚决定:BCM7_ADD0 指定默认启动地址 0x08000000,这个值通过 DTS 的 chosen → Kconfig 的 dt_chosen_reg_addr_hex() → 链接脚本的 #include 宏,最终成为链接脚本中的 __rom_region_start

  2. reset.S 是一段精密的汇编初始化序列:CONTROL 重置 → MSP 设置 → TCM 清零 → MPU 禁用 → NVIC/Cache 初始化 → BASEPRI 屏蔽普通中断 → PSP 切换。每一步都有明确的硬件目的。

  3. Cache 必须在 .data/.bss 初始化之后才能启用:D-Cache 关闭时写操作直达 RAM(最简单可预测)。启用 Cache 后再初始化内存,会导致 Cache 和 RAM 内容不一致。

  4. 初始化顺序由链接脚本的段名决定,而非代码顺序.z_init_{LEVEL}_P_{PRIO}_ 格式的段名在链接时按字母排序,保证编译产物中的初始化数组天然有序。

  5. 首次上下文切换前需要假线程_current 在内核中广泛使用,解引用 NULL 会触发 HardFault。假线程是一个安全的占位指针,确保在内核初始化完成前所有代码都能安全访问 _current


参考源码(基于 Zephyr 主线):

  • zephyr/arch/arm/core/cortex_m/reset.S — 复位向量汇编
  • zephyr/arch/arm/core/cortex_m/prep_c.c — C 运行时准备
  • zephyr/kernel/init.c — 内核初始化入口
  • zephyr/arch/arm/core/cortex_m/scb.c — 架构硬件初始化
  • zephyr/soc/st/stm32/stm32h7x/soc_m7.c — STM32H7 SoC 钩子
  • zephyr/arch/arm/core/cortex_m/thread.c — 上下文切换实现