STM32的启动流分析

第1章 启动流程分析

嵌入式程序上电后的经典启动流程:上电 --> 执行初始化指令 --> 调用 SystemInit 配置时钟 --> 调用 __main 初始化 C 语言环境 --> 最终进入你写的 main() 函数开始执行主程序。

__main 函数是在keil软件中的一个库文件中。在该函数中才是调用我们自己写的main() 函数。

1.1 keil软件编译生成的axf文件分析

AXF 的全称是 ARM Executable File (ARM 可执行文件)。当你在 Keil 等 IDE 中写完 C 语言代码,点击"编译链接(Build)"后,编译器会生成这个 .axf 文件。

从上图可以看出整个汇编文件的启动流程。

1.2 单片机的硬件架构

1.3 存储器映射

CPU是通过地址来访问寄存器、RAM、FLASH、定时器、串口等片上外设。ARM寻址范围4GB,分为多个块:

1.3.1 代码区(CODE区)

ARM公司将0x0000 0000 ~ 0x2000 0000 设置为代码区。

  1. 0x0000 0000 - 0x002F FFFF:实现存储空间的"重映射",完成程序的跳转。
    1. 这个区域的作用是实现存储空间的"重映射"。它可以将Main Flash 或厂商预置的 Bootloader 映射到 0x00000000 这个起始地址,具体映射哪一个,取决于 BOOT 引脚的配置。
    2. 单片机上电后,CPU 固定从 0x00000000 取指执行;当内存控制器识别到该地址为映射窗口时,会自动将访问请求重定向至对应的物理存储区,从而引导程序执行。
  2. 0x0800 0000 - 0x082F FFFF:Main Flash区域。该区域是用户烧写程序的地址区域。
  3. 0x1FFF B000 - 0x1FFF F7FF:厂商BootLoader区域。单片机厂商出厂时预制进去的一段Bootloader程序(出厂时固化)。该程序是完成ISP机制功能。
    • ISP机制:借助下载器(如 J-Link 或 STLink),通过芯片的 SWD 引脚,把你编译好的bin文件里的机器码,直接写进 GD32 内部的 Flash 存储器里的。总之,ISP机制就完成两个功能,一是接受二进制镜像文件,二是将二进制镜像烧录到Main Flash中。

1.3.2 SRAM区(内存区)

ARM公司将0x2000 0000 ~ 0x4000 0000 设置为SRAM区。

1.3.2.1 栈空间

函数的代码存放在CODE区中的Main Flash【0x0800 0000~0x082F FFFF】中(下图的左侧),而函数运行时产生的数据(栈帧)存放在 RAM 的栈空间中(下图的右侧)。

(1)栈空间的大小

对于ARM 32位单片机,栈空间最大值是在startupARMCM4.s文件里定义的,0x00000400对应十进制是1024,代表栈空间最大值是1024个字节。

栈的大小可以根据工程师需求自己人为修改。工程的复杂度,以及工程师的代码水平都可能影响栈空间的大小。

1.3.2.2 栈空间的失败案例
cpp 复制代码
#include <stdio.h>
#include <string.h>

// 1. 定义包含数组的结构体
typedef struct {
    char name[50];      // 字符数组
    int scores[512];    // 整数数组
    int count;          // 成绩数量
} Student;

// 2. 值传递(会拷贝整个结构体)
void printByValue(Student s) {
    printf("【值传递】姓名: %s, 首个成绩: %d\n", s.name, s.scores[0]);
}

// 3. 指针传递(推荐,只读操作)
void printByPtr(const Student *s) {
    printf("【指针传递】姓名: %s, 首个成绩: %d\n", s->name, s->scores[0]);
}

// 4. 指针传递(用于修改数据)
void updateScore(Student *s, int index, int newScore) {
    s->scores[index] = newScore; // 直接修改原结构体中的数组元素
}

