3. 存储器映射与启动流程:Flash、SRAM、Bootloader、向量表
好,咱们进入第三章。这一章讲的是单片机启动时的那点事——说白了,就是芯片上电后,从哪取第一条指令,怎么把程序跑起来。
很多初学者写好了代码,下载进去,灯亮了,就完事了。但一旦遇到程序跑飞、中断不响应、Bootloader 升级失败,就抓瞎了。我当年也栽过跟头,所以这一章咱们把底层的映射关系和启动流程彻底理清楚。
3.1 存储器映射——芯片的“地址地图”
每个单片机都有一个固定的地址空间,比如 32 位地址总线能寻址 4GB 空间。芯片厂商把 Flash、SRAM、外设寄存器等分别映射到不同的地址段。这就是存储器映射。
以 STM32F103 为例,它的映射大概是这样的:
| 起始地址 | 结束地址 | 区域 | 用途 |
|---|---|---|---|
| 0x0000 0000 | 0x0000 FFFF | 别名区 / 启动区 | 启动时映射 Flash 或系统存储器 |
| 0x0800 0000 | 0x0801 FFFF | Flash 主存储区 | 存放代码和常量 |
| 0x1FFF F000 | 0x1FFF F7FF | 系统存储器 | 内置 Bootloader |
| 0x2000 0000 | 0x2000 4FFF | SRAM | 运行时数据、堆栈 |
| 0x4000 0000 | 0x4002 3FFF | 外设寄存器 | GPIO、USART、TIM 等控制 |
你看,Flash 和 SRAM 的地址是固定的。但 0x0000 0000 这个区域比较特殊——它是个“别名区”,启动时会被映射到真正的物理存储器上。
核心要点:CPU 复位后,从 0x0000 0000 取向量表,但这个地址不一定对应 Flash。具体映射到哪,由 BOOT 引脚决定。
3.2 启动模式选择——BOOT 引脚说了算
大多数 Cortex-M 单片机都有两个 BOOT 引脚。不同的电平组合,决定了启动时 0x0000 0000 映射到哪块物理存储器。
| BOOT1 | BOOT0 | 启动区域 | 典型用途 |
|---|---|---|---|
| X | 0 | Flash 主存储区 | 正常运行用户程序 |
| 0 | 1 | 系统存储器 | 出厂 Bootloader(串口下载) |
| 1 | 1 | 内嵌 SRAM | 调试或临时运行 |
我个人习惯把 BOOT0 拉低,直接让芯片从 Flash 启动。只有在需要串口下载固件时,才把 BOOT0 拉高,BOOT1 拉低,进入系统存储器的出厂 Bootloader。
小技巧:量产时,如果不想留 BOOT 跳线,可以用程序在 Flash 里实现软件跳转,模拟 Bootloader 功能。我做过一个产品,外壳全密封,只能靠 IAP 升级,就是这个思路。
3.3 向量表——中断的“总索引”
向量表是单片机启动时第一个要读的数据结构。它放在 Flash 的最开头,也就是 0x0800 0000 地址(如果从 Flash 启动)。
向量表的前两个条目至关重要:
- 第 0 项(地址偏移 0x0000):栈顶指针 MSP 的初始值。CPU 复位后,直接把这个值加载到 SP 寄存器。
- 第 1 项(地址偏移 0x0004):复位中断服务函数 Reset_Handler 的地址。CPU 加载完 SP 后,立即跳转到这个地址执行。
后面的条目依次是各种异常和中断的入口地址:NMI、HardFault、SysTick、GPIO、USART……每个占 4 字节。
为什么会这样设计?因为 Cortex-M 内核规定,中断来临时,硬件自动从向量表取出对应地址,压栈后跳转。不需要软件查表,速度极快。
我曾经踩过的坑:有一次我修改了链接脚本,把向量表偏移到了 0x0801 0000,但忘了在代码里重新设置 SCB->VTOR 寄存器。结果中断一触发,CPU 跑到原来的向量表取地址,直接 HardFault。排查了整整半天。
3.4 启动流程——从复位到 main()
咱们把整个启动流程串起来。芯片上电后,硬件自动完成以下步骤:
- 复位释放:CPU 从 0x0000 0000 读取栈顶指针,存入 SP。
- 取复位向量:从 0x0000 0004 读取 Reset_Handler 地址,存入 PC。
- 执行 Reset_Handler:这是启动文件(startup_xxx.s)里的汇编函数。
- 初始化数据段:把 Flash 中的 .data 段复制到 SRAM,把 .bss 段清零。
- 调用 SystemInit:配置时钟、PLL、Flash 等待周期等。
- 跳转 main():准备好 C 运行时环境后,调用 main 函数。
你看,从复位到 main(),中间做了不少事。如果启动文件写错了,或者链接脚本把段地址搞乱了,程序根本跑不到 main。
避坑指南:我建议你每次新建工程时,先看一眼启动文件里的栈大小。默认 1KB 或 2KB 往往不够,特别是用了 printf、RTOS 或大量局部变量时。我曾经在项目后期才发现栈溢出,改起来非常痛苦。
3.5 Bootloader——让固件自己升级自己
Bootloader 是一段放在 Flash 开头的小程序。它的作用是:上电后先运行,判断是否需要升级固件。如果需要,就通过串口、USB 或无线接收新固件,写入 Flash;如果不需要,就跳转到用户程序。
典型的 Flash 布局如下:
| 地址范围 | 内容 | 大小 |
|---|---|---|
| 0x0800 0000 - 0x0800 3FFF | Bootloader 程序 | 16KB |
| 0x0800 4000 - 0x0801 FFFF | 用户应用程序 | 112KB |
Bootloader 跳转到用户程序时,需要做两件事:
- 重新设置向量表偏移:把 SCB->VTOR 设为 0x0800 4000。
- 获取用户程序的栈顶和复位向量:从 0x0800 4000 和 0x0800 4004 读取,然后跳转。
跳转代码通常用 C 内嵌汇编写:
/* 跳转到用户应用程序 */
void jump_to_app(uint32_t app_addr)
{
uint32_t msp = *(uint32_t *)app_addr; // 取栈顶
uint32_t reset = *(uint32_t *)(app_addr + 4); // 取复位向量
__set_MSP(msp); // 设置主栈指针
SCB->VTOR = app_addr; // 偏移向量表
/* 跳转 */
void (*app_entry)(void) = (void (*)(void))reset;
app_entry();
}
注意:跳转前要关闭所有外设中断、关闭全局中断,否则用户程序初始化时可能被残留的中断打断。我习惯在跳转前调用 DeInit 函数清理一遍。
3.6 知识结构图
下面这张图把本章的核心关系画出来了。从复位到启动,再到 Bootloader 跳转,一目了然。
3.7 本章小结
存储器映射是单片机地址空间的骨架。启动流程是芯片上电后的第一段旅程。向量表是中断响应的总索引。Bootloader 则是实现固件升级的桥梁。
嗯,这一章的内容比较底层,但非常重要。你想想看,如果连芯片从哪里启动、向量表怎么放都不清楚,出了问题根本无从下手。
我个人建议你打开一个实际工程的启动文件(.s 文件)和链接脚本(.ld 文件),对照着看一遍。把每个段、每个符号的地址算一算,印象会深很多。