嵌入式烧录地址全解析:0x08000000、0x00000000与0x6000到底啥关系?
2026/9/14 15:55:20 网站建设 项目流程

搞嵌入式开发的朋友,应该都遇到过这种困惑:同样是烧录程序,有时候编译器里 Flash 起始地址填 0,有时候填 0x08000000,换个带 Bootloader 的工程又要填 0x6000,这三者到底什么关系?网上资料东一句西一句,今天我把这块彻底说透。

先说结论,这三个地址背后其实是三套不同的逻辑:0x08000000 是 STM32 这类 Cortex-M 内核芯片内部 Flash 的实际起始地址,0x00000000 是芯片上电后 CPU 取向量表时映射出来的“假地址”,而 0x6000 则是你在 Bootloader 后面放应用时人为算出来的偏移地址。搞清楚这三者的关系,你不仅不会再填错,遇到程序跑飞、无法跳转、擦除错扇区这类问题,也能自己排查出个大概方向。

这篇文章是我在实际调试中踩了不少坑之后整理的,会结合原理、Keil/IAR 工程配置、STM32CubeProgrammer、ESP32 的 esptool 等真实场景,从“三个地址为什么不一样”一直讲到“我到底该怎么确认当前板子的烧录地址”,适合刚入门的小白,也适合被 Bootloader 跳转折磨过的老手。文章没有任何水分,全程干货。

1. 三个烧录地址,先分清各是谁家的“门牌号”

1.1 0x08000000:Cortex-M 内核芯片内部 Flash 的真正起点

如果你用 Keil 新建一个 STM32 工程,打开 Options for Target -> Target 标签页,IROM1 的起始地址默认就是 0x08000000,大小根据芯片型号填 0x80000(512KB)、0x100000(1MB)等。这个地址就是芯片内部 Flash 的物理起始地址,由芯片设计厂商决定,ST 的 Cortex-M 系列基本都是这个值。

为什么非要放在这个位置,而不是从 0 开始?这牵扯到 ARM Cortex-M 内核的存储器映射规则。简单打个比方,Cortex-M 内核就像一个“规定好各房间位置”的公寓楼,0x00000000 到 0x1FFFFFFF 这段是给代码区准备的,0x20000000 到 0x3FFFFFFF 是 SRAM 区,0x40000000 开始才是外设区。ST 把内部 Flash 物理地放在 0x08000000,属于代码区里靠后的一段。因为规定得死,所以这个地址对所有 STM32 基本统一。

有个细节容易混淆:物理地址 0x08000000 是 Flash 出厂就被硬件固定映射的,但程序里如果直接声明const uint32_t data = 0x55;,这个常量有机会落在 Flash 里,并不代表所有只读数据都从 0x08000000 开始排。只能说,烧录器把二进制文件烧进 Flash 时,默认的基地址就是它。

1.2 0x00000000:启动时的“影子地址”和映射逻辑

很多入门教材会说“STM32 上电后从 0x00000000 取栈顶指针,从 0x00000004 取复位向量”,于是有人就困惑了:那到底是从 0 启动还是从 0x08000000 启动?

答案是:CPU 从 0x00000000 读取,但 0x00000000 并不是 Flash 本身,而是一个可以重映射的通路。芯片上电时,BOOT0/BOOT1 引脚决定了 0x00000000 这个区域映射到什么物理介质上:

  • 从主 Flash 启动:0x00000000 映射到 0x08000000,读 0 地址实际读的是 Flash 的最前面;
  • 从系统存储器启动:0x00000000 映射到系统区内置 Bootloader 的地址;
  • 从 SRAM 启动:0x00000000 映射到 0x20000000。

这就是为什么“烧录地址填 0”在某些场景下也能跑起来。严格说,烧录工具最终写入的还是物理 Flash 地址 0x08000000,只是部分工具或一些极简的裸机工程在描述“起始地址”时,直接从用户视角的 0 开始写。你可以把它理解为“系统看到的地址”和“物理存在的地址”这两套坐标。调试器里看 PC 指针复位后的值,通常会停在 0x08000000 附近的地址上,本质上就是从映射后的 Flash 区域开始执行的。

