MCU启动与代码执行教程

1. 简介

本教程旨在帮助理解,深入剖析ARM Cortex-M系列单片机上电后的完整启动流程,以及程序在Flash、RAM、寄存器三者的协同执行机制。基于STM32等典型MCU,从硬件复位瞬间开始,逐步讲解向量表加载、Reset_Handler的初始化职责、main()函数运行时的栈帧布局与函数调用细节,最终建立起对单片机底层执行模型的系统性认知。

2. MCU系统架构

在这里插入图片描述

2.1 核心模块

FLASH模块
  • 地址范围: 以STM32为例子:0x08000000 - 0x080FFFFF (典型值)
  • 存储内容: 机器码(汇编指令编译后的二进制代码)
  • 关键地址:
    • 0x08000000: 用户程序起始地址,Flash首地址
RAM模块
  • 地址范围: 0x20000000 - 0x2001FFFF (典型值)
  • 存储内容:
    • 全局变量
    • 局部变量
    • 函数调用栈
    • 堆内存
  • 关键地址:
    • 0x20000000: RAM起始地址
    • 0x20000770: (仅限于此文档用例)主栈指针初始值(_estack)
CPU模块
  • ARM Cortex-M系列处理器
  • 工作频率: 通常为几十MHz到几百MHz
寄存器
  • R0-R12: 通用寄存器,用于数据存储和计算
  • PC (Program Counter): 程序计数器,指向下一条要执行的指令地址
  • LR (Link Register): 链接寄存器,存储函数返回地址
  • SP (Stack Pointer): 栈指针,分为MSP(主栈指针)和PSP(进程栈指针)
  • CPSR: 当前程序状态寄存器

2.2 总线系统

AHB (Advanced High-performance Bus)
  • 用途: 连接高性能模块
  • 连接设备: CPU、Flash、RAM、DMA等
APB (Advanced Peripheral Bus)
  • 用途: 连接低速外设
  • 连接设备: GPIO、USART、I2C、SPI等

2.3 外设控制器

  • GPIO: 通用输入输出
  • USART: 串行通信
  • I2C: I2C总线通信
  • SPI: SPI总线通信
  • TIM: 定时器
  • ADC: 模数转换器

3. MCU启动流程

3.1 上电初始化

当MCU上电或收到复位信号后,硬件逻辑会执行以下固定步骤:
电源与时钟稳定:内部电压调节器启动,HSI/HSE时钟源起振,复位信号释放。
从固定地址加载栈指针:Cortex-M内核从地址0x00000000读取主栈指针(MSP)的初始值。但在实际STM32中,由于Flash被映射到0x08000000,且0x00000000区域通过BOOT引脚重映射到Flash,因此实际读取的是0x08000000处的内容。该值应为RAM的末尾地址(如0x20000770),符号为_estack。
从固定地址加载PC初值:内核从地址0x00000004(即物理0x08000004)读取复位向量,即Reset_Handler函数的入口地址。将该地址写入程序计数器PC,随后CPU从Reset_Handler开始取指执行。
⚠️ 关键点:ARM Cortex-M的向量表第一个字是SP初始值,第二个字是复位向量,与普通ARM不同。这两个值必须正确存放在Flash起始处,否则复位后会立即跑飞。

  1. MCU上电

    • 电源稳定
    • 时钟源启动
    • 复位信号释放
  2. 向量表解析

    • 从Flash地址0x08000000读取向量表
    • 获取Reset_Handler地址(初始化PC寄存器)
    • 获取初始栈指针值(_estack SP的值)
      在这里插入图片描述
  3. 向量表示例

地址(Flash) 内容 说明
0x08000000 0x20000770 主栈指针初始值(_estack)
0x08000004 0x080001a1 Reset_Handler地址(LSB=1表示Thumb)
0x08000008 0x080001XX NMI_Handler
其他异常向量

3.2 Reset_Handler—— 真正的系统初始化

