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()

咱们把整个启动流程串起来。芯片上电后,硬件自动完成以下步骤:

  1. 复位释放:CPU 从 0x0000 0000 读取栈顶指针,存入 SP。
  2. 取复位向量:从 0x0000 0004 读取 Reset_Handler 地址,存入 PC。
  3. 执行 Reset_Handler:这是启动文件(startup_xxx.s)里的汇编函数。
  4. 初始化数据段:把 Flash 中的 .data 段复制到 SRAM,把 .bss 段清零。
  5. 调用 SystemInit:配置时钟、PLL、Flash 等待周期等。
  6. 跳转 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 跳转,一目了然。

存储器映射与启动流程结构图 存储器映射与启动流程 Flash 0x0800 0000 SRAM 0x2000 0000 系统存储器 0x1FFF F000 外设寄存器 BOOT0 / BOOT1 引脚决定 0x0000 0000 映射到谁 BOOT0=0 → Flash | BOOT0=1,BOOT1=0 → 系统存储器 | BOOT0=1,BOOT1=1 → SRAM 启动流程(硬件 + 启动文件) ① 取栈顶 → ② 取复位向量 → ③ 初始化 .data / .bss ④ SystemInit → ⑤ 跳转 main() Bootloader 模式 接收固件 → 写入 Flash → 跳转 用户程序运行 正常执行 main 中的业务逻辑 跳转 核心:向量表偏移 + 栈顶/复位向量正确传递,是 Bootloader 跳转成功的关键

3.7 本章小结

存储器映射是单片机地址空间的骨架。启动流程是芯片上电后的第一段旅程。向量表是中断响应的总索引。Bootloader 则是实现固件升级的桥梁。

嗯,这一章的内容比较底层,但非常重要。你想想看,如果连芯片从哪里启动、向量表怎么放都不清楚,出了问题根本无从下手。

我个人建议你打开一个实际工程的启动文件(.s 文件)和链接脚本(.ld 文件),对照着看一遍。把每个段、每个符号的地址算一算,印象会深很多。