版本:v2.0 | 适用版本:Zephyr v3.x | 架构:ARM Cortex-M

目录

  1. 启动流程总览
  2. 第一阶段:硬件复位到C环境
  3. 第二阶段:C环境准备
  4. 第三阶段:内核初始化
  5. 第四阶段:多线程启动
  6. 关键问题解析
  7. 调试技巧

启动流程总览

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)提供栈空间
  • 第二次:切换到中断栈,专门用于中断处理

最终栈分配

  • MSPz_interrupt_stacks(中断处理)
  • PSPz_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?

原因

  1. 在第一次上下文切换之前,系统需要一个"当前线程"
  2. z_dummy_thread 提供了这个临时上下文
  3. 它不在任何就绪队列中,因此不会被调度
  4. 第一次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的启动流程体现了以下设计思想:

  1. 分层解耦:从硬件到应用,每层职责清晰
  2. 安全优先:中断禁用直到系统就绪
  3. 灵活扩展:多级初始化,便于驱动扩展
  4. 资源高效:main stack复用为线程栈

理解启动流程对于:

  • 移植到新平台
  • 调试启动问题
  • 开发设备驱动
  • 优化启动时间

都有重要意义。


本文档基于Zephyr v3.7源码分析

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