Reset_Handler是复位后第一个执行的用户代码(通常由启动文件startup_stm32xxx.s提供)。它负责建立C运行环境,为main()函数的执行铺路。

.section .text.Reset_Handler
.weak Reset_Handler
.type Reset_Handler, %function
Reset_Handler:
    ldr   r0, =_estack      ; 将_estack地址加载到r0
    mov   sp, r0          ; 设置栈指针sp
    
    /* Copy the data segment initializers from flash to SRAM */
    ldr r0, =_sdata       ; .data段起始地址
    ldr r1, =_edata       ; .data段结束地址
    ldr r2, =_sidata      ; .data段初始值在Flash中的起始地址
    movs r3, #0           ; 初始化计数器
    b LoopCopyDataInit    ; 跳转到复制循环
    
CopyDataInit:
    ldr r4, [r2, r3]      ; 从Flash读取初始值
    str r4, [r0, r3]      ; 写入到RAM
    adds r3, r3, #4       ; 计数器加4
    
LoopCopyDataInit:
    adds r4, r0, r3       ; 计算当前地址
    cmp r4, r1            ; 比较是否到达.data段结束地址
    bcc CopyDataInit      ; 如果小于,继续复制
    
    /* Zero fill the bss segment. */
    ldr r2, =_sbss        ; .bss段起始地址
    ldr r4, =_ebss        ; .bss段结束地址
    movs r3, #0           ; 准备写入0
    b LoopFillZerobss     ; 跳转到清零循环
    
FillZerobss:
    str  r3, [r2]         ; 向.bss段写入0
    adds r2, r2, #4       ; 地址加4
    
LoopFillZerobss:
    cmp r2, r4            ; 比较是否到达.bss段结束地址
    bcc FillZerobss       ; 如果小于,继续清零
    
    /* Call SystemInit function */
    bl SystemInit         ; 调用系统初始化函数
    
    /* Call the application's entry point.*/
    bl main               ; 调用main函数
    bx lr                 ; 如果main返回,跳转到LR指向的地址(通常进入死循环)
.size Reset_Handler, .-Reset_Handler

3.3 Reset_Handler详解

3.3.1 栈指针设置
ldr   r0, =_estack      ; 将_estack地址加载到r0
mov   sp, r0          ; 设置栈指针sp
  • _estack是栈的顶部地址,通常定义为RAM的末尾
  • 设置SP指向栈顶,为函数调用做准备
3.3.2 数据段复制
ldr r0, =_sdata       ; .data段起始地址
ldr r1, =_edata       ; .data段结束地址
ldr r2, =_sidata      ; .data段初始值在Flash中的起始地址
movs r3, #0           ; 初始化计数器
b LoopCopyDataInit    ; 跳转到复制循环
  • 将Flash中初始化好的.data段复制到RAM中
  • .data段包含已初始化的全局变量和静态变量
3.3.3 BSS段清零
ldr r2, =_sbss        ; .bss段起始地址
ldr r4, =_ebss        ; .bss段结束地址
movs r3, #0           ; 准备写入0
b LoopFillZerobss     ; 跳转到清零循环
  • 将.bss段清零
  • .bss段包含未初始化的全局变量和静态变量
3.3.4 系统初始化
bl SystemInit         ; 调用系统初始化函数
  • SystemInit函数配置系统时钟
  • 设置Flash等待周期
  • 配置其他系统参数
3.3.5 调用main函数
bl main               ; 调用main函数
bx lr                 ; 如果main返回,跳转到LR指向的地址
  • 调用用户主函数
  • 如果main函数返回,通常进入死循环

3.4 SystemInit函数

SystemInit函数通常由芯片厂商提供,负责:

  • 配置系统时钟
  • 设置Flash等待状态
  • 配置电压调节器
  • 初始化其他系统参数

3.5 各项初始化动作的作用

