欢迎光临
我们一直在努力

启动流程详解:从上电到main()发生了什么

你有没有想过:按下电源开关到main()开始执行,中间发生了什么?
很多人写了几年STM32,从来没打开过startup文件。但面试一问"启动流程"就懵了。
这篇从上电一步步讲到main(),帮你搞懂那段"看不见的代码"。


一、为什么需要了解启动流程?

1.1 看得见的问题

  • 程序跑飞,HardFault,不知道哪里错了
  • 全局变量初始值不对
  • printf不工作
  • 自定义 bootloader 跳转后崩溃

这些问题可能都和启动流程有关。

1.2 面试必问

“STM32从上电到进入main(),经历了哪些步骤?”——这是中高级嵌入式岗位的高频面试题。


二、启动流程全景图

1. 上电/复位

2. CPU从0x08000000(Flash起始)读栈指针(MSP)

> **MSP是什么?** MSP(Main Stack Pointer,主栈指针)指向栈的顶部。栈是一段RAM空间,用于函数调用时保存局部变量和返回地址。CPU复位时第一件事就是读MSP,因为后面执行代码可能就要调用函数,需要知道栈在哪。
> **什么是"段"?** 编译器把不同类型的变量分到不同的内存区域,这些区域叫"段"。.data段存有初始值的全局变量,.bss段存没初始值的全局变量(自动清零)。

3. CPU从0x08000004读复位函数地址(Reset_Handler)

4. 执行Reset_Handler(启动文件里的汇编代码)

5. 初始化.data段(已初始化全局变量从Flash拷到RAM)

6. 清零.bss段(未初始化全局变量)

7. 调用SystemInit()(配置时钟)

8. 调用__main(C库初始化)

9. 调用main()(你的程序入口)


三、逐步详解

3.1 第1步:读栈指针

