嵌入式进阶——MCU启动与代码执行教程
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起始处,否则复位后会立即跑飞。
-
MCU上电
- 电源稳定
- 时钟源启动
- 复位信号释放
-
向量表解析
- 从Flash地址0x08000000读取向量表
- 获取Reset_Handler地址(初始化PC寄存器)
- 获取初始栈指针值(_estack SP的值)

-
向量表示例
| 地址(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 函数调用过程
-
调用函数
function1(local_var1, local_var2); -
汇编代码
; 参数传递 mov r0, local_var1 ; 第一个参数放入r0 mov r1, local_var2 ; 第二个参数放入r1 ; 调用函数 bl function1 ; 跳转到function1,并保存返回地址到LR -
函数执行
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 指令执行周期
- 取指:CPU将PC的值输出到地址总线,Flash控制器返回对应地址的机器码(Thumb-2指令)。
- 译码:指令解码单元识别操作码。
- 执行:ALU、乘法器或加载存储单元执行操作。若为LDR/STR,则访问RAM地址(通过AHB总线);若为算术指令,则操作寄存器。
- 写回:结果写回寄存器或内存。
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;时:
- 编译器生成ldr r0, =global_x的地址,再用ldr r0, [r0]将RAM中的值读到R0。
- 类似地将global_y读到R1。
- add r0, r0, r1 → 结果在R0。
- 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时的向量表重定位与栈指针重设。
通过理解上述底层机制,工程师能够更自信地调试启动失败、栈溢出、硬错误等问题,并为编写高效、健壮的嵌入式固件打下坚实基础。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)