步骤 作用 若不执行会怎样?
设置MSP 指定栈在RAM中的位置,使能函数调用和局部变量 压栈操作会破坏内存,程序立即崩溃
复制.data段 将已初始化的全局/静态变量从Flash搬到RAM,使其可读写 这些变量初始值错误(为Flash内容而非预期值)
清零.bss段 将未初始化的全局/静态变量所在区域置0 变量内容随机,产生未定义行为
SystemInit 配置系统时钟(PLL)、Flash等待周期、向量表重定位等 内核可能运行在低速时钟,外设无法正确工作
调用main 进入应用程序主函数 无用户代码执行

💡 专业提示:SystemInit通常由芯片厂商提供,位于system_stm32xxx.c。它不会清零.bss或复制.data——这些由启动汇编负责。部分RTOS还会在main之前完成堆初始化、全局构造函数调用等。

4. 用户代码执行

4.1 main函数示例

#include <stdio.h>

// 全局变量
int global_var = 10;
int global_uninit_var;  // 未初始化,在.bss段

// 函数声明
void function1(int param1, int param2);
void function2(void);

int main(void)
{
    // 局部变量
    int local_var1 = 20;
    int local_var2 = 30;
    int array[5] = {1, 2, 3, 4, 5};
    
    printf("Hello, MCU!\n");
    printf("global_var = %d\n", global_var);
    printf("local_var1 = %d\n", local_var1);
    
    function1(local_var1, local_var2);
    function2();
    
    while(1) {
        // 主循环
    }
    
    return 0;
}

void function1(int param1, int param2)
{
    int local_func_var = param1 + param2;
    
    printf("function1: param1 = %d, param2 = %d\n", param1, param2);
    printf("function1: local_func_var = %d\n", local_func_var);
    
    function2();
}

void function2(void)
{
    int local_func_var2 = 100;
    
    printf("function2: local_func_var2 = %d\n", local_func_var2);
}

经过编译后的机器码,烧录到flash后的地址示意:

flash地址 机器码     汇编指令 C语言对应
0x08001108 b086     SUB sp,sp,#0x18     int local_var1 = 20, local_var2 = 30; int array[5]
0x0800110A 2414     MOVS r4,#0x14       local_var1 = 20
0x0800110C 251E     MOVS r5,#0x1E       local_var2 = 30
0x0800110E 2214     MOVS r2,#0x14
0x08001110 490B     LDR r1,[pc,#44]
0x08001112 A801     ADD r0,sp,#4
0x08001114 F7FFF8E0 BL __aeabi_memcpy4  array = {1,2,3,4,5}
0x08001118 A00A     ADR r0,{pc}+0x2C
0x0800111A F7FFF861 BL __2printf        printf("Hello, MCU!\n")
0x0800111E 480D     LDR r0,[pc,#52]
0x08001120 6801     LDR r1,[r0,#0]
0x08001122 A00D     ADR r0,{pc}+0x36
0x08001124 F7FFF85C BL __2printf        printf("global_var = %d", global_var)
0x08001128 4621     MOV r1,r4
0x0800112A A010     ADR r0,{pc}+0x42
0x0800112C F7FFF858 BL __2printf        printf("local_var1 = %d", local_var1)
0x08001130 4629     MOV r1,r5
0x08001132 4620     MOV r0,r4
0x08001134 F7FFFF9A BL function1       function1(local_var1, local_var2)
0x0800106C B570     PUSH {r4-r6,lr}    void function1(int param1, int param2)
0x0800106E 4604     MOV r4,r0
0x08001070 460D     MOV r5,r1
0x08001072 1966     ADDS r6,r4,r5      int local_func_var = param1 + param2
0x08001074 462A     MOV r2,r5
0x08001076 4621     MOV r1,r4
0x08001078 A004     ADR r0,{pc}+0x14
0x0800107A F7FFF8B1 BL __2printf        printf("function1: param1 = %d, param2 = %d")
0x0800107E 4631     MOV r1,r6
0x08001080 A00C     ADR r0,{pc}+0x34
0x08001082 F7FFF8AD BL __2printf       printf("function1: local_func_var = %d")
0x08001086 F000F825 BL function2       function2()
0x080010D4 B510     PUSH {r4,lr}       void function2(void)
0x080010D6 2464     MOVS r4,#0x64      int local_func_var2 = 100
0x080010D8 4621     MOV r1,r4
0x080010DA A002     ADR r0,{pc}+0xA
0x080010DC F7FFF880 BL __2printf      printf("function2: local_func_var2 = %d")
0x080010E0 BD10     POP {r4,pc}
0x0800108A BD70     POP {r4-r6,pc}
0x08001138 F7FFFFCC BL function2      function2()
0x080010D4 B510     PUSH {r4,lr}      void function2(void)
0x080010D6 2464     MOVS r4,#0x64     int local_func_var2 = 100
0x080010D8 4621     MOV r1,r4
0x080010DA A002     ADR r0,{pc}+0xA
0x080010DC F7FFF880 BL __2printf      printf("function2: local_func_var2 = %d")
0x080010E0 BD10     POP {r4,pc}
0x0800113C BF00     NOP
0x0800113E E7FE     B 0x0800113E      while(1)

这里可以自行根据汇编代码捋一捋这段C代码再MCU中的运行流程,也有制作一个动画演示这个流程,需要的话后期会上传。
在这里插入图片描述

4.2 函数调用栈帧

栈帧(Stack Frame)结构
每个活跃函数在栈中占据一块连续区域,称为栈帧。典型栈帧包含:

高地址
+-------------------------+
| 调用者函数的局部变量等   |  ← 调用者的栈帧顶部
+-------------------------+
| 函数参数(如果多于4个)   |  ← 参数区域
+-------------------------+
| 返回地址(LR)           |
+-------------------------+
| 被调用者保存的寄存器     |  (如R4-R11, 可选)
+-------------------------+
| 局部变量区域             |
+-------------------------+  ← 当前SP (帧底部)
低地址

ARM Cortex-M使用满递减栈:SP指向最后一个压入的数据,栈向低地址增长。压栈时SP减小,弹栈时SP增大。

4.2.1 栈结构
高地址
+---------------------+
| 局部变量           | <- 栈顶 (SP)
+---------------------+
| 返回地址           | <- LR
+---------------------+
| 参数               |
+---------------------+
| 保存的寄存器       |
+---------------------+
| 函数帧指针         | <- FP (可选)
+---------------------+
低地址
4.2.2 函数调用过程
  1. 调用函数

    function1(local_var1, local_var2);
    
  2. 汇编代码

    ; 参数传递
    mov r0, local_var1    ; 第一个参数放入r0
    mov r1, local_var2    ; 第二个参数放入r1
    
    ; 调用函数
    bl function1          ; 跳转到function1,并保存返回地址到LR
    
  3. 函数执行

    function1:
        push {r7, lr}      ; 保存r7和返回地址
        sub sp, sp, #8     ; 分配局部变量空间
        
        mov r2, r0         ; 参数1
        mov r3, r1         ; 参数2
        
        ; 执行函数体
        
        add sp, sp, #8     ; 释放局部变量空间
        pop {r7, pc}       ; 恢复r7和返回地址
    

4.3 内存布局

0x08000000 +---------------------+
           | 代码段 (.text)      | <- Flash
           |                     |
           | Reset_Handler       |
           | SystemInit          |
           | main                 |
           | function1            |
           | function2            |
           |                     |
0x0800FFFF +---------------------+
           |                     |
           |                     |
0x20000000 +---------------------+
           | 数据段 (.data)       | <- RAM
           |                     |
           | global_var          |
           |                     |
           |                     |
           | BSS段 (.bss)        |
           |                     |
           | global_uninit_var   |
           |                     |
           |                     |
           ||
           |                     |
           | 局部变量             |
           | 函数参数             |
           | 函数返回地址         |
0x20005000 +---------------------+
           |                     |
           |                     |
0x2001FFFF +---------------------+

4.4栈回溯

待续

5. RAM、寄存器、Flash的协同工作模型

三者协同执行程序的核心关系如下:

组件 角色 关键特性
Flash 非易失性存储:存放代码(.text)、常量(.rodata)、初始值(.data初始镜像) 只读(正常模式),按字/半字访问
RAM 易失性存储:存放可写数据(.data, .bss)、堆、栈 读写速度快,断电丢失
寄存器 CPU内部临时存储:指令指针(PC)、栈指针(SP)、通用寄存器(R0-R12)、状态寄存器等 访问最快,数量有限,函数参数/返回值载体

5.1 指令执行周期

  1. 取指:CPU将PC的值输出到地址总线,Flash控制器返回对应地址的机器码(Thumb-2指令)。
  2. 译码:指令解码单元识别操作码。
  3. 执行:ALU、乘法器或加载存储单元执行操作。若为LDR/STR,则访问RAM地址(通过AHB总线);若为算术指令,则操作寄存器。
  4. 写回:结果写回寄存器或内存。

5.2 数据流示例

int global_x = 10;      // 存储在.data段(RAM)
int global_y;           // 存储在.bss段(RAM)

int main(void) {
    int local_z;        // 栈上分配
    local_z = global_x + global_y;
    return local_z;
}

执行local_z = global_x + global_y;时:

  1. 编译器生成ldr r0, =global_x的地址,再用ldr r0, [r0]将RAM中的值读到R0。
  2. 类似地将global_y读到R1。
  3. add r0, r0, r1 → 结果在R0。
  4. str r0, [sp, #offset]写入局部变量local_z的栈位置。

5.3 启动与运行阶段的协同图

上电 → 硬件从Flash[0]取SP → 硬件从Flash[4]取PC → 执行Reset_Handler
                                                          │
                                                          ↓
                                             从Flash复制.data到RAM
                                             清零.bss
                                             设置SP为_estack
                                             调用SystemInit (配置时钟等)
                                                          │
                                                          ↓
                                                     调用main()
                                                          │
                                                          ↓
                                         函数调用:参数放R0-R3/栈,跳转
                                         函数内:SP减分配局部变量,访问RAM/寄存器
                                         返回:恢复SP,弹出PC

6. 总结与进阶思考

6.1 核心要点回顾

ARM Cortex-M复位后自动加载SP和PC,开发者只需确保向量表正确放置在Flash起始。

Reset_Handler完成C运行时环境初始化:设置栈、搬运.data、清零.bss、系统时钟配置。

函数调用遵循AAPCS,使用栈帧保存返回地址、局部变量和寄存器上下文。

Flash提供指令和只读数据,RAM存放可写数据与栈,寄存器为运算核心,三者的高效协同是嵌入式程序运行的基础。

6.2 常见陷阱与建议

向量表地址不对齐:Cortex-M要求向量表对齐到256字节(若重定位),否则可能触发硬错误。

栈溢出:未定义栈大小导致覆盖.data或.bss,表现为随机硬错误。建议使用栈哨兵或MPU保护。

中断中的栈使用:中断服务函数使用当前栈(通常是MSP),需确保主栈足够大以容纳嵌套中断的栈帧。

内存屏障:在某些低功耗模式切换或Flash配置更改后,需使用__DSB()等指令保证操作完成。

6.3 扩展阅读方向

链接脚本(.ld):如何定义_sdata、_estack等符号。

启动文件中的弱定义(Weak)与中断向量表默认处理函数。

使用__attribute__((section(“.ramfunc”)))将关键函数放到RAM中执行以提升速度。

从Bootloader跳转到App时的向量表重定位与栈指针重设。

通过理解上述底层机制,工程师能够更自信地调试启动失败、栈溢出、硬错误等问题,并为编写高效、健壮的嵌入式固件打下坚实基础。

Logo

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

更多推荐