CPU复位后,硬件自动做两件事:

  • 从0x08000000读4字节 → 存入MSP(主栈指针)
  • 从0x08000004读4字节 → 作为PC(程序计数器),跳转过去
  • Flash地址 内容
    0x08000000 → 栈顶地址(如0x20005000,RAM末尾)
    0x08000004 → Reset_Handler函数地址
    0x08000008 → NMI_Handler
    0x0800000C → HardFault_Handler
    … (中断向量表)

    这两步是硬件固定行为,不可更改。所以向量表必须在Flash起始地址。

    3.2 第2步:执行Reset_Handler

    启动文件(startup_stm32f103xb.s)里的汇编代码:

    Reset_Handler:
    LDR R0, =__initial_sp ; 设置栈指针
    MSR MSP, R0

    BL SystemInit ; 调用SystemInit()配置时钟
    BL __main ; 调用C库初始化,最终跳到main()

    3.3 第3步:初始化.data段

    .data段存已初始化的全局变量,但它们的初始值存在Flash里,运行时要拷到RAM。

    /* 全局变量初始值存在Flash,运行时拷到RAM */
    int counter = 100; /* .data段 */
    const int version = 1; /* .rodata段(只读,留Flash) */

    启动代码做的事:

    从Flash的_data_load地址 → 拷贝到 → RAM的_data_run地址
    长度 = .data段大小

    3.4 第4步:清零.bss段

    .bss段存未初始化或初始化为0的全局变量,启动时统一清零。

    int flag; /* .bss段,自动清零 */
    int buf[256]; /* .bss段,自动清零 */
    static int count = 0; /* .bss段 */

    启动代码做的事:

    把RAM的.bss区域全部填0

    为什么不存Flash:.bss全是0,存Flash浪费空间。只在RAM里清零即可。

    3.5 第5步:SystemInit()

    /* system_stm32f1xx.c */
    void SystemInit(void)
    {
    /* 配置中断向量表地址 */
    SCB->VTOR = FLASH_BASE; /* SCB=System Control Block(系统控制块),VTOR=Vector Table Offset Register(向量表偏移寄存器),这行设置中断向量表的位置 */
    /* CubeMX会在这里调用SystemClock_Config()配置PLL */
    }

    3.6 第6步:__main → main()

    __main是C库的初始化函数,做:

    • 初始化堆(用于malloc)
    • 调用C++构造函数(如果有)
    • 最终调用你的main()

    四、内存分布

    Flash (0x08000000):
    ┌──────────────┐
    │ 向量表 │ ← 中断向量,固定在最前面
    ├──────────────┤
    │ .text │ ← 代码
    ├──────────────┤
    │ .rodata │ ← const常量、字符串
    ├──────────────┤
    │ .data初始值 │ ← 全局变量的初始值
    └──────────────┘

    RAM (0x20000000):
    ┌──────────────┐
    │ .data │ ← 已初始化全局变量(从Flash拷来)
    ├──────────────┤
    │ .bss │ ← 未初始化全局变量(清零)
    ├──────────────┤
    │ Heap │ ← 堆(malloc用,向下生长)
    ├──────────────┤
    │ │
    │ Stack │ ← 栈(向下生长,MSP指向栈顶)
    └──────────────┘


    五、启动文件里的关键信息

    5.1 栈和堆大小

    Stack_Size EQU 0x00000400 ; 栈1KB
    Heap_Size EQU 0x00000200 ; 堆512B

    注意:这里设的栈是MSP(主栈),中断也用这个栈。FreeRTOS的任务栈是另外分配的。

    5.2 向量表

    __Vectors:
    DCD __initial_sp ; 栈顶地址
    DCD Reset_Handler ; 复位
    DCD NMI_Handler ; NMI
    DCD HardFault_Handler ; 硬件错误

    DCD USART1_IRQHandler ; USART1中断

    函数名必须和这里一致,否则中断进不去。

    5.3 弱符号(Weak)

    NMI_Handler PROC
    EXPORT NMI_Handler [WEAK]
    B . ; 死循环
    ENDP

    [WEAK]表示弱定义:如果你没写NMI_Handler函数,用这个默认的(死循环);如果你写了,用你的。

    这就是为什么HAL回调能用:HAL库里用__weak声明,你可以重写覆盖。


    六、常见踩坑

    坑1:全局变量初始值不对

    原因:.data段没正确拷贝(可能是修改了启动文件或链接脚本)。

    检查:用调试器看全局变量的值是否等于定义时的初始值。


    坑2:栈溢出

    原因:启动文件里Stack_Size太小,函数调用层级深时溢出。

    解决:调大Stack_Size(如从1KB改为4KB)。


    坑3:HardFault_Handler死循环

    默认HardFault_Handler是B .(死循环)。调试时可以改成打印故障信息:

    void HardFault_Handler(void)
    {
    printf("HardFault! PC=0x%08X LR=0x%08X\\n",
    *(uint32_t*)(__get_MSP() + 24), /* PC */
    *(uint32_t*)(__get_MSP() + 20)); /* LR */
    while (1);
    }


    坑4:Bootloader跳转后崩溃

    从bootloader跳转到APP前,必须:

  • 关闭所有中断
  • 重新设置MSP(读APP的向量表第0项)
  • 跳转到APP的Reset_Handler
  • /* Bootloader跳转到APP */
    typedef void (*func_t)(void);
    uint32_t app_addr = 0x08008000;
    func_t app_reset = (func_t)(*(uint32_t *)(app_addr + 4));

    __disable_irq(); /* 关中断 */
    __set_MSP(*(uint32_t *)app_addr); /* 设置MSP */
    app_reset(); /* 跳转 */


    七、总结

    步骤做什么
    1. 硬件 从Flash起始读MSP和Reset_Handler地址
    2. Reset_Handler 汇编初始化,设置栈
    3. .data初始化 从Flash拷贝已初始化全局变量到RAM
    4. .bss清零 未初始化全局变量清零
    5. SystemInit 配置时钟和向量表
    6. __main C库初始化,调用main()

    一句话总结:启动流程的核心是"硬件读向量表→汇编初始化→拷贝.data→清零.bss→配置时钟→进入main()",理解了这6步,启动流程就通了。


    下一篇预告:《HAL库 vs LL库 vs 寄存器:到底用哪个?》

    如果这篇文章对你有帮助,点赞 + 收藏 + 关注,这是我持续更新的动力!
    有问题欢迎评论区交流,我会逐条回复。

    作者:嵌入式阿蔡

    赞(0)
    未经允许不得转载:171主机测评 » 启动流程详解:从上电到main()发生了什么
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址