搞懂STM32与ESP32烧录地址:从0x08000000到分区偏移
2026/9/17 23:39:59 网站建设 项目流程

搞嵌入式的,早晚都会被烧录地址这个问题绊一下。我最早用 J-Link 给 STM32 烧程序的时候,Keil 里 Flash Download 那一栏写着 0x08000000,IAR 编译出来的 hex 文件打开一看地址是从 0x0 开始的,后来上手 ESP32,命令行里又冒出来 0x1000、0x8000、0x10000 一堆偏移,再往后做某个国产 Cortex-M 的项目,Bootloader 跳转表上赫然写着 0x6000。同样的"烧录"两个字,地址怎么就这么花?这不是工具在跟你开玩笑,每一个数字背后都有它必须成立的理由。这篇博文就把烧录地址这件事从头到尾讲透——为什么有的芯片从 0 开始,有的从 0x08000000 开始,有的偏偏落在 0x6000 这种看似没规律的位置;它们分别对应什么芯片架构、什么内存映射方式、什么工具视角;最后再落回到最实际的问题上:怎么看 ESP32 的烧录地址,以及遇到地址对不上时该怎么排查。不管你是刚摸单片机的学生,还是已经做过几个项目、但对这些地址还是一知半解的工程师,这篇都能给你一个能自洽的解释框架。

1. 烧录地址到底是谁定的

1.1 先分清"烧录地址"和"运行地址"

很多人一上来就把这两个概念混在一起,结果越看越糊涂。烧录地址,指的是烧录工具把一段二进制数据写进目标存储器时,写入的起始位置;运行地址,指的是 CPU 真正执行这段代码时,从哪个地址去取指令。这两个地址在有些芯片上是同一个数,在另一些芯片上则完全不是一回事,而烧录地址的"花式变化",本质上就是这两个概念在不同架构下关系的体现。

拿最典型的两种情况对比就清楚了。STM32 这类芯片,Flash 是直接挂在 CPU 的地址总线上的,代码可以原地执行(XIP,Execute In Place),Flash 的物理地址就是 0x08000000,CPU 从这个地址取指令,烧录工具也把代码写到这个地址,烧录地址和运行地址统一了。ESP32 完全是另一套路子,它的 Flash 是外挂的 SPI Flash,CPU 地址空间里根本没有这么大一块连续区域直接映射它,运行时靠 MMU 把 Flash 里的一段内容映射到内部 RAM 的某个地址去执行,所以烧录时用的是相对 Flash 起始位置的"偏移",0x1000、0x10000 这些数字指的是在 Flash 芯片内部的第几个字节开始写,而不是 CPU 地址。

理解了这一层,你就能明白为什么有人告诉你"烧到 0 就行",有人坚持"必须写 0x08000000"——他们说的场景根本不一样,一个是在讲 Flash 芯片被看作一块从零开始编址的裸存储,另一个是在讲 CPU 视角下的物理映射地址。

1.2 链接脚本和内存映射表才是真正的源头

烧录地址不是烧录工具随手编的,它来源于编译阶段生成的链接脚本(linker script)和芯片的内存映射表(memory map)。链接器在把各个目标文件拼成最终可执行文件的时候,需要知道代码段、数据段、只读数据段分别应该落在哪个地址区间,这个信息就写在链接脚本里,比如 GNU 工具链常见的 .ld 文件,或者 Keil 工程里的 Scatter File,IAR 里的 .icf 文件。

链接脚本里的地址又是根据芯片手册里的内存映射表来的。芯片厂商在设计时就把地址空间划分好了:哪一段是 Flash,哪一段是 SRAM,哪一段是外设寄存器,哪一段是系统存储器(System Memory),哪一段是选项字节区域,全都有固定的分配。链接脚本只是把这些分配如实抄下来,告诉链接器"代码就放这儿"。

