ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

搞懂STM32与ESP32烧录地址:从0x08000000到分区偏移

2026/9/17 23:40:24 拓冰建站 浏览量
搞懂STM32与ESP32烧录地址:从0x08000000到分区偏移 搞嵌入式的早晚都会被烧录地址这个问题绊一下。我最早用 J-Link 给 STM32 烧程序的时候Keil 里 Flash Download 那一栏写着 0x08000000IAR 编译出来的 hex 文件打开一看地址是从 0x0 开始的后来上手 ESP32命令行里又冒出来 0x1000、0x8000、0x10000 一堆偏移再往后做某个国产 Cortex-M 的项目Bootloader 跳转表上赫然写着 0x6000。同样的烧录两个字地址怎么就这么花这不是工具在跟你开玩笑每一个数字背后都有它必须成立的理由。这篇博文就把烧录地址这件事从头到尾讲透——为什么有的芯片从 0 开始有的从 0x08000000 开始有的偏偏落在 0x6000 这种看似没规律的位置它们分别对应什么芯片架构、什么内存映射方式、什么工具视角最后再落回到最实际的问题上怎么看 ESP32 的烧录地址以及遇到地址对不上时该怎么排查。不管你是刚摸单片机的学生还是已经做过几个项目、但对这些地址还是一知半解的工程师这篇都能给你一个能自洽的解释框架。1. 烧录地址到底是谁定的1.1 先分清烧录地址和运行地址很多人一上来就把这两个概念混在一起结果越看越糊涂。烧录地址指的是烧录工具把一段二进制数据写进目标存储器时写入的起始位置运行地址指的是 CPU 真正执行这段代码时从哪个地址去取指令。这两个地址在有些芯片上是同一个数在另一些芯片上则完全不是一回事而烧录地址的花式变化本质上就是这两个概念在不同架构下关系的体现。拿最典型的两种情况对比就清楚了。STM32 这类芯片Flash 是直接挂在 CPU 的地址总线上的代码可以原地执行XIPExecute In PlaceFlash 的物理地址就是 0x08000000CPU 从这个地址取指令烧录工具也把代码写到这个地址烧录地址和运行地址统一了。ESP32 完全是另一套路子它的 Flash 是外挂的 SPI FlashCPU 地址空间里根本没有这么大一块连续区域直接映射它运行时靠 MMU 把 Flash 里的一段内容映射到内部 RAM 的某个地址去执行所以烧录时用的是相对 Flash 起始位置的偏移0x1000、0x10000 这些数字指的是在 Flash 芯片内部的第几个字节开始写而不是 CPU 地址。理解了这一层你就能明白为什么有人告诉你烧到 0 就行有人坚持必须写 0x08000000——他们说的场景根本不一样一个是在讲 Flash 芯片被看作一块从零开始编址的裸存储另一个是在讲 CPU 视角下的物理映射地址。1.2 链接脚本和内存映射表才是真正的源头烧录地址不是烧录工具随手编的它来源于编译阶段生成的链接脚本linker script和芯片的内存映射表memory map。链接器在把各个目标文件拼成最终可执行文件的时候需要知道代码段、数据段、只读数据段分别应该落在哪个地址区间这个信息就写在链接脚本里比如 GNU 工具链常见的 .ld 文件或者 Keil 工程里的 Scatter FileIAR 里的 .icf 文件。链接脚本里的地址又是根据芯片手册里的内存映射表来的。芯片厂商在设计时就把地址空间划分好了哪一段是 Flash哪一段是 SRAM哪一段是外设寄存器哪一段是系统存储器System Memory哪一段是选项字节区域全都有固定的分配。链接脚本只是把这些分配如实抄下来告诉链接器代码就放这儿。所以当你看到烧录地址是 0x08000000其实是在告诉你这个芯片的 Flash 从 CPU 地址空间的 0x08000000 开始看到 0x0则可能是这个芯片的代码从地址空间的零地址开始也可能是这个工具把 Flash 当成裸存储、按偏移写入。要彻底搞清楚最靠谱的办法是翻芯片的参考手册找到 Memory Map 那一章对着看一目了然。2. 0x08000000XIP 架构下的物理地址2.1 STM32 为什么把 Flash 放在 0x080000000x08000000 这个地址几乎已经成了 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 这个别名区映射到三个不同的物理区域之一主 Flash0x08000000、系统存储器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还有 16KB0x4000、32KB0x8000、48KB0xC000、64KB0x10000这些常见的 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 场景下的 0x60000x6000 也可能是别的架构里的地址。比如 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 各自对应一个固定偏移。ESP32Xtensa 架构那几个型号的 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 是手工硬写的那就要重点核对它有没有和相邻分区重叠。一个合理的分区表看起来应该像下面这样分区名类型子类型偏移大小bootloaderbootprimary0x10000x7000partition-tablepartprimary0x80000x1000nvsdatanvs0x90000x6000otadatadataota0xF0000x2000phy_initdataphy0x110000x1000factoryappfactory0x200001M这张表里0x1000 是 bootloader 的地址0x8000 是分区表自己的地址对分区表也知道自己放哪儿0x9000 往后是数据区。你会发现所有地址都是 4KB 对齐的这不是巧合是 Flash 扇区擦除粒度的硬性要求。ESP32 的 Flash 擦除最小单位是 4KB任何跨扇区的写操作都会带来麻烦所以分区地址必须对齐到 4KB。注意自定义分区表时分区的大小最好也取 4KB 的整数倍否则容易出现分区之间打架烧录时报 overlap 错误。5.4 用 NVS 分区做例子完整走一遍拿 nvs 分区举个例子把上面这些概念串起来。nvs 分区的 Offset 是 0x9000Size 是 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 是 4KBSTM32 不同型号的扇区大小不一样F1 系列早期型号是 1KB 或 2KB 一个扇区F4 系列是 16KB 到 128KB 不等的扇区。这意味着你在规划 Bootloader 预留空间时必须按目标芯片的扇区粒度对齐不能照搬别的项目的 0x6000。同样是应用从 0x6000 开始在 4KB 扇区的芯片上没问题在 8KB 扇区的芯片上就可能跨扇区擦除 Bootloader 时把应用的第一扇区一起擦掉。另一个细节是加密和签名。有些芯片开了安全启动或者 Flash 加密烧录地址附近的元数据区域会多出一些保留位置比如密钥区、签名区这些位置不能被普通应用占用。规划地址时必须把它们避开具体位置查芯片的安全手册。这个坑比较隐蔽一旦踩上就是启动失败而且报错信息往往含糊其辞很费时间。最后一个细节量产烧录时用的是合并后的固件。很多产线工具会把多个 bin 按地址合并成一个文件烧录时一次写入。合并过程最容易出错的地方就是地址偏移一个文件偏了几百字节整个合并结果就废了。所以合并脚本一定要用编译系统生成的参数来驱动不要手工拼命令。我自己写合并脚本时习惯把 flasher_args.json 解析后打印一份人类可读的地址清单人工扫一眼确认无误再执行虽然多一步但省下的是产线上返工的代价。