1.3 0x6000:偏移量,短小却不简单

0x6000 这个值十六进制展开是 0x6000,十进制 24576,也就是 24KB。在 STM32 的上下文中,它不像 0x08000000 那样是芯片手册里写死的物理地址,而是一个“偏移量”或者“应用起始地址”。

最常见的情况是:产品里先运行一段 Bootloader(放在 Flash 最开头,即 0x08000000),然后引导跳转到真正的应用程序,而应用程序不从头放,而是放在 0x08000000 + 0x6000 的位置。为什么是 0x6000?因为 Bootloader 的镜像大小是 24KB,预留出 24KB 给它,应用程序从第 24KB 处开始放,自然就是 0x6000 偏移。

也可能是你用的外置 SPI Flash,Nor Flash 的起始地址在某些映射方案里写作 0x6000,但这相对少见。多数人遇到 0x6000,都是 Bootloader + App 分区方案里应用工程的 Flash 起始地址。

2. 为什么 Flash 不直接从 0x00000000 开始?

2.1 ARM Cortex-M 的存储器映射规则

前面提到 Cortex-M 内核规定了一套地址划分,本质上是为了让同一套编译器、调试器、RTOS 能适配不同芯片厂商的产品。如果每个厂商的 Flash 地址都不一样,工具链没办法统一支持,所以 ARM 规定代码区必须放在 0x00000000 到 0x1FFFFFFF。

问题来了:既然整段 0x00000000~0x1FFFFFFF 都是代码区,那芯片厂商可以自由选择把 Flash 放在这个区间的任意位置吗?可以,但业界普遍的做法是放在 0x08000000,既不会和 0x00000000 附近的“启动映射区”冲突,又留有充足空间。Flash 不直接放 0 地址,还有一个重要原因:0 地址附近往往还要承担中断向量表、启动配置、系统存储器等功能,如果 Flash 物理上也从 0 开始,一旦启动模式变化就容易冲突。

2.2 从 0 到 0x08000000 的“跳转”逻辑

有人会问:那我直接用调试器把程序下载到 0x08000000,上电后 CPU 从 0x00000000 开始取指,它怎么知道去 0x08000000 找代码?

答案是硬件自动映射,不需要软件干预。芯片内部有一个启动配置逻辑,根据 BOOT 引脚的电平状态,决定将哪个物理存储器的地址映射到 0x00000000。如果选择从主 Flash 启动,那么 CPU 在 0x00000000 和 0x00000004 取到的内容,就等同于 0x08000000 和 0x08000004 的内容。

这也是为什么你可以用__attribute__((section(".isr_vector")))把向量表放在任意位置,但不代表启动时 CPU 会自动去那个位置找向量表。向量表偏移需要软件设置SCB->VTOR寄存器,否则 CPU 还是老老实实从 0x00000000 拿复位向量。

2.3 外置 Flash 与 0x60000000 的扩展

还有一个容易混的地址是 0x60000000。在 STM32 的 FSMC/FMC 控制器里,Bank1 的起始地址就是 0x60000000,用于映射外部 NOR Flash、PSRAM 等。如果你听到有人说“外部 Flash 地址 0x6000”,千万别和外置存储控制器地址搞混,0x6000 只是 24KB 偏移,0x60000000 是整整 1.5GB 之后的一个段,两码事。

不过在一些带外部 Flash 启动的芯片方案里,比如从外部 Nor Flash 启动的 i.MX RT 系列,它的启动地址确实会在 0x60000000 附近,因为芯片允许直接从外部存储器映射执行代码(XIP)。所以当你在恩智浦、华邦等方案的文档里看到 0x60000000 字样,这就是另一条技术路线了。