所以当你看到烧录地址是 0x08000000,其实是在告诉你"这个芯片的 Flash 从 CPU 地址空间的 0x08000000 开始";看到 0x0,则可能是"这个芯片的代码从地址空间的零地址开始",也可能是"这个工具把 Flash 当成裸存储、按偏移写入"。要彻底搞清楚,最靠谱的办法是翻芯片的参考手册,找到 Memory Map 那一章,对着看,一目了然。

2. 0x08000000:XIP 架构下的物理地址

2.1 STM32 为什么把 Flash 放在 0x08000000

0x08000000 这个地址几乎已经成了 STM32 的代名词,用过 HAL 库的人对这个数字绝对不陌生。为什么偏偏是它?答案藏在 ARM Cortex-M 的地址空间规划里。

Cortex-M 系列把 4GB 的地址空间做了一个非常明确的划分:0x00000000 到 0x1FFFFFFF 是 Code 区,0x20000000 到 0x3FFFFFFF 是 SRAM 区,0x40000000 到 0x5FFFFFFF 是外设区,0x60000000 往后是外部 RAM 和外部设备区,再往上是厂商自定义和系统级区域。Code 区里,0x00000000 那一小段被规定为"别名区",上电后由芯片的启动配置决定它实际映射到哪里;而 0x08000000 开始的这一段,才是各大厂商放片上 Flash 的"标准位置"。

ST 选择了 0x08000000 作为 Flash 的物理基地址,配套的还有 0x1FFF0000 附近的系统存储器(里面固化着出厂 Bootloader,也就是平时说的 ISP 引导程序),以及 0x1FFFF800 附近的选项字节。链接脚本里写 Flash 起始 0x08000000、长度 512K,编译器就会把所有代码放在这个区间,烧录工具也照这个地址写进去。

2.2 别名区 0x00000000 与烧录地址的区别

讲到这里就得解释一个困扰无数人的现象:为什么 STM32 的启动文件里,复位向量表看起来是从 0x00000000 开始的,可烧录地址又是 0x08000000?

关键在于别名机制。上电复位后,芯片会根据 BOOT0 / BOOT1 引脚的状态,把 0x00000000 这个别名区映射到三个不同的物理区域之一:主 Flash(0x08000000)、系统存储器(0x1FFF0000)或者内嵌 SRAM。如果 BOOT 引脚选了主 Flash,那么 CPU 从 0x00000000 取到的第一条指令,实际上就是 Flash 里 0x08000000 处的第一条指令。这是一种"视角重映射",物理地址没变,只是 CPU 看到的虚拟地址不同。

烧录工具不玩这套重映射,它直接对着物理地址下手,所以写的是 0x08000000。你在 hex 文件里看到的 0x0,是因为很多 hex 文件格式(比如 Intel HEX)在生成时会做地址重定位,把 0x08000000 处的内容标成 0x0 开头的偏移,具体解释要看生成工具和命令参数,不能一概而论。

注意:判断一个 hex 文件到底烧到哪里,不能只看它开头几行的地址字段,要结合芯片手册里的 Flash 基地址和烧录工具的偏移设置一起看,否则很容易误判。

2.3 一个真实的烧录对不上案例

我碰到过一次典型的地址对不上:同事用 ST-Link Utility 烧一个 STM32F103 的 hex,工具自动识别地址是 0x08000000,烧完能跑;换成自己写的一个 Python 脚本调用 STM32CubeProgrammer 命令行,脚本里把地址显式写成了 0x0,结果死活烧不进去,报"address out of range"。

原因很简单,STM32CubeProgrammer 在处理二进制文件(.bin)时,必须显式指定烧录地址,而这个地址要按物理地址来写,也就是 0x08000000。写 0x0 的话,它会认为你要写到别名区或者更奇怪的区域,直接拒绝。如果是 hex 文件,里面自带地址信息,一般不需要手动指定地址。这个区别非常关键:bin 文件是"裸数据",没有地址信息;hex 文件、elf 文件带有地址信息。

