
我做了小十年嵌入式从 51 到 STM32 再到 ESP32中间被“烧录地址”这个问题折磨过无数次。群里的新手也总爱问为什么用 FlyMcu 给 STM32 烧程序时地址是 0x08000000用 ESP Flash Download Tool 给 ESP8266 烧 AT 固件时要填 0x00000翻某些 ESP32 教程又看到 0x10000、0x8000甚至还有人把分区表里的 0x6000 当成了烧录地址填进去结果板子直接变砖。这三个数看着都是“地址”但它们的坐标系完全不一样。这篇我把每个数背后到底是谁在定义、工具拿它干嘛、填错了会怎样一次讲清楚最后给出一套在 ESP32 上查烧录地址的实操流程保证你以后拿到任何固件都能自己判断该填哪。1. 烧录地址之争CPU 地址还是 Flash 偏移先对号入座1.1 同一个词两套坐标系先说个最简单的事实一颗 Flash 芯片从物理层面看第一个字节就是偏移 0。无论是 STM32 片内的 Flash还是 ESP32 外挂的 SPI Flash存储阵列的编址永远是“从 0 开始往数”。烧录器干的事本质上就是把你的固件数据写到某一颗 Flash 的某个偏移处仅此而已。那为什么 STM32 烧录时要写 0x08000000因为这是 CPU 视角。Cortex-M 处理器上电后从 0x00000000 取向量表但 STM32 把“片内 Flash 基址”安排在了 0x08000000。烧录器如果填的是 CPU 绝对地址那它就不得不把 0x08000000 换算成 Flash 内部偏移 0 再写入。于是同一个物理位置就有了两种叫法CPU 叫它 0x08000000Flash 自己管它叫 0。你手上的烧录工具有的要 CPU 绝对地址Keil、IAR、J-Flash、STM32CubeProgrammer有的要 Flash 偏移地址esptool.py、ESP Flash Download Tool、各种 SPI Flash 编程器。同一个固件前者让你填 0x08000000后者让你填 0x0其实烧的是同一个地方。这不叫矛盾叫坐标系不同。1.2 十秒判断工具要的是哪种地址拿不准的时候就看三点工具是否让你“选芯片型号”。让你先选型号、然后自动带出起始地址的基本是 CPU 绝对地址体系因为基址是芯片厂商在内存映射里定死的。工具是否让你“手动填 Offset”。只给你一个输入框让你填 0x1000、0x8000、0x10000 这种值的99% 是 Flash 偏移从 Flash 芯片头部开始算。固件格式。Intel HEX 文件自带头部和绝对地址工具能自己解析纯 bin 文件就是裸数据没有地址信息必须由你指定写到哪个偏移。所以“bin 文件要配地址hex 文件不用太操心”是条朴素而有效的经验。顺带说一句很多国产工具界面上写的是“烧录地址”但内部逻辑是“偏移量”两者混着叫这也是大家懵的主要原因之一。你只要记住CPU 地址可以很大、很整、带芯片家族特征0x08 开头Flash 偏移通常是小数字、从 0 起步就能少踩一半坑。2. 0x08000000 的身世Cortex-M 内存映射给了片内 Flash 一个“户口”2.1 在 4GB 地址空间里Flash 就该住这ARM Cortex-M 的地址空间一共 4GB被划分成几大块0x00000000 到 0x1FFFFFFF 是代码区0x20000000 到 0x3FFFFFFF 是 SRAM 区0x40000000 到 0x5FFFFFFF 是外设区。STM32 把片内 Flash 放在代码区里的 0x08000000这个位置不是随便定的而是芯片设计时在总线矩阵里固定的“户口”。无论你怎么改链接脚本Flash 物理地址都是 0x08000000改的只是“程序认为自己被烧在哪个地址”。很多人不理解为什么不是 0x00000000。实际上 0x00000000 这一段在 STM32 里是“别名区”具体映射到谁由 BOOT0/BOOT1 引脚决定BOOT00启动时 0x00000000 映射到片内 Flash即把 0x08000000 的内容“镜像”到 0x00000000。BOOT01、BOOT10映射到系统存储器ROM 里的 ISP 引导程序。BOOT01、BOOT11映射到 SRAM。所以 STM32 有一个很迷惑的特性你把程序链接到 0x08000000 能跑链接到 0x00000000 在某些启动配置下也能跑。但工程上永远用 0x08000000因为这是 Flash 在 CPU 地址线上的真实基址调试器、烧录器、Bootloader 之间的约定都基于它。2.2 中断向量表让你必须对地址敏感Cortex-M 上电第一件事是从地址 0x00000004 读初始 PC从 0x00000000 读初始 SP。程序跑起来之后中断来了CPU 从“向量表”里取中断服务函数地址。在 STM32 上向量表默认放在 Flash 起始处也就是 0x08000000。这就牵出一个经典问题IAP 升级时你的用户程序往往不会从 0x08000000 开始而是从 0x08006000 或 0x08008000 之类的位置开始。此时如果不告诉 CPU“向量表搬家了”CPU 仍然去 0x08000000 读中断向量取到的却是 Bootloader 的向量程序一旦进中断就直接 HardFault。F4 系列可以写SCB-VTOR 0x08006000搞定F1 系列没有 VTOR 寄存器老办法是改system_stm32f10x.c里的 VECT_TAB_OFFSET或者把向量表拷到 SRAM。这也是为什么 IAP 工程里烧录地址、链接地址、中断向量表地址必须三者保持一致少改一个都白搭。2.3 HEX 文件里就藏着这个地址拿记事本打开 STM32 的 .hex 文件你会看到类似这样的行:020000040800F2 :1000000000070020D1070008B5070008...:02000004是“扩展线性地址”记录后面的0800表示高 16 位地址是 0x0800再后面的数据记录地址从 0x0000 开始两者拼起来就是 0x08000000。烧录器就是靠这些记录知道该往哪写。所以 Keil 编译时你把 IROM1 起始地址改成 0x08006000生成的 hex 里前几行地址就会变成 0x08006000工具照着写就行。这一节想说明的重点是0x08000000 是芯片厂商在 CPU 内存映射里规定的 Flash 基址它解决的是“CPU 去哪儿取指”的问题。理解了这个你再看填 0 的场景就会豁然开朗。3. 填 0 没有错Flash 偏移地址本来就该从 0 起算3.1 ESP32 的“烧录地址”和 STM32 根本不是一回事STM32 是片内 FlashCPU 能直接寻址所以烧录地址可以直接用 0x08000000。ESP32 不一样它用的是外挂 SPI FlashXtensa 核心并不通过“0x08000000 这种地址”访问 Flash。工具往 SPI Flash 里写数据时用的是这颗 Flash 芯片从 0 开始的偏移。所以你在 ESP32 的烧录命令里看到的 0x1000、0x8000、0x10000全部是 SPI Flash 偏移不是 CPU 地址。这也是 STM32 用户刚转 ESP32 时最容易懵的地方elf 文件里明明写着入口地址 0x400819fc、段地址 0x400d0018怎么烧录命令里让填 0x10000因为那些 0x400xxxxx 是 CPU 运行时的地址是二级 Bootloader 把 Flash 内容加载/映射过去之后才生效的烧录这一步只关心“数据在 Flash 芯片的哪个偏移”。记住一句话ESP32 里烧录地址 SPI Flash 偏移不等于程序入口地址。这下再看到 0x400d0018 就不会慌了。3.2 什么时候会出现“烧录地址 0x0”最常见的场景就是烧合并固件merged bin。ESP-IDF 里可以用esptool.py merge_bin把 Bootloader、分区表、App 按各自偏移拼成一个整包然后一条命令烧到 0x0esptool.py --chip esp32 merge_bin -o merged.bin 0x1000 build/bootloader/bootloader.bin 0x8000 build/partition_table/partition-table.bin 0x10000 build/hello-world.bin esptool.py --chip esp32 -p COM3 -b 460800 write_flash 0x0 merged.bin为什么整包可以填 0x0因为 merge 出来的文件里偏移 0x1000 的位置放的就是 Bootloader 的内容。你把整包从 Flash 偏移 0x0 开始写Bootloader 自然就落到了 0x1000正好是 ESP32 ROM 里写死的“二级引导加载偏移”。ESP8266 的老 AT 固件同理很多完整固件包就是设计成从 0x00000 整包烧入的内部布局由 SDK 自己保证。另外还有些芯片Flash 基址天生就是 0。比如 nRF52 系列Flash 从 0x00000000 开始、LPC17xx、瑞萨 RA 系列它们上电后直接从 0x0 取向量表。给 nRF52832 烧裸机程序时Keil 里 IROM1 起始地址就填 0x0如果工程加了 SoftDeviceApp 就要挪到 0x26000烧录地址也跟着变。在这些平台上CPU 地址和 Flash 偏移是重合的填 0 反而是最自然的事。3.3 烧到错误偏移会看到什么ESP32 的 ROM 引导程序对 Bootloader 所在偏移是固定的如果你把 App 直接烧到 0x0而不是合并整包芯片复位后去 0x1000 找二级 Bootloader发现是乱数据串口会吐出类似invalid header: 0xffffffff或者直接反复复位。这不是芯片坏了是地址填错了。同样的现象在 STM32 上往往表现为“程序烧进去了但一开中断就 HardFault”因为向量表不对位。4. 0x6000 的真面目分区表大小、IAP 预留区别把“大小”当“地址”4.1 先泼冷水ESP32 默认分区表里的 0x6000 根本不是地址很多人是在 ESP-IDF 的默认分区表里第一次见到 0x6000。partitions_singleapp.csv长这样# Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x6000, phy_init, data, phy, 0xf000, 0x1000, factory, app, factory, 0x10000, 0x200000,看清楚列名0x6000 在Size列表示 NVS 分区占 24KB它对应的Offset是 0x9000。如果有人把 0x6000 填到烧录地址里等于把数据写在分区表0x8000和 NVS0x9000之间把 Bootloader 依赖的 NVS 区域完全打乱板子起不来就怨不得别人。所以看到 0x6000、0x4000、0x2000 这样的十六进制数时先分辨它到底是 Offset 还是 Size再决定要不要用它。这个教训我见过太多次了CSV 表格被当成填地址手册几乎每个初学者都会中招一次。4.2 但 STM32 IAP 里0x6000 确实可以是地址不过 0x6000 也不总是大小。STM32 IAP 工程里非常常见的布局是Bootloader 占 24KB也就是 0x00000000 到 0x00005FFF用户 App 从 0x08006000 开始。这里的 0x6000 是 Flash 基址上的偏移完整的 CPU 地址是 0x08006000。“0x6000”其实是 24KB 的十六进制写法等于 Bootloader 占用的空间大小。Bootloader 越大App 的起始地址就越往后推0x08004000、0x08005000、0x08006000、0x08008000 都是常见取值。这种“预留区推着 App 往后走”的思路和 ESP32 的分区表逻辑完全一致Bootloader 在前中间夹着各种配置、元数据分区App 被安排在后面。ESP32 出厂 App 默认在 0x10000也是因为 0x1000Bootloader、0x8000分区表、0x9000NVS、0xe000OTAData把前面占满了。所以回答标题的问题“0x6000 是什么”只能说它可能是个大小也可能是个偏移关键是拿出分区表或链接脚本对一下不能凭直觉。4.3 为什么这些地址总是“整整齐齐”的你可能注意到了无论 0x1000、0x8000、0x9000、0x10000、0x6000全是 0x1000 的整数倍。这不是巧合。ESP32 的 SPI Flash 擦除扇区是 4KB0x1000分区表要求所有偏移 4KB 对齐STM32F1 的 Flash 页是 1KB 起步所以 IAP 的偏移也习惯取整 0x1000 的倍数。Flash 的这种“按块擦除”特性决定了地址规划必须留有对齐余量看到整齐的十六进制地址可以默认它是 4KB 或 1KB 对齐过的合法偏移。5. ESP32 烧录地址怎么查按这套流程一次定位5.1 串口日志是最快的答案ESP-IDF 的二级 Bootloader 每次启动都会打印它从哪个偏移加载了 App。用串口助手打开 115200按复位键能看到一行I (587) boot: Loaded app from partition at offset 0x10000这行字直接告诉你当前固件在 Flash 的实际偏移是 0x10000。管它分区表怎么写、教程怎么教芯片自己说出来的最可信。这也是“怎么看 esp32 的烧录地址”最简单的方法不需要装任何额外工具。5.2 烧录时开详细日志抄下 esptool 命令Arduino IDE 里把“文件 → 首选项 → 编译并上传时显示详细日志 → 上传”勾上重新上传一次控制台会打印出完整命令。你会看到类似esptool.py -p COM3 -b 921600 --chip esp32 write_flash --flash_mode dio --flash_freq 80m --flash_size 4MB 0xe000 boot_app0.bin 0x1000 bootloader.bin 0x8000 partitions.bin 0x10000 firmware.bin注意最后几个参数0xe000、0x1000、0x8000、0x10000这就是 Arduino 核心为你算好的烧录地址。以后别人问“固件烧在 0x多少”把这条日志甩给他比什么教程都有说服力。ESP-IDF 工程可以用idf.py -v flash或直接看 Makefile 里打印的--flash_args文件同样能拿到完整偏移清单。5.3 esptool.py image_info 看固件自身的“期望位置”bin 文件本身不记录烧录偏移但 ESP32 的 App image 头部带有段信息和 Flash 加载地址。用 esptool 读一下esptool.py image_info build/hello-world.bin较新版本会输出类似Flash addresses: 0x00010000的一行表示这个固件编出来后期望自己被放在 Flash 偏移 0x10000。注意别被Entry point: 0x400819fc这种运行地址吓到那是 CPU 侧的地址不是烧录偏移。如果版本太老没有这行输出就用下面分区表的方式确认。5.4 parttool.py 读回芯片里真实的分区表想知道芯片里现在实际烧了哪些分区用 ESP-IDF 自带的 parttool 把分区表读出来python $IDF_PATH/components/partition_table/parttool.py --port COM3 print_partition_table输出就是当前芯片上真实生效的分区表和编译时的partition-table.bin完全对应# Espressif Partition Table # Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x6000, otadata, data, ota, 0xe000, 0x2000, phy_init, data, phy, 0xf000, 0x1000, factory, app, factory, 0x10000, 0x200000, ota_0, app, ota_0, 0x210000, 0x200000, ota_1, app, ota_1, 0x410000, 0x200000,没有 IDF 环境的话直接看你工程里的.csv文件也一样那是烧录前的规划两者对不上说明芯片里的程序和工程不是同一份先同步再说。Arduino 用户可以在“工具 → Partition Scheme”里切换方案也能在 Arduino15 包里找到对应的default.csv。5.5 常用地址速查表内容常见默认烧录地址说明合并整包 merged.bin0x0内容已按内部偏移排好从 Flash 头整烧二级 Bootloader0x1000ESP32 ROM 固定要求的位置分区表0x8000默认偏移可在 menuconfig 改动NVS 分区0x9000存取 Key-ValueOTA Data0xe000用于 OTA 切换出厂 Appfactory0x10000常见单 App 默认位置OTA App00x210000具体取决于分区表ESP8266 整包 AT 固件0x00000老 SDK 习惯用法ESP8266 新版 user1.bin0x01000/0x02000取决于 Boot 版本和 Flash 大小这张表只适用于默认配置。产品里有人把分区表挪到 0x1000 之外有人把 factory 放在 0x6000只要不重叠、对齐就没问题所以查具体地址永远以 5.1、5.4 的实际结果为准速查表只是让你“看着不慌”。6. 翻车现场与我常用的烧录前检查清单6.1 三种典型翻车方式烧 ESP32 时把 App 直接填 0x0覆盖了本该放 Bootloader 的区域重启后日志提示invalid header或者反复 reset。这种情况不要急着重新刷回 App先把 Bootloader、分区表、Boot_app0、App 四个文件按标准地址重新烧一遍再验证。烧 STM32 IAP 时只改了 App 的烧录地址忘了改中断向量表偏移程序能跑到 main但一按按键或来一个定时器中断就 HardFault。排错很容易误判成“硬件有问题”其实只要把SCB-VTORF4/H7或 VECT_TAB_OFFSETF1设成和新 App 地址一致即可。还有一种是把 bin 当 hex 用烧录工具不认文件头数据往错误偏移乱写。bin 文件必须显式指定偏移这个原则适用于所有平台谁偷懒谁返工。6.2 我个人的烧录前固定动作每次拿到一个不熟的板子或固件我习惯走一遍这个流程能避免九成低级事故先esptool.py flash_id确认 Flash 容量防止给 4MB 板子填 8MB 分区的地址。烧录前用esptool.py erase_region清掉目标 App 区域的旧数据避免残留扇区里的旧分区信息干扰 Bootloader 判断。烧完后用read_flash把关键位置读回来和本地 bin 对比一下确认数据真的写到预期偏移。每换一次 SDK 版本或 Arduino 核心版本重新看一遍构建日志里的烧录命令不吃老本。因为分区方案可能随版本变化。在工程目录放一个flash_notes.md把这块板的 Bootloader、分区表、App 各自地址记下来。三个月后回来看你会感谢现在的自己。最后再提醒一句无论在哪个平台看到“烧录地址”四个字先问自己这是 CPU 地址还是 Flash 偏移。想明白坐标系0、0x08000000、0x6000 这些数字就再也唬不住你了。