3. 0x6000 的来龙去脉:Bootloader 与应用分区

3.1 为什么 Bootloader 要占前面一段?

做产品级固件,几乎不可能只有一个裸机程序从头写到尾。你需要远程升级、需要校验固件完整性、需要在应用卡死时还能恢复,所以常见方案是:Flash 最前面放 Bootloader,Bootloader 负责检查是否有新固件、是否触发跳转,后面区域放应用 App。

Bootloader 必须放在 Flash 起始位置 0x08000000,因为芯片上电后第一段执行的代码就是它。App 则不能覆盖 Bootloader 区域,所以给它分配一个偏移地址。这个偏移是多少,完全由你的工程规划决定,0x6000 只是其中一种选择,偏移 0x8000(32KB)、0x10000(64KB)的也都有。

3.2 0x6000 是怎么算出来的?

如果 Bootloader 编译完的固件大小是 0x5A00 字节(约 23KB),你会预留多少空间给它?如果只留 0x5A00,App 紧贴其后,一旦 Bootloader 后续加了功能、改了编译选项,体积变大,就会把 App 的开头覆盖掉。所以工程上习惯按扇区对齐,并留出余量。

STM32 内部 Flash 的扇区大小不是均匀的,以常见的 STM32F103 为例,前 4 个扇区每个 16KB,后面的是 64KB。如果你的 Bootloader 固件是 20KB,逻辑上需要占用 2 个 16KB 扇区,那预留 32KB 也就是 0x8000 更合理。如果 Bootloader 压缩到 10KB 以内,预留一个 16KB 扇区再往后推一点,用 0x6000 也就说得通——16KB 是 0x4000,再加点余量变成 0x6000,或者这个数直接来自你 Bootloader 工程里<scatter file>.icf文件的定义。

我自己遇到过一种更尴尬的情况:Bootloader 编译出来 23.5KB,当时图省事把 App 起始地址设成 0x6000(24KB),结果半年后改了 Bootloader 一次功能,体积涨到 25KB,App 开头直接被覆盖,产品在 OTA 后集体变砖。从此我学乖了:Bootloader 区域必须比实际固件至少大 1 倍以上,并且要在链接脚本里把 Bootloader 区域末尾的“哨兵值”或 CRC 校验固定下来,一旦超规划马上报警。

3.3 偏移之后,向量表也要跟着搬家

App 放在 0x08000000 + 0x6000 之后,你需要在 App 工程的启动文件里设置向量表偏移。Cortex-M 内核提供了向量表偏移寄存器(VTOR),在 SystemInit() 函数或者 main() 最开始设置:

#define APP_ADDRESS 0x08006000 SCB->VTOR = APP_ADDRESS;

如果 App 使用 RTOS,还需要确认vPortSVCHandler等中断的向量名有没有正确匹配,否则一旦发生系统调用,会跳到 Bootloader 的中断向量表里去,程序立刻跑飞。

一个比较容易忽略的坑:擦除 Flash 的时候,如果 App 里的 IAP 程序需要擦除自身所在扇区,而你的扇区计算是按 0x08000000 为基地址算的,App 运行在 0x08006000,很容易算错擦除范围。我见过有人 App 里做在线升级,结果升级时把 Bootloader 扇区给擦了,断电后整个设备变砖,只能重新用烧录器连 SWD 恢复。排查这种问题最好的办法,是在擦除前打印目标扇区地址,和地图比对。

4. 实操:怎么看、怎么确认当前板子的烧录地址

4.1 从 IDE 和链接脚本出发

拿到一个别人的工程,第一件事就是看烧录地址。用 Keil 的直接打开 Options for Target -> Target,找到 IROM1 的起始地址;用 IAR 的打开 Project -> Options -> Linker,看 Config 里的#define或者 .icf 文件里对FLASH起始地址的定义;用 STM32CubeIDE 的看链接脚本 .ld 文件:

MEMORY { FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 512K RAM (xrw) : ORIGIN = 0x20000000, LENGTH = 128K }

如果 ORIGIN 写的是 0x08006000,那这个工程就是偏移过的 App。如果 ORIGIN 写 0x08000000,那它是从 Flash 最开头直接运行的裸机工程或者 Bootloader。

4.2 用命令行工具确认

不依赖 IDE 也可以确认固件本身的地址信息。编译生成的 .hex 文件里,每一行记录都有地址字段,比如:

:020000040800E2

这行里的 0800 表示扩展地址是 0x0800,后面数据的基地址就是 0x08000000。可以用文本打开 hex 文件,搜索第一行数据记录,看扩展地址段写的是什么。用 bin 文件的话,则需要用烧录工具或脚本读偏移。

用 STM32 的话,装一个 STM32CubeProgrammer,连上板子后执行:

STM32_Programmer_CLI -c port=SWD mode=UR -r8 0x08000000 256

这条命令从 0x08000000 读 256 字节,你可以对比读到的内容是否和你编译的二进制前缀一致。如果 App 在 0x08006000,那 0x08000000 处读到的很可能是 Bootloader 的向量表,两个地址开头的内容会有明显差异。

4.3 串口打印与调试器读取实测

更直观的方式是在代码里打印当前程序所在的位置。比如在 App 的 main() 开头加一句:

printf("App run, vector table at 0x%X\r\n", (unsigned int)SCB->VTOR); printf("Flash base: 0x%X\r\n", (unsigned int)&__Vectors);

注意__Vectors的地址通常在链接脚本里被定义为段起始地址,可以直接反映出当前镜像被链接到哪个地址。如果打印出来是 0x08006000,就证明这个 App 确实跑在偏移地址处。还有一种方法是调试器暂停后看 PC 指针,如果 PC 停在 0x08006xxx,说明程序执行流确实来自偏移区,配合反汇编窗口看函数地址就一目了然。

用 J-Link 的话,命令行执行:

JLinkExe -device STM32F103C8 -if SWD -speed 4000 -autoconnect 1 mem32 0x08000000 16

读出来 0x08000000 的内容如果和 Bootloader 的.bin开头一致,那就是 Bootloader;再用mem32 0x08006000 16读一下,如果内容和 App 的.bin前缀一致,说明 App 确实放在了偏移地址。

5. ESP32 的烧录地址又是另一套逻辑

5.1 ESP32 的 Flash 布局

这个话题最近热度很高,很多人从 STM32 转到 ESP32 之后,对烧录地址一头雾水。ESP32 与 STM32 有个本质区别:它没有内部 Flash,而是通过 SPI 接口外挂一块 Flash 芯片,CPU 通过 Cache 映射方式直接在这个 SPI Flash 上执行代码,所以它的地址体系和 STM32 完全不同。

ESP32 默认的分区方案里,烧录地址大概是这样:

  • bootloader:一般烧到 0x1000
  • partition table(分区表):一般烧到 0x8000
  • 应用程序(factory app):一般烧到 0x10000

看到 0x1000、0x8000、0x10000 这些短地址,别用 STM32 的思维去套,它不是“偏移 4KB”,而是整个 Flash 的起始地址就这么规划。ESP32 的 Flash 起始地址是 0x0000,芯片上电后 ROM 里的 bootloader 会从 0x1000 加载二级 bootloader,再根据分区表找到应用入口。所以你看 ESP32 的烧录地址,经常是直接用esptool.py write_flash 0x1000 bootloader.bin 0x8000 partitions.bin 0x10000 firmware.bin这种命令行方式,地址和文件一一对应。

5.2 怎么看 ESP32 当前烧录地址

查看 ESP32 的烧录地址,最直接的方法是看工程生成的 partition CSV 文件。在 ESP-IDF 里,partitions.csv会写明每个分区的类型、偏移和大小,比如:

# Name, Type, SubType, Offset, Size factory, app, factory, 0x10000, 1M