后来我把脚本里所有涉及地址的地方改回 0x08000000,问题立刻消失。你如果也在写自动化烧录脚本,记住这条:二进制文件必须配物理地址,带地址信息的文件可以省掉,但省掉之后仍要确认文件里的地址是对的

3. 0:从零开始编址的那些芯片

3.1 AVR 和早期架构为何从 0 起步

把目光从 ARM 挪开,看看 AVR 系列。ATmega 系列芯片的 Flash 就是从 0x0000 开始编址的,程序计数器复位后也是从 0x0000 开始取指令,所以烧录地址就是 0x0,没有任何弯弯绕。原因也不复杂:AVR 的地址空间里,Flash 就是占据了最开头的一段,没有像 Cortex-M 那样专门划出一个别名区、把物理 Flash 挪到 0x08000000 这么远的地方。

这类架构的特点是结构简单直接,Flash、SRAM、EEPROM、寄存器各占一段,地址空间划分一目了然。用 avrdude 烧录的时候,命令里通常会看到-U flash:w:firmware.hex,这里的 flash 是一个区域名,具体地址由 hex 文件内部记录,你用 avrdude 直接烧 bin 文件时需要指定偏移量,但绝大多数时候烧的是 hex,所以看起来"地址就是 0"。

同样从零起步的还有不少别的芯片,比如部分 PIC 系列,早期的一些 8 位机,以及 ESP8266。ESP8266 用 esptool 烧录时,boot.bin 的地址经常写成 0x0,因为它的内部机制就是让第一段代码从 Flash 偏移 0 开始。

3.2 烧录工具里的 0 可能只是"起始偏移"

还有一个更容易产生误会的情况:工具界面上显示的 0,并不代表代码被写进了芯片地址空间的第 0 号字节,而只是代表"从 Flash 存储器的开头开始写"。尤其是在烧外部 SPI Flash 的场景里,工具根本不知道 CPU 怎么看待这块 Flash,它只知道这是一块从 0 开始编址的裸存储芯片,于是提示你"起始地址 0"。

ESP32 就是最好的例子。用 esptool.py 烧录的时候,地址参数 0x1000、0x8000、0x10000 都是相对 SPI Flash 芯片起始位置的偏移,Flash 芯片本身的第一字节就是偏移 0。你写 0x1000,意思是从 Flash 的第 4096 字节开始写,与 CPU 地址空间里 0x1000 是什么东西毫无关系。

这就解释了一个常见的困惑:为什么同一个 ESP32 项目,烧录时看到好几个地址,却都不是 0x08000000 这种"ARM 味儿"的地址?因为 ESP32 的 Flash 不挂在 CPU 地址总线上,CPU 根本不会直接去 0x1000 取指令,它通过 MMU 映射,代码的实际执行地址是 0x400xxxxx 或 0x3Fxxxxxx 这些内部 RAM 地址。烧录地址和运行地址在这里彻底分道扬镳,这也是为什么新手看 ESP32 的烧录日志会格外晕。

提示:凡是看到"偏移地址"这个词,基本可以断定是在讲外挂存储器的内部位置,而不是 CPU 地址空间。

4. 0x6000 这类地址从哪冒出来的

4.1 自定义 Bootloader 留下来的偏移

0x6000 这种看着没规律的地址,最常见的来源是自定义 Bootloader。很多项目为了支持 OTA 升级或者加一层固件校验,会先烧一个自己写的 Bootloader 到 Flash 开头的一段,然后让应用程序从 Bootloader 结束之后的位置开始存放。Bootloader 占多大,应用就从多大开始,于是就有了各种非标准偏移。

