Zephyr RTOS 启动流程深度解析
·
版本:v2.0 | 适用版本:Zephyr v3.x | 架构:ARM Cortex-M
目录
启动流程总览
Zephyr的启动流程分为四个关键阶段,从硬件复位到应用层main()函数执行:
┌─────────────────────────────────────────────────────────────┐
│ Phase 1: 硬件复位 → C环境 (reset.S + z_prep_c) │
│ Phase 2: C环境准备 → 内核启动 (z_cstart) │
│ Phase 3: 内核初始化 → 多线程准备 (prepare_multithreading) │
│ Phase 4: 多线程启动 → 应用执行 (bg_thread_main → main) │
└─────────────────────────────────────────────────────────────┘
关键设计思想:
- 分层初始化:从硬件到应用,逐层解耦
- 双栈机制:MSP用于中断,PSP用于线程
- 延迟初始化:设备和驱动按需初始化
第一阶段:硬件复位到C环境
1.1 向量表入口
// arch/arm/core/cortex_m/vector_table.S
/*
* 向量表定义 - 系统复位后硬件从这里开始执行
* 注意:这是物理地址,通常位于Flash起始位置
*/
SECTION_SUBSEC_FUNC(exc_vector_table, _vector_table_section, _vector_table)
/* 初始栈顶地址 - MSP复位后的值 */
.word z_main_stack + CONFIG_MAIN_STACK_SIZE
/* 复位向量 - 指向复位处理函数 */
.word z_arm_reset
/* 其他异常向量... */
.word z_arm_nmi // NMI
.word z_arm_hard_fault // Hard Fault
.word z_arm_mpu_fault // MPU Fault (如果启用)
...
关键注释:
z_main_stack定义在kernel/init.c,由K_THREAD_PINNED_STACK_DEFINE创建CONFIG_MAIN_STACK_SIZE默认通常为 1024 字节,可通过 Kconfig 配置- 向量表必须对齐到 0x100 边界(ARM要求)
1.2 复位处理函数
// arch/arm/core/cortex_m/reset.S
/*
* z_arm_reset - 系统复位入口点
*
* 功能:
* 1. 初始化核心寄存器
* 2. 设置栈指针(MSP和PSP)
* 3. 切换到C环境
*
* 执行环境:
* - 运行在Thread模式,Privileged级别
* - 中断已禁用(上电默认状态)
*/
SECTION_SUBSEC_FUNC(TEXT, _reset_section, z_arm_reset)
// 别名定义,便于bootloader识别
SECTION_SUBSEC_FUNC(TEXT, _reset_section, __start)
// ========== 步骤1: 基础硬件初始化 ==========
#if defined(CONFIG_INIT_ARCH_HW_AT_BOOT)
// 清零CONTROL寄存器,确保在Thread模式
movs.n r0, #0
msr CONTROL, r0
isb // 指令同步屏障
#if defined(CONFIG_CPU_CORTEX_M_HAS_SPLIM)
// 清零栈限制寄存器(防止意外触发栈溢出)
msr MSPLIM, r0
msr PSPLIM, r0
#endif
#endif
// ========== 步骤2: SoC早期钩子 ==========
#if defined(CONFIG_SOC_EARLY_RESET_HOOK)
// 调用SoC特定的早期初始化代码
// 注意:此时RAM可能还未初始化,谨慎使用栈
bl soc_early_reset_hook
#endif
// ========== 步骤3: 电源管理恢复处理 ==========
#if defined(CONFIG_PM_S2RAM)
/*
* S2RAM(Suspend to RAM)恢复处理
*
* 当系统从S2RAM唤醒时,MSP可能已被破坏。
* 这里临时设置MSP到中断栈,用于执行恢复检查函数。
*/
ldr r0, =z_interrupt_stacks + CONFIG_ISR_STACK_SIZE + MPU_GUARD_ALIGN_AND_SIZE
msr msp, r0
// 检查是否需要恢复,或只是正常启动
bl arch_pm_s2ram_resume
#endif
// ========== 步骤4: 设置主栈(MSP)==========
/*
* 关键:必须设置MSP到z_main_stack
*
* 原因:
* 1. 如果使用PM_S2RAM,MSP当前指向中断栈
* 2. 如果通过chain loading或调试器加载,MSP值未知
* 3. 早期C代码(如memset)需要栈空间
*/
ldr r0, =z_main_stack + CONFIG_MAIN_STACK_SIZE
msr msp, r0 // MSP = main stack top
// ========== 步骤5: 调试支持 ==========
#if defined(CONFIG_DEBUG_THREAD_INFO)
// 清零z_sys_post_kernel标志,告知调试器系统正在启动
movs.n r0, #0
ldr r1, =z_sys_post_kernel
strb r0, [r1]
#endif
// ========== 步骤6: SoC复位钩子 ==========
#if defined(CONFIG_SOC_RESET_HOOK)
bl soc_reset_hook
#endif
// ========== 步骤7: 禁用中断 ==========
/*
* 在切换到主任务之前保持中断禁用
* 使用BASEPRI而非PRIMASK,允许特定优先级中断
*/
#if defined(CONFIG_ARMV6_M_ARMV8_M_BASELINE)
cpsid i // 简单禁用所有中断
#elif defined(CONFIG_ARMV7_M_ARMV8_M_MAINLINE)
movs.n r0, #_EXC_IRQ_DEFAULT_PRIO
msr BASEPRI, r0 // 屏蔽低优先级中断
#else
#error "Unknown ARM architecture"
#endif
// ========== 步骤8: 初始化中断栈 ==========
#ifdef CONFIG_INIT_STACKS
// 将中断栈填充为0xAA(便于调试时检测栈使用)
ldr r0, =z_interrupt_stacks
ldr r1, =0xaa
ldr r2, =CONFIG_ISR_STACK_SIZE + MPU_GUARD_ALIGN_AND_SIZE
bl arch_early_memset
#endif
// ========== 步骤9: 设置进程栈(PSP)==========
/*
* 关键:使用PSP执行C代码,保持MSP用于中断
*
* 这样设计的好处:
* - 中断自动切换到MSP(z_interrupt_stacks)
* - 线程代码使用PSP(z_main_stack)
* - 实现线程栈和中断栈的隔离
*/
ldr r0, =z_interrupt_stacks
ldr r1, =CONFIG_ISR_STACK_SIZE + MPU_GUARD_ALIGN_AND_SIZE
adds r0, r0, r1 // r0 = z_interrupt_stacks顶部
msr PSP, r0 // PSP = interrupt stack top
// 设置CONTROL寄存器,切换到PSP
mrs r0, CONTROL
movs r1, #(2 | CONTROL_ARM_PAC_MASK | CONTROL_ARM_BTI_MASK)
orrs r0, r1 // 设置CONTROL.SPSEL = 1(使用PSP)
msr CONTROL, r0
isb // 必须同步,确保后续指令使用新栈
// ========== 步骤10: 跳转到C环境 ==========
bl z_prep_c // 永不返回
关键设计决策:
| 决策 | 说明 | 原因 |
|---|---|---|
| 使用PSP而非MSP执行C代码 | 线程模式使用PSP | 保持MSP专用于中断处理 |
| 禁用中断直到切换完成 | cpsid i / BASEPRI |
防止初始化过程中断 |
| 栈初始化填充0xAA | CONFIG_INIT_STACKS |
便于调试检测栈溢出 |
第二阶段:C环境准备
2.1 z_prep_c函数
// arch/arm/core/cortex_m/prep_c.c
/*
* z_prep_c - C环境准备函数
*
* 这是第一个C函数,此时:
* - BSS段尚未清零
* - 数据段尚未复制(如果是XIP)
* - 全局变量不可用
*
* 注意:此时不能使用未初始化的全局变量!
*/
FUNC_NORETURN void z_prep_c(void)
{
// ========== 步骤1: SoC准备钩子 ==========
/*
* 在重定位向量表之前执行SoC特定准备
* 某些SoC需要在此阶段配置时钟或内存
*/
soc_prep_hook();
// ========== 步骤2: 向量表重定位 ==========
/*
* 将向量表从Flash复制到RAM(如果启用SRAM_VECTOR_TABLE)
* 并设置VTOR寄存器指向新的向量表
*
* 好处:
* 1. 可以动态修改向量表(如添加/移除中断)
* 2. 支持bootloader链式加载
*/
relocate_vector_table();
// ========== 步骤3: FPU初始化 ==========
#if defined(CONFIG_CPU_HAS_FPU)
z_arm_floating_point_init();
#endif
// ========== 步骤4: BSS段清零 ==========
/*
* 关键:必须在使用全局变量之前清零BSS
*
* BSS段包含:
* - 未初始化的全局变量
* - 未初始化的静态变量
*
* 这些变量默认值为0,C标准要求
*/
arch_bss_zero();
// ========== 步骤5: 数据段复制 ==========
/*
* 如果是XIP(Execute In Place),代码直接从Flash运行
* 但.data段(已初始化全局变量)需要在RAM中
*
* 从Flash的加载地址复制到RAM的运行地址
*/
arch_data_copy();
// ========== 步骤6: 中断控制器初始化 ==========
#if defined(CONFIG_ARM_CUSTOM_INTERRUPT_CONTROLLER)
// SoC特定中断控制器
z_soc_irq_init();
#else
// 标准NVIC初始化
z_arm_interrupt_init();
#endif
// ========== 步骤7: Cache初始化 ==========
#if CONFIG_ARCH_CACHE
arch_cache_init();
#endif
// ========== 步骤8: 空指针检测 ==========
#ifdef CONFIG_NULL_POINTER_EXCEPTION_DETECTION_DWT
z_arm_debug_enable_null_pointer_detection();
#endif
// ========== 步骤9: 进入C启动 ==========
z_cstart(); // 永不返回
}
2.2 向量表重定位详解
// arch/arm/core/cortex_m/prep_c.c
/*
* relocate_vector_table - 重定位中断向量表
*
* ARM Cortex-M的VTOR寄存器控制向量表位置。
* 默认情况下,向量表位于0x00000000(通常是Flash)。
* 重定位到RAM可以:
* - 动态修改中断向量
* - 支持运行时中断向量变更
*/
void __weak relocate_vector_table(void)
{
#ifdef CONFIG_SRAM_VECTOR_TABLE
/*
* 将向量表从Flash复制到SRAM
*
* 计算向量表大小(包含所有中断)
* 标准Cortex-M有16个内核异常 + 最多240个外部中断
*/
size_t vector_size = (size_t)_vector_end - (size_t)_vector_start;
arch_early_memcpy(_sram_vector_start, _vector_start, vector_size);
// 设置VTOR指向SRAM中的向量表
SCB->VTOR = (uintptr_t)_sram_vector_start;
#else
// 使用Flash中的向量表
SCB->VTOR = (uintptr_t)_vector_start;
#endif
// 数据同步屏障 + 指令同步屏障
barrier_dsync_fence_full();
barrier_isync_fence_full();
}
向量表结构:
地址偏移 内容 说明
─────────────────────────────────────────────────
0x00000000 MSP初始值 主栈指针
0x00000004 Reset_Handler 复位向量
0x00000008 NMI_Handler NMI
0x0000000C HardFault_Handler Hard Fault
0x00000010 MemManage_Handler MPU Fault
0x00000014 BusFault_Handler 总线错误
0x00000018 UsageFault_Handler 使用错误
... ... ...
0x00000040 外部中断0 IRQ0
0x00000044 外部中断1 IRQ1
... ... ...
第三阶段:内核初始化
3.1 z_cstart函数
// kernel/init.c
/*
* z_cstart - C启动主函数
*
* 这是内核初始化的核心函数,负责:
* 1. 分层初始化系统组件
* 2. 设置多线程环境
* 3. 最终切换到用户main()
*
* 注意:此函数永不返回
*/
FUNC_NORETURN void z_cstart(void)
{
// ========== 步骤1: GCOV初始化 ==========
// 代码覆盖率测试支持
gcov_static_init();
// ========== 步骤2: 早期初始化 ==========
/*
* INIT_LEVEL_EARLY:最早的初始化级别
* 适用于架构扩展或SoC底层驱动
*
* 此时可用:
* - 基础内存访问
* - 轮询延时
*
* 此时不可用:
* - 中断
* - 内核服务(线程、信号量等)
*/
z_sys_init_run_level(INIT_LEVEL_EARLY);
// ========== 步骤3: 架构初始化 ==========
/*
* arch_kernel_init() 包含:
* - z_arm_interrupt_stack_setup(): 设置MSP指向中断栈
* - z_arm_exc_setup(): 配置异常处理
* - z_arm_fault_init(): 故障处理初始化
* - z_arm_cpu_idle_init(): 空闲状态初始化
* - z_arm_mpu_init(): MPU初始化(如果启用)
*
* 关键:这里将MSP从main stack切换到interrupt stack
*/
arch_kernel_init();
// ========== 步骤4: 日志系统初始化 ==========
LOG_CORE_INIT();
// ========== 步骤5: 虚拟线程初始化 ==========
#if defined(CONFIG_MULTITHREADING)
/*
* 创建虚拟线程结构,用于:
* - 初始化期间有当前上下文概念
* - 首次上下文切换的源线程
*/
z_dummy_thread_init(&_thread_dummy); // ------设置_thread_dummy为当前线程(_current_cpu->current)
#endif
// ========== 步骤6: 设备状态初始化 ==========
/*
* 初始化所有设备对象的状态
* 此时只是准备数据结构,实际初始化在后面
*/
z_device_state_init();
// ========== 步骤7: 早期钩子函数 ==========
soc_early_init_hook();
board_early_init_hook();
// ========== 步骤8: 预内核阶段1 ==========
/*
* INIT_LEVEL_PRE_KERNEL_1
*
* 初始化内容:
* - 系统时钟
* - 控制台UART
* - GPIO控制器
*
* 此时仍不能使用线程调度
*/
z_sys_init_run_level(INIT_LEVEL_PRE_KERNEL_1);
// ========== 步骤9: SMP初始化 ==========
#if defined(CONFIG_SMP)
arch_smp_init();
#endif
// ========== 步骤10: 预内核阶段2 ==========
/*
* INIT_LEVEL_PRE_KERNEL_2
*
* 更复杂的设备初始化
* 依赖于PRE_KERNEL_1阶段已初始化的服务
*/
z_sys_init_run_level(INIT_LEVEL_PRE_KERNEL_2);
// ========== 步骤11: 堆栈保护初始化 ==========
#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
// ========== 步骤12: 时序功能初始化 ==========
#ifdef CONFIG_TIMING_FUNCTIONS_NEED_AT_BOOT
timing_init();
timing_start();
#endif
// ========== 步骤13: 切换到多线程 ==========
#ifdef CONFIG_MULTITHREADING
switch_to_main_thread(prepare_multithreading());
#else
// 无多线程模式,直接调用main
bg_thread_main(NULL, NULL, NULL);
// 永不返回
irq_lock();
while (true) { }
#endif
CODE_UNREACHABLE;
}
3.2 初始化级别详解
// include/zephyr/init.h
/*
* Zephyr使用分层初始化机制,通过SYS_INIT()宏注册初始化函数。
* 每个级别在系统启动的特定阶段执行。
*/
enum init_level {
/*
* EARLY: 最早阶段,刚进入C环境
*
* 使用场景:
* - 架构特定代码
* - SoC底层硬件初始化
* - 扩展内核功能
*/
INIT_LEVEL_EARLY = 0,
/*
* PRE_KERNEL_1: 预内核阶段1
*
* 使用场景:
* - 系统时钟初始化
* - 基础设备驱动(UART、GPIO)
*
* 限制:不能使用内核服务(线程、信号量等)
*/
INIT_LEVEL_PRE_KERNEL_1 = 1,
/*
* PRE_KERNEL_2: 预内核阶段2
*
* 使用场景:
* - 依赖PRE_KERNEL_1服务的驱动
* - 更复杂的硬件初始化
*/
INIT_LEVEL_PRE_KERNEL_2 = 2,
/*
* POST_KERNEL: 内核启动后
*
* 使用场景:
* - 可以使用所有内核服务
* - 线程、信号量、消息队列等
*/
INIT_LEVEL_POST_KERNEL = 3,
/*
* APPLICATION: 应用层
*
* 使用场景:
* - 应用特定的初始化
* - 在main()之前执行
*/
INIT_LEVEL_APPLICATION = 4,
/*
* SMP: SMP特定初始化
*
* 只在多核系统启用
*/
INIT_LEVEL_SMP = 5,
};
/*
* SYS_INIT宏用法
*
* @param init_fn: 初始化函数
* @param level: 初始化级别(EARLY、PRE_KERNEL_1等)
* @param prio: 优先级(0-99,越小越早执行)
*/
#define SYS_INIT(init_fn, level, prio) \
SYS_INIT_NAMED(init_fn, init_fn, level, prio)
// 示例:注册一个PRE_KERNEL_1级别的初始化函数,优先级50
SYS_INIT(my_driver_init, PRE_KERNEL_1, 50);
初始化级别优先级规则:
- 相同级别内,按优先级排序(0最先)
- 高优先级初始化依赖低优先级初始化的结果
- 链接器按
.z_init_XXX段名排序
第四阶段:多线程启动
4.1 多线程准备
// kernel/init.c
/*
* prepare_multithreading - 准备多线程环境
*
* 功能:
* 1. 初始化调度器
* 2. 创建主线程
* 3. 创建空闲线程
* 4. 设置CPU状态
*
* 返回:主线程栈指针(用于首次上下文切换)
*/
static char *prepare_multithreading(void)
{
char *stack_ptr;
// ========== 步骤1: 调度器初始化 ==========
/*
* 初始化就绪队列、优先级队列等数据结构
*/
z_sched_init();
#ifndef CONFIG_SMP
/*
* 单核优化:预加载主线程到就绪队列缓存
*
* 原因:
* - 就绪队列缓存不能为NULL
* - 主线程是第一个运行的线程
* - 其他线程尚未初始化,优先级字段是垃圾值
*/
_kernel.ready_q.cache = &z_main_thread;
#endif
// ========== 步骤2: 创建主线程 ==========
/*
* 主线程参数:
* - 栈:z_main_stack(最初复位时使用的栈)
* - 入口:bg_thread_main(内核后台线程)
* - 优先级:CONFIG_MAIN_THREAD_PRIORITY(通常为0)
* - 选项:K_ESSENTIAL(系统关键线程)
*/
stack_ptr = z_setup_new_thread( // ------ 调用该函数会把thread_state置为_THREAD_SLEEPING
&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" // 名称
);
// 清楚_THREAD_SLEEPING状态(准备就绪)
z_mark_thread_as_not_sleeping(&z_main_thread);
// 将主线程加入就绪队列_kernel.ready_q.runq
z_ready_thread(&z_main_thread);
// ========== 步骤3: CPU初始化 ==========
z_init_cpu(0);
return stack_ptr; // 返回主线程栈指针,用于切换
}
/*
* z_init_cpu - 初始化CPU核心
*
* 为每个CPU核心:
* - 创建空闲线程
* - 设置中断栈
* - 初始化CPU数据结构
*/
void z_init_cpu(int id)
{
// 初始化空闲线程
init_idle_thread(id);
// 设置CPU数据结构
_kernel.cpus[id].idle_thread = &z_idle_threads[id];
_kernel.cpus[id].id = id;
_kernel.cpus[id].irq_stack =
(K_KERNEL_STACK_BUFFER(z_interrupt_stacks[id]) +
K_KERNEL_STACK_SIZEOF(z_interrupt_stacks[id]));
// 其他CPU状态初始化...
}
4.2 切换到主线程
// kernel/init.c
/*
* switch_to_main_thread - 切换到主线程执行
*
* 这是第一次上下文切换,从此进入多线程世界。
*/
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()后,当前上下文消失
* - 永远不会返回到此函数
*/
ARG_UNUSED(stack_ptr);
z_swap_unlocked();
#endif
CODE_UNREACHABLE;
}
void arch_switch_to_main_thread(struct k_thread *main_thread, char *stack_ptr,
k_thread_entry_t _main)
{
z_arm_prepare_switch_to_main();
z_current_thread_set(main_thread);
#if defined(CONFIG_THREAD_LOCAL_STORAGE)
/* On Cortex-M, TLS uses a global variable as pointer to
* the thread local storage area. So this needs to point
* to the main thread's TLS area before switching to any
* thread for the first time, as the pointer is only set
* during context switching.
*/
extern uintptr_t z_arm_tls_ptr;
z_arm_tls_ptr = main_thread->tls;
#endif
#ifdef CONFIG_INSTRUMENT_THREAD_SWITCHING
z_thread_mark_switched_in();
#endif
/* the ready queue cache already contains the main thread */
#if defined(CONFIG_MPU_STACK_GUARD) || defined(CONFIG_USERSPACE)
/*
* If stack protection is enabled, make sure to set it
* before jumping to thread entry function
*/
z_arm_configure_dynamic_mpu_regions(main_thread);
#endif
#if defined(CONFIG_BUILTIN_STACK_GUARD)
/* Set PSPLIM register for built-in stack guarding of main thread. */
#if defined(CONFIG_CPU_CORTEX_M_HAS_SPLIM)
__set_PSPLIM(main_thread->stack_info.start);
#else
#error "Built-in PSP limit checks not supported by the hardware."
#endif
#endif /* CONFIG_BUILTIN_STACK_GUARD */
#ifdef CONFIG_ARM_PAC_PER_THREAD
__set_PAC_KEY_P((uint32_t *)&main_thread->arch.pac_keys);
#endif
/*
* Set PSP to the highest address of the main stack
* before enabling interrupts and jumping to main.
*
* The compiler may store _main on the stack, but this
* location is relative to `PSP`.
* This assembly block ensures that _main is stored in
* a callee saved register before switching stack and continuing
* with the thread entry process.
*
* When calling arch_irq_unlock_outlined, LR is lost which is fine since
* we do not intend to return after calling z_thread_entry.
*/
__asm__ volatile("mov r4, %0\n" /* force _main to be stored in a register */
"msr PSP, %1\n" /* __set_PSP(stack_ptr) */
"movs r0, #0\n" /* arch_irq_unlock(0) */
"ldr r3, =arch_irq_unlock_outlined\n"
"blx r3\n"
"mov r0, r4\n" /* z_thread_entry(_main, NULL, NULL, NULL) */
"movs r1, #0\n"
"movs r2, #0\n"
"movs r3, #0\n"
"ldr r4, =z_thread_entry\n"
/* We don’t intend to return, so there is no need to link. */
"bx r4\n"
/* Force a literal pool placement for the addresses referenced above */
#ifndef __IAR_SYSTEMS_ICC__
".ltorg\n"
#endif
:
: "r"(_main), "r"(stack_ptr)
: "r0", "r1", "r2", "r3", "r4", "ip", "lr", "memory");
CODE_UNREACHABLE;
}
4.3 主线程执行
// kernel/init.c
/*
* bg_thread_main - 主线程入口(背景线程)
*
* 这是"真正的"系统启动线程,完成最终初始化并调用用户main()
*/
static void bg_thread_main(void *unused1, void *unused2, void *unused3)
{
ARG_UNUSED(unused1);
ARG_UNUSED(unused2);
ARG_UNUSED(unused3);
// ========== 步骤1: 内存管理初始化 ==========
#ifdef CONFIG_MMU
/*
* 启用MMU,建立页表
* 之后可以进行虚拟内存管理
*/
z_mem_manage_init();
#endif
// 标记已进入POST_KERNEL阶段
z_sys_post_kernel = true;
// ========== 步骤2: 中断卸载初始化 ==========
#if CONFIG_IRQ_OFFLOAD
arch_irq_offload_init();
#endif
// ========== 步骤3: POST_KERNEL初始化 ==========
z_sys_init_run_level(INIT_LEVEL_POST_KERNEL);
// ========== 步骤4: 晚期钩子函数 ==========
soc_late_init_hook();
board_late_init_hook();
// ========== 步骤5: 栈指针随机化 ==========
#if defined(CONFIG_STACK_POINTER_RANDOM) && (CONFIG_STACK_POINTER_RANDOM != 0)
z_stack_adjust_initialized = 1;
#endif
// ========== 步骤6: 打印启动横幅 ==========
boot_banner();
// ========== 步骤7: 应用层初始化 ==========
z_sys_init_run_level(INIT_LEVEL_APPLICATION);
// ========== 步骤8: 初始化静态线程 ==========
/*
* 启动通过K_THREAD_DEFINE()定义的静态线程
*/
z_init_static_threads();
// ========== 步骤9: SMP初始化 ==========
#ifdef CONFIG_SMP
if (!IS_ENABLED(CONFIG_SMP_BOOT_DELAY)) {
z_smp_init(); // 启动其他CPU核心
}
z_sys_init_run_level(INIT_LEVEL_SMP);
#endif
// ========== 步骤10: 内存管理最终设置 ==========
#ifdef CONFIG_MMU
z_mem_manage_boot_finish();
#endif
// ========== 步骤11: 调用用户main() ==========
#ifdef CONFIG_BOOTARGS
// 支持命令行参数
extern int main(int, char **);
extern char **prepare_main_args(int *argc);
int argc = 0;
char **argv = prepare_main_args(&argc);
(void)main(argc, argv);
#else
extern int main(void);
(void)main();
#endif
// ========== 步骤12: main()返回后的处理 ==========
/*
* 如果main()返回(不应该发生):
* - 清理线程标记
* - 转储覆盖率数据(如果启用)
* - 进入无限循环
*/
z_thread_essential_clear(&z_main_thread);
#ifdef CONFIG_COVERAGE_DUMP
gcov_coverage_dump();
#elif defined(CONFIG_COVERAGE_SEMIHOST)
gcov_coverage_semihost();
#endif
}
关键问题解析
Q1: 为什么z_arm_reset要设置两次MSP?
答案:实际上是不同的栈!
// 第一次:在reset.S中
ldr r0, =z_main_stack + CONFIG_MAIN_STACK_SIZE
msr msp, r0
// 第二次:在arch_kernel_init()中
void z_arm_interrupt_stack_setup(void) {
uint32_t msp = (uint32_t)(K_KERNEL_STACK_BUFFER(z_interrupt_stacks[0])) +
K_KERNEL_STACK_SIZEOF(z_interrupt_stacks[0]);
__set_MSP(msp);
}
原因:
- 第一次:为早期C代码(
z_prep_c)提供栈空间 - 第二次:切换到中断栈,专门用于中断处理
最终栈分配:
- MSP →
z_interrupt_stacks(中断处理) - PSP →
z_main_stack(主线程执行)
Q2: 启动过程中的栈是如何切换的?
栈切换流程:
阶段 | MSP指向 | PSP指向 | 说明
────────────────────────────────────────────────────────────────────────
复位后 | z_main_stack | z_interrupt_stacks | 早期代码
arch_kernel_init | z_interrupt_stacks | z_interrupt_stacks | 准备多线程
prepare_multithreading | z_interrupt_stacks | 未设置 | 创建主线程
切换到主线程 | z_interrupt_stacks | z_main_stack | 主线程运行
────────────────────────────────────────────────────────────────────────
注意:
- 中断发生时,硬件自动从PSP切换到MSP
- 中断返回时,硬件自动从MSP切换回PSP
- 这种设计确保了中断不会破坏线程栈
Q3: 为什么需要z_dummy_thread?
原因:
- 在第一次上下文切换之前,系统需要一个"当前线程"
z_dummy_thread提供了这个临时上下文- 它不在任何就绪队列中,因此不会被调度
- 第一次
z_swap()会切换到真正的主线程
调试技巧
1. 启动失败排查
症状:系统卡在启动阶段
排查步骤:
// 1. 检查复位向量
(gdb) x/4wx 0x00000000
// 应该看到MSP值和z_arm_reset地址
// 2. 检查z_arm_reset是否被执行
(gdb) break z_arm_reset
(gdb) continue
// 应该命中断点
// 3. 检查是否能进入z_prep_c
(gdb) break z_prep_c
(gdb) continue
// 4. 检查是否能进入z_cstart
(gdb) break z_cstart
(gdb) continue
// 5. 检查初始化级别
(gdb) break z_sys_init_run_level
(gdb) continue
// 每次命中时检查传入的level参数
2. 栈溢出检测
// 启用栈初始化填充
CONFIG_INIT_STACKS=y
// 在调试器中检查栈使用
(gdb) x/256wx z_main_stack
// 查看0xAA模式是否被破坏
// 或使用栈哨兵
CONFIG_STACK_SENTINEL=y
3. 启动时间测量
// 在关键位置添加计时
#include <zephyr/timing/timing.h>
void z_cstart(void) {
timing_t start_time = timing_counter_get();
// ... 初始化代码 ...
timing_t init_end = timing_counter_get();
uint64_t cycles = timing_cycles_get(&start_time, &init_end);
printk("Init time: %llu cycles\n", cycles);
}
总结
Zephyr的启动流程体现了以下设计思想:
- 分层解耦:从硬件到应用,每层职责清晰
- 安全优先:中断禁用直到系统就绪
- 灵活扩展:多级初始化,便于驱动扩展
- 资源高效:main stack复用为线程栈
理解启动流程对于:
- 移植到新平台
- 调试启动问题
- 开发设备驱动
- 优化启动时间
都有重要意义。
本文档基于Zephyr v3.7源码分析
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)