嵌入式进阶——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)内容说明0x080000000x20000770主栈指针初始值(_estack)0x080000040x080001a1Reset_Handler地址(LSB1表示Thumb)0x080000080x080001XXNMI_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 applications entry point.*/ bl main ; 调用main函数 bx lr ; 如果main返回跳转到LR指向的地址通常进入死循环 .size Reset_Handler, .-Reset_Handler3.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函数示例#includestdio.h// 全局变量intglobal_var10;intglobal_uninit_var;// 未初始化在.bss段// 函数声明voidfunction1(intparam1,intparam2);voidfunction2(void);intmain(void){// 局部变量intlocal_var120;intlocal_var230;intarray[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){// 主循环}return0;}voidfunction1(intparam1,intparam2){intlocal_func_varparam1param2;printf(function1: param1 %d, param2 %d\n,param1,param2);printf(function1: local_func_var %d\n,local_func_var);function2();}voidfunction2(void){intlocal_func_var2100;printf(function2: local_func_var2 %d\n,local_func_var2);}经过编译后的机器码烧录到flash后的地址示意flash地址 机器码 汇编指令 C语言对应0x08001108b086 SUB sp,sp,#0x18intlocal_var120,local_var230;intarray[5]0x0800110A2414MOVS r4,#0x14local_var1200x0800110C251E MOVS r5,#0x1Elocal_var2300x0800110E2214MOVS r2,#0x140x08001110490B LDR r1,[pc,#44]0x08001112A801 ADD r0,sp,#40x08001114F7FFF8E0 BL __aeabi_memcpy4 array{1,2,3,4,5}0x08001118A00A ADR r0,{pc}0x2C0x0800111AF7FFF861 BL __2printfprintf(Hello, MCU!\n)0x0800111E480D LDR r0,[pc,#52]0x080011206801LDR r1,[r0,#0]0x08001122A00D ADR r0,{pc}0x360x08001124F7FFF85C BL __2printfprintf(global_var %d,global_var)0x080011284621MOV r1,r40x0800112AA010 ADR r0,{pc}0x420x0800112CF7FFF858 BL __2printfprintf(local_var1 %d,local_var1)0x080011304629MOV r1,r50x080011324620MOV r0,r40x08001134F7FFFF9A BL function1function1(local_var1,local_var2)0x0800106CB570 PUSH{r4-r6,lr}voidfunction1(intparam1,intparam2)0x0800106E4604MOV r4,r00x08001070460D MOV r5,r10x080010721966ADDS r6,r4,r5intlocal_func_varparam1param20x08001074462A MOV r2,r50x080010764621MOV r1,r40x08001078A004 ADR r0,{pc}0x140x0800107AF7FFF8B1 BL __2printfprintf(function1: param1 %d, param2 %d)0x0800107E4631MOV r1,r60x08001080A00C ADR r0,{pc}0x340x08001082F7FFF8AD BL __2printfprintf(function1: local_func_var %d)0x08001086F000F825 BL function2function2()0x080010D4B510 PUSH{r4,lr}voidfunction2(void)0x080010D62464MOVS r4,#0x64intlocal_func_var21000x080010D84621MOV r1,r40x080010DAA002 ADR r0,{pc}0xA0x080010DCF7FFF880 BL __2printfprintf(function2: local_func_var2 %d)0x080010E0BD10 POP{r4,pc}0x0800108ABD70 POP{r4-r6,pc}0x08001138F7FFFFCC BL function2function2()0x080010D4B510 PUSH{r4,lr}voidfunction2(void)0x080010D62464MOVS r4,#0x64intlocal_func_var21000x080010D84621MOV r1,r40x080010DAA002 ADR r0,{pc}0xA0x080010DCF7FFF880 BL __2printfprintf(function2: local_func_var2 %d)0x080010E0BD10 POP{r4,pc}0x0800113CBF00 NOP0x0800113EE7FE B0x0800113Ewhile(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 数据流示例intglobal_x10;// 存储在.data段RAMintglobal_y;// 存储在.bss段RAMintmain(void){intlocal_z;// 栈上分配local_zglobal_xglobal_y;returnlocal_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弹出PC6. 总结与进阶思考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时的向量表重定位与栈指针重设。通过理解上述底层机制工程师能够更自信地调试启动失败、栈溢出、硬错误等问题并为编写高效、健壮的嵌入式固件打下坚实基础。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2500856.html
如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!