0x6000 换算成十进制是 24576,也就是 24KB。这恰好是一些 Bootloader 预留区域的常见大小。比如一个 Bootloader 加上它的配置区、跳转表、版本信息,凑够 24KB,应用就从 0x6000 开始。如果你的芯片 Flash 扇区划分是 8KB 一个扇区,24KB 正好是 3 个扇区,对齐得整整齐齐。这就是地址选择背后的工程考量:Bootloader 大小必须对齐到 Flash 扇区的整数倍,不能卡在两扇区中间,否则擦除时会连累隔壁的数据

除了 24KB,还有 16KB(0x4000)、32KB(0x8000)、48KB(0xC000)、64KB(0x10000)这些常见的 Bootloader 预留大小,分别对应不同复杂度的引导程序。你看到某个项目的应用起始地址是这些数,基本就能反推出它的 Bootloader 大概占了多少空间。

4.2 ESP32 分区表自定义出来的地址

ESP32 上的 0x6000,绝大部分时候来自分区表(Partition Table)的自定义配置。ESP-IDF 默认的单应用分区表里,nvs 分区的大小恰好是 0x6000,但这只是"大小",容易和"地址"混淆,得看仔细。

真正会出现 0x6000 作为地址的场景,是在自定义分区表里手动指定某个分区的 Offset。比如下面这样一段 CSV:

# Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x6000, otadata, data, ota, 0xf000, 0x2000, phy_init, data, phy, 0x11000, 0x1000, factory, app, factory, 0x20000, 1M, custom, data, 0x99, 0x6000, 0x1000,

最后一行里,Offset 写的就是 0x6000,工具会把这块数据烧到 Flash 偏移 0x6000 处。这种自定义偏移完全是项目需求驱动的,可能是一片配网参数、一段出厂校准数据、一份字体文件,总之是把 Flash 当硬盘分区用,哪块放什么自己说了算。

还有一种情况是某些开发板厂商的预置配置。厂商为了兼容自己的出厂固件、配网信息或者硬件参数,会在标准分区表之外额外划一块,位置可能就在 0x6000 附近。碰到这种地址,别急着怀疑工具出错,先去找项目里的分区表文件,答案九成就在那里。

4.3 非 ESP32 场景下的 0x6000

0x6000 也可能是别的架构里的地址。比如 Nordic nRF52 系列芯片,如果用了 SoftDevice,应用起始地址是由 SoftDevice 决定的,不同版本占的空间不一样,常见的应用起始地址有 0x1B000、0x26000 这些;再比如某些国产芯片把 ISP 引导或者出厂参数区域放在偏移 0x6000 处,用户程序要避开这段。还有一种情况是内部 SRAM 的地址划分,某些芯片 SRAM 分成几块连续区域,第二块从 0x6000 开始,这类地址出现在链接脚本里会让不熟悉的人扶额。

判断的通用方法只有一条:拿到芯片手册的内存映射章节,和工程里的链接脚本、分区表逐一对照。图纸和图纸对上,一切疑惑就烟消云散;图纸对不上,那问题一定出在某一方被改过。

5. 实操:怎么看 ESP32 的烧录地址

5.1 编译完成之后,去 build 目录翻

用 ESP-IDF 编译完一个工程后,build 目录里就藏着所有烧录地址的答案。最直接的线索是 build 目录下的 flasher_args.json 文件,里面清清楚楚地列出了每个待烧录文件应该烧到哪个偏移地址,包括 bootloader.bin、partition-table.bin、应用 bin,还有可能存在的其他数据区。这个文件是 idf.py flash 命令内部实际使用的参数来源,看它最准。

还有一个文件也值得关注,就是 build 目录下的 flash_project_args,这是一个纯文本文件,把烧录命令的所有参数都摊开写好了,用 esptool.py 可以直接读取。我在做产线烧录脚本的时候,就是直接从 flasher_args.json 里解析地址,保证脚本和正式编译流程用的是同一份数据,避免手工抄地址抄错。

再往下看,bootloader 目录里的 bootloader.bin 和 partition_table 目录里的 partition-table.bin 各自对应一个固定偏移。ESP32(Xtensa 架构那几个型号)的 bootloader 通常在 0x1000,分区表在 0x8000,应用在 0x10000;而 ESP32-C3、S3 这些 RISC-V 架构的型号,bootloader 偏移变成了 0x0,因为它们的 Flash 布局做了调整。看到这里,你应该能明白为什么"烧录地址有时是 0"了——ESP32-C3/S3 的 bootloader 就是从 Flash 偏移 0 开始写,这就是活生生的例子。

5.2 用命令行把地址问出来

不打开文件、直接在终端查地址,有几个很顺手的命令。第一个是查看分区表:

idf.py partition-table

这条命令会把当前工程的分区表打印出来,每个分区的名称、类型、子类型、偏移地址和大小一清二楚。想确认某个分区到底在 Flash 的哪个位置,看它的 Offset 列就行。

第二个是查看完整的烧录参数:

idf.py flash --help

虽然这条是帮助命令,但配合项目工程能间接推出默认地址。更直接的是直接调 esptool 询问:

esptool.py --port /dev/ttyUSB0 flash_id

这会读出 Flash 芯片的厂商 ID、型号 ID 和容量信息,虽然不给分区地址,但能确认工具跟芯片通信正常,容量够不够放你的固件。把读出来的容量和分区表里的最大偏移对比一下,就能判断分区表有没有超出 Flash 范围。

如果想把分区表从 Flash 里读出来验证:

esptool.py --port /dev/ttyUSB0 read_flash 0x8000 0xC00 partition_dump.bin gen_esp32part.py partition_dump.bin

第一条命令从偏移 0x8000 读取 0xC00 字节的分区表数据,第二条把二进制格式翻译成人类可读的 CSV。这个组合在排查"烧进去的分区表跟代码里写的不一致"时特别有用。

5.3 从分区表 CSV 反推每个地址的来历

工程根目录下的 partitions.csv 是所有地址的源头。打开它,从上往下看,每个分区的 Offset 都应该是上一分区 Offset 加 Size 再对齐后的结果。如果发现某个 Offset 是手工硬写的,那就要重点核对它有没有和相邻分区重叠。

一个合理的分区表看起来应该像下面这样:

分区名类型子类型偏移大小
bootloaderbootprimary0x10000x7000
partition-tablepartprimary0x80000x1000
nvsdatanvs0x90000x6000
otadatadataota0xF0000x2000
phy_initdataphy0x110000x1000
factoryappfactory0x200001M

这张表里,0x1000 是 bootloader 的地址,0x8000 是分区表自己的地址(对,分区表也知道自己放哪儿),0x9000 往后是数据区。你会发现所有地址都是 4KB 对齐的,这不是巧合,是 Flash 扇区擦除粒度的硬性要求。ESP32 的 Flash 擦除最小单位是 4KB,任何跨扇区的写操作都会带来麻烦,所以分区地址必须对齐到 4KB。

注意:自定义分区表时,分区的大小最好也取 4KB 的整数倍,否则容易出现分区之间"打架",烧录时报 overlap 错误。

5.4 用 NVS 分区做例子完整走一遍

拿 nvs 分区举个例子,把上面这些概念串起来。nvs 分区的 Offset 是 0x9000,Size 是 0x6000,意思是它从 Flash 偏移 0x9000 开始,占 24KB 空间。烧录工具在处理 NVS 数据时,就往 0x9000 处写;read_flash 命令想读它,也是从 0x9000 处读。

看到那个 0x6000 了吗?这就是文章开头提到的那个数字在 ESP32 场景里最常见的出处。很多人查资料时看到"nvs 大小 0x6000",又看到别人说"某个分区偏量 0x6000",两者一混,就以为烧录地址是 0x6000。其实一个是大小,一个是偏移,含义完全不同。搞清楚这一点,0x6000 这个数字的性质就明确了:它可以是大小,也可以是偏移,具体看它出现在表格的哪一列。

6. 烧录地址踩坑实录与排查速查

6.1 常见问题速查表

把平时遇到的高频问题整理成一张表,出错的时候先照着对一遍,能省不少时间。

现象可能原因排查动作
烧录报 address out of rangebin 文件配了错误地址确认地址是否为 Flash 物理基地址或正确偏移
烧完不运行,复位后无反应地址写到了别名区或错误区域用调试器读 Flash 内容比对
多个分区烧录后互相覆盖分区地址重叠或未对齐检查分区表 Offset 和 Size 的对齐
ESP32 烧录提示 offset 错误bootloader/分区表偏移不对查 flasher_args.json 确认偏移
换了芯片型号后地址全错不同型号 Flash 基地址不同查新型号手册的内存映射章节
工具默认地址与工程不一致手工指定了地址参数检查烧录命令和脚本里的地址参数

这张表里的每一条我都实际踩过,尤其是"地址写到了别名区"这条,早期调试 STM32 时吃过一次大亏,程序烧进去明明烧录成功,上电就是不跑,折腾了半天才发现是烧录工具在二进制文件模式下默认地址搞错,写到了 SRAM 区。

6.2 我总结的几条排查思路

第一条,先判断芯片类型。是 XIP 架构(Flash 直挂总线)还是外挂 Flash 架构,这决定了地址是物理地址还是偏移地址。前者比如 STM32、大部分 Cortex-M,地址是 CPU 地址空间的真实位置;后者比如 ESP32、ESP8266,地址是 Flash 芯片内部的偏移。

第二条,看文件类型。bin 是裸数据,烧录时必须显式给地址;hex、elf 自带地址信息,通常不需要手动指定,但指定了也不一定错,关键看工具的规则。遇到地址疑问,先把文件类型理清楚。

第三条,以编译产物为准。不要凭记忆写地址,去 build 目录里翻 flasher_args.json,或者直接idf.py partition-table打印,这两个地方的数据是编译系统自己算出来的,最不容易错。手工抄地址是低级错误的高发区,我见过太多次因为手抄偏移少写一个 0 导致固件起不来。

第四条,遇到非标准地址,先找分区表或链接脚本。0x6000 这种地址不会凭空出现,它一定来自某个配置文件的 Offset 字段或者某个 Bootloader 的预留空间。找到源头,一切就清楚了。

6.3 几个容易被忽略的小细节

还有一个细节,Flash 的擦除粒度。ESP32 是 4KB,STM32 不同型号的扇区大小不一样,F1 系列早期型号是 1KB 或 2KB 一个扇区,F4 系列是 16KB 到 128KB 不等的扇区。这意味着你在规划 Bootloader 预留空间时,必须按目标芯片的扇区粒度对齐,不能照搬别的项目的 0x6000。同样是"应用从 0x6000 开始",在 4KB 扇区的芯片上没问题,在 8KB 扇区的芯片上就可能跨扇区,擦除 Bootloader 时把应用的第一扇区一起擦掉。

另一个细节是加密和签名。有些芯片开了安全启动或者 Flash 加密,烧录地址附近的元数据区域会多出一些保留位置,比如密钥区、签名区,这些位置不能被普通应用占用。规划地址时必须把它们避开,具体位置查芯片的安全手册。这个坑比较隐蔽,一旦踩上就是启动失败,而且报错信息往往含糊其辞,很费时间。

最后一个细节,量产烧录时用的是合并后的固件。很多产线工具会把多个 bin 按地址合并成一个文件,烧录时一次写入。合并过程最容易出错的地方就是地址偏移,一个文件偏了几百字节,整个合并结果就废了。所以合并脚本一定要用编译系统生成的参数来驱动,不要手工拼命令。我自己写合并脚本时,习惯把 flasher_args.json 解析后打印一份人类可读的地址清单,人工扫一眼确认无误再执行,虽然多一步,但省下的是产线上返工的代价。

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

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

立即咨询