int main() {
    Student stu;
    strcpy(stu.name, "Alice");
    stu.scores[0] = 85;
    stu.count = 1;

    printByValue(stu);      // 传入结构体副本
    printByPtr(&stu);       // 传入结构体地址
    updateScore(&stu, 0, 100); // 通过指针修改原数据
    
    printf("修改后的首个成绩: %d\n", stu.scores[0]);
    return 0;
}

结构体的内存占用说明。

  • char name[50] = 50 字节
  • int scores[512] = 512 × 4 = 2048 字节
  • int count = 4 字节
  • 加上内存对齐,总大小大约在 2100 字节左右。
  1. 进入 main 函数(栈空间消耗 ~2100 字节)

当程序进入 main 函数时,编译器会在栈上为局部变量 stu 分配内存。因为结构体里有 512 个 int,所以 stu 会占用约 2100 字节的栈空间。

此时,栈指针(SP)向下移动了 2100 字节。

  1. 调用 printByValue(stu)(栈空间再次消耗 ~2100 字节)

因为 printByValue 的参数是按值传递 (没有带 * 号),C 语言的规则是:把实参的值原封不动地复制一份给形参

在单片机底层,这通常是通过一段汇编指令(比如 LDM/STMmemcpy)把 main 栈里的这 2100 字节,硬生生地再拷贝一份 ,压入到 printByValue 函数自己的栈帧中。

此时,栈指针(SP)会继续向下移动 2100 字节

工程中的栈总共只有 1024 字节,而仅仅传一次参数就需要 2100 字节,这远远超出了栈的极限,必然引发 Stack Overflow(栈溢出)。

1.3.2 片上外设区

第2章 ARM单片机的启动模式

通过对单片机的引脚进行配置,单片机的启动方式大致分为三种:

(1)从0x0800 0000 - 0x082F FFFF:Main Flash方式启动。

(2)从0x1FFF B000 - 0x1FFF F7FF:Bootloader方式启动。

(3)从片上SRAM启动。

(1)和(2)这两种方式我们经常用。

第3章 单片机的复位

在嵌入式单片机(比如你之前看的 GD32、STM32 等 ARM Cortex-M 内核)中,程序的最开始确实是一张"中断向量表"(Interrupt Vector Table) 。这张表本质上是一个**"指针数组"**(里面存放的全是内存地址)。

  • 第 0 个位置(地址 0x00000000) :存放的是初始主栈指针(MSP)的值。CPU 上电的第一件事,就是把这个值加载到 SP 寄存器里,从而开辟好内存堆栈空间。
  • 第 1 个位置(地址 0x00000004) :存放的是复位中断服务函数(Reset_Handler)的地址 。CPU 初始化好栈之后,就会跳转到这里,开始执行我们之前分析过的启动汇编代码(SystemInit -> __main -> main)。
  • 后续的位置:依次存放着 NMI、硬件错误(HardFault)、定时器中断、串口中断等各种异常和中断的入口地址。

3.1 中断向量表

3.2 上电复位或系统复位

CPU 上电复位时,它的硬件逻辑被设计为固定去 0x0000 0000 这个地址取数据(读取 MSP 和 PC)。CPU 根本不关心这个地址背后到底是物理内存的哪一块,它只管这个绝对地址。

0x0000 0000 - 0x002F FFFF : Aliased to Main Flash or Boot loader (别名映射)

这就是单片机的"地址映射(Mapping)"机制。

  • 物理上:你的中断向量表确实存放在 Main Flash 的最开头,即物理地址 0x0800 0000 开始的地方。
  • 逻辑上:单片机通过内部硬件电路(内存别名),把物理地址 0x0800 0000 "投射"(镜像)到了 0x0000 0000。也就是说放访问0x0000 0000地址时,其实是访问了0x08000 0000这个地址。

至此单片机上电复位或系统复位首先需要做的两件事情是:

(1)从地址0x0000 0000处取出MSP的初始值(栈顶地址),用于函数分配内存栈空间;

(2)从地址0x0000 0004处取出PC的初始值,这个值是Reset Handler复位函数的地址,然后从这个地址开始执行程序。

观察并分析上图我们发现:

  1. 第1个红框,也就是说Main Flash的第1个地址存放的是栈顶地址。栈顶地址为0x20000428。

  2. 第2个红框,也就是说Main Flash的第2个地址存放的是Reset Handler复位函数的地址Reset Handler函数的地址为0x08000145。

  3. 也就是说Main Flash的第3个地址存放的是NMI_Handler函数的地址NMI_Handler函数的地址为0x0800014D。

  4. 也就是说Main Flash的第4个地址存放的是HardFault_Handler函数的地址HardFault_Handler函数的地址为0x0800014F。

  5. 也就是说Main Flash的第5个地址存放的是MemManage_Handler函数的地址MemManage_Handler函数的地址为0x08000151。

  6. 也就是说Main Flash的第6个地址存放的是BusFault_Handler函数的地址BusFault_Handler函数的地址为0x08000153。

依此类推。

3.3 main函数的反汇编

  1. 0x0800044a 地址里存放是一条"跳转指令(命令),跳转指令要跳转到0x0800018C去执行。
  • 0x0800044a :这是当前 main 函数所在的地址
  • 0x800018c :这才是真正的延时函数初始化代码(DelayInit)所在的物理地址
  • BL指令是跳转去执行完毕之后,要返回来的。
  1. 0x0800044e 地址里存放是一条"跳转指令(命令),跳转指令要跳转到0x0800020C去执行。

  2. 0x08000452 地址里存放是一条"跳转指令(命令),跳转到 0x08000498(即 main 函数起始地址偏移 78 个字节的位置)。

  • 这里使用的是 B 指令(普通跳转),意味着它只去不回 。CPU 跳到 0x08000498 后,就不会再回到 0x08000452 了,直接进入了主循环。

3.3 存储器映射

再次重复单片机的"地址映射(Mapping)"机制。

  • 物理上:你的中断向量表确实存放在 Main Flash 的最开头,即物理地址 0x0800 0000 开始的地方。
  • 逻辑上:单片机通过内部硬件电路(内存别名),把物理地址 0x0800 0000 "投射"(镜像)到了 0x0000 0000。也就是说放访问0x0000 0000地址时,其实是访问了0x08000 0000这个地址。

第4章 工程分析

相关推荐
2601_949499941 小时前
芯瑞科技 DT‑1414完全兼容HFBR‑1414TZ光模块国产化优选方案深度解析(工程师视角)
运维·网络·人工智能·科技·光模块
一路向北finish1 小时前
《CentOS7 OpenStack Mitaka 全栈部署实战:双节点搭建全流程与典型故障排坑指南》
运维·mysql·serverless·云计算·openstack
XUEYUAN52121 小时前
代理日志分析与监控:代理池健康状态巡检与分级告警体系搭建(运维实战)
运维·网络·网络协议·tcp/ip·算法·架构
Fcy6481 小时前
Linux下 Socket编程 —— UDP网络编程
linux·网络·udp
guo_wen_qiang1 小时前
mac中iTerm有没有类似xshell远程连接服务器的用户名密码方式,不用每次都要输入用户密码
运维·服务器·macos
xzl041 小时前
Ubuntu24.04 生产环境 Docker(全国内源)
运维·docker·容器
lajidecrd1 小时前
Ubuntu 24.04 LTS安装
linux·运维·ubuntu
沫璃染墨2 小时前
《从零入门Linux系统篇(二十九):文件篇·二——深入文件描述符:从文件描述符表到重定向,再到Shell实现》
linux·运维·服务器·c++·驱动开发·系统架构
jing.wang_20252 小时前
制作最小Linux(Ubuntu)系统
linux·嵌入式硬件·ubuntu·dsp开发