你用idf.py partition-table生成的二进制分区表烧到 0x8000,之后 bootloader 就是靠这张表找到 app 的偏移地址。也可以用 esptool 回读:

esptool.py -p COM3 read_flash 0x10000 0x1000 app_dump.bin

这会把偏移 0x10000 处的 4KB 读出来,用十六进制编辑器看是不是你的 app 开头。更省事的办法是idf.py monitor启动后看启动日志,ESP-IDF 的 bootloader 启动时会在日志里打出一行类似:

I (30) boot: Loaded app from partition at offset 0x10000

这就直接告诉你当前是从哪个偏移启动的。如果你用 Arduino 生态,esptool.py -p COM3 read_flash 0x10000 0x1000 dump.bin同样有效,Arduino 的 bootloader 通常也在 0x1000,分区表在 0x8000,app 偏移是 0x10000。

6. 烧录地址相关的常见坑与排查实录

6.1 烧了 App 之后程序跑飞,但 Bootloader 单独烧又能跑

这个情况八成是 App 偏移和向量表设置不匹配。Bootloader 跳转到 App 时,需要先把 App 的向量表设好,具体做法是读 App 起始位置的栈顶指针(SP)和复位向量(PC),然后设置 VTOR,再跳转。如果你的 Bootloader 在跳转前没有正确拿到 App 的向量表地址,或者 App 内部又把 VTOR 改错了,就会表现为:单独烧 Bootloader 没问题,单独烧 App 也能跑(因为从 0 启动时默认映射),但组合在一起跳转过去就死。

排查思路:先在 Bootloader 跳转前把 App 地址区域的 8 个字节读出来打印,确认栈顶指针是否指向有效 RAM 区域,复位向量是否落在 App 的 Flash 范围。栈顶指针是一个 RAM 地址,比如 0x20002000,复位向量应该在 0x0800xxxx 区间。如果读出来的值是 0xFFFFFFFF,说明 App 根本没烧进去,或者烧录地址和你设置的偏移不一致。

6.2 用下载器烧录时地址填错,芯片直接“变砖”

这是新手最容易犯的错:用 ST-Link 下载程序时,在烧录工具的“编程地址”或“下载地址”里直接填了 0x08006000,这是不对的。烧录工具默认会把你的 .hex/.bin 烧到芯片的物理 Flash 起始地址,也就是 0x08000000。如果你想做 Bootloader + App 分区,不是让下载器从 0x08006000 开始“物理写”,而是要在链接脚本里把 App 的代码段地址设成 0x08006000,烧录器仍然从 0x08000000 开始,把你编译出的 App 的机器码写到 0x08000000 + 0x6000 处。

怎么解决?如果用的是 .hex 文件,它自带地址信息,烧录器按 hex 里的地址写入,不需要你填偏移;如果用 .bin 文件,烧录工具往往会让你指定烧录地址,这时你要填 0x08006000,工具才会把 bin 的内容放到偏移处。简单记:hex 自带门牌号,bin 不带,需要你告诉它门牌号。

还有一种情况是 OTA 升级时,固件包里既有 Bootloader 又含 App,Bootloader 在跳转前会对 App 做 CRC 校验,如果你 OTA 工具写入 App 时基地址和 App 编译时链接地址不一致,校验会失败,出现“升级成功了但一直跳不过去”的假现象。排查时先确认生成 OTA 包时的地址参数和后端下发的地址参数是否一致,这个坑我见人踩过不止三次。

6.3 设置了偏移之后还是从 0 执行?

有一个非典型的坑:你在 App 工程里设置了偏移地址 0x6000,也设置了SCB->VTOR,但程序一上电还是从 0x08000000 执行。这通常是你的 App 里包含了一个全量擦除 Flash 的 IAP 功能,执行时把 0x08000000 开头的区域也擦掉了,或者你的下载器配置成了“整片擦除”,烧 App 的时候把 Bootloader 一起擦掉了。

另外也检查一下是不是中断向量表段名写错。GCC 环境下,启动文件把.isr_vector段放在 FLASH 最前面,如果你自定义了段名,链接脚本里的.isr_vector没有跟着改,向量表就会被放在别的段后面,导致 CPU 上电时取到的复位向量并不是你的 main。排查办法是反汇编看生成的 .map 文件,__isr_vector地址如果在 0x08006000 附近,说明放置正确;如果在 0x08000000,那你的偏移设置其实没生效,重新检查链接脚本的段布局。

6.4 擦除扇区时地址算错,把 Bootloader 干掉了

这个话题在前面提过,值得单独列一条。在 App 里做 Flash 擦除时,不同芯片的扇区大小不一样,STM32F103 前 4 个扇区是 16KB,之后是 64KB;STM32F407 的扇区布局又完全不同,前 4 个 16KB,第 5 个 64KB,后面还有 128KB。

如果你要擦除 0x08006000 开始的一个扇区,不能简单往上加 4KB 再按 16KB 对齐。正确做法是拿 Flash 编程手册里的扇区映射表核对。我写了一个简单的扇区偏移计算函数,每次擦除前都校验目标地址是否落在 Bootloader 保护区域之外,低于阈值直接报错返回:

#define BOOTLOADER_END_ADDR 0x08006000 #define APP_START_ADDR 0x08006000 static uint32_t flash_sector_start(uint32_t addr) { if (addr >= 0x08000000 && addr < 0x08004000) return 0x08000000; if (addr >= 0x08004000 && addr < 0x08008000) return 0x08004000; return 0xFFFFFFFF; }

这套方法虽然粗暴,但能阻止我在调试阶段因为算错基地址而把 Bootloader 整片擦掉。把保护范围写在宏里而不是靠人记,这才是嵌入式代码该有的态度。

7. 烧录地址速查表与几条实用结论

如果你时间紧,不想看原理,这里直接给你一张速查表:

地址含义典型场景
0x00000000启动映射地址,硬件别名CPU上电取向量表,BOOT引脚决定映射目标
0x08000000STM32内部Flash物理起始地址无Bootloader工程、Bootloader自身、绝大多数标准工程
0x08006000内部Flash + 24KB偏移Bootloader占用前24KB时的App起始地址
0x60000000FMC/FSMC外部NOR Flash起始地址外部并行总线Flash映射
0x1000ESP32二级bootloader偏移ESP32 bootloader固件
0x8000ESP32分区表偏移ESP32 partition table
0x10000ESP32 factory app偏移ESP32应用程序默认入口

调试时记住这几条结论就够了:

  • 芯片物理 Flash 地址优先看数据手册,而不是凭经验猜,STM32 系列绝大多数是 0x08000000,但不是所有 Cortex-M 芯片都一样,有些低功耗芯片 Flash 从 0x00000000 开始。
  • Bootloader 工程和 App 工程的偏移量必须一致,且要预留足够余量,我个人的习惯是 Bootloader 区域至少留出固件实际大小的 1.5 到 2 倍,并且用编译后检查脚本卡住体积。
  • 应用工程一定要设置 VTOR,否则中断向量表错位,程序大概率一进中断就跑飞。
  • 用 bin 文件烧录时必须手动指定烧录地址,用 hex 文件则不用,但下载器如果配置了全片擦除,还是要小心。

最后再分享一个小技巧:如果你拿到一块二手板子,不知道程序是什么状态,就先读一下 0x08000000 开头的 4 个字节,看看是不是 0x2000xxxx 开头的栈顶指针,再读 0x08000004 看看复位向量地址。这两个值如果是有效的,板子大概率有可运行固件;如果全是 0xFFFFFFFF,那就是空片或者 Flash 被擦过了。这个检查方法我用了很多年,比直接上电看现象快得多,推荐你也养成这个习惯。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询