
前几天帮一个朋友看板子他拿着我们编出来的 app.bin 在下载工具里把 Base address 填成了 0x08000000点下载然后板子就彻底不亮了——bootloader 被整段盖掉。他特别不服气同一个工程Keil 的 Target 里 IROM1 写的是 0x08000000我自己编 APP 的时候链接脚本写的是 0x08006000现在手动烧录你又让我填 0这三个数字到底哪个才是烧录地址这个疑问几乎每个做嵌入式的人都撞过我自己也在这个问题上栽过不止一次。结论先说清楚0、0x08000000、0x6000 根本不是同一个层面的东西把它们放在一起比谁对谁错本身就问错了问题。0 说的是镜像文件里的相对偏移0x08000000 说的是芯片地址空间里的物理坐标0x6000 说的是在一个已经切过区的 Flash 里这块固件的起跑线在哪儿。三个数字各管一段混用就会出事。下面我把这三个数字拆开讲透顺便把 STM32、ESP32、AVR 这些常见平台烧录地址的来龙去脉、怎么查、怎么算、怎么排错一次性讲完。内容偏向实操适合刚上手 IAP/OTA 的兄弟也适合做了几年但一直照着教程填地址没深想过为什么的人。1. 三个数字其实是三件事文件偏移、链接地址与芯片地址1.1 裸 bin 文件里压根没有地址这个概念很多人以为 bin 文件里带着地址信息这是个根深蒂固的误解。bin 是一段纯粹的字节流它的第 1 个字节就是文件的第 0 个字节文件里没有任何字段记录我应该被放到哪里。这是objcopy -O binary这个动作的本质决定的——它把 ELF 里所有带地址的段.text、.rodata、.data 的初值按地址顺序抠出来中间的空白用 0xFF 或 0x00 填平地址信息本身被丢掉了。正因为如此任何烧录工具打开一个 bin 文件时都必然要问你一句base address 是多少而它默认给出的值往往就是 0。这个 0 不是芯片的第 0 号地址它的真实含义是从文件第 0 个字节开始按顺序往后写。我见过太多人在这里翻车用 J-Flash 或 STM32CubeProgrammer 打开 bin默认 base 0x00000000 直接点下载结果把 bootloader 覆盖了。工具没错是你没告诉它镜像原本的家在哪。1.2 链接脚本里的 ORIGIN才是代码以为自己住在哪GCC 工具链里决定这件事的是链接脚本.ldMEMORY { FLASH (rx) : ORIGIN 0x08006000, LENGTH 488K RAM (rwx) : ORIGIN 0x20000000, LENGTH 64K }Keil 里对应的是分散加载文件.sctLR_IROM1 0x08006000 0x0007A000 { ; load region size_region ER_IROM1 0x08006000 0x0007A000 { *.o (RESET, First) *(InRoot$$Sections) .ANY (RO) } RW_IRAM1 0x20000000 0x00010000 { .ANY (RW ZI) } }IAR 里是.icfdefine region ROM_region mem:[from 0x08006000 to 0x0807FFFF]; place at address mem:0x08006000 { readonly section .intvec };这三份配置说的是同一句话这份代码里所有的绝对地址引用都以 0x08006000 为基准来计算。函数指针、跳转表、常量字符串的地址、中断向量表里的入口地址全都建立在这个基准上。这就带来一个非常关键的推论如果你把这个以 0x08006000 为基准编译出来的 bin烧到了 0x08000000那它里面的每一个绝对地址引用都偏了 0x6000程序必然跑不起来。这不是可能有点问题是必死。1.3 芯片地址空间是物理坐标不随你的工程变化第三层是芯片自己的存储器映射这是刻在硅片上的你看不看它都在那儿。以 STM32F103 这类经典 Cortex-M3 为例翻开参考手册的 Memory map 章节地址范围用途0x00000000 - 0x1FFFFFFFCode 区别名/重映射区0x08000000 - 0x0807FFFF主 Flash512KB 型号0x1FFFF000 - 0x1FFFF7FF系统存储器出厂 ROM bootloader0x20000000 - 0x2000FFFFSRAM0x40000000 - 0x5FFFFFFF外设寄存器Flash 的物理起点就是 0x08000000。这一点不会被你的链接脚本改掉也不会因为下载工具默认填 0 而改变。所以严格来说0x08000000 才是烧录地址这个话题里的正统答案另外两个数字都是它的派生品。2. 0x08000000 的来历Cortex-M 映射表里的那层别名2.1 为什么 STM32 的 Flash 不直接放在 0x00000000按 ARMv7-M 架构规范0x00000000 到 0x1FFFFFFF 属于 Code 区理论上放代码最合适从 0 开始编号也最符合直觉。但意法半导体没这么干把主 Flash 放到了 0x08000000SRAM 放 0x20000000外设从 0x40000000 起。这么设计的好处是地址本身就带语义你看到 0x0800xxxx 就知道这是 Flash看到 0x2000xxxx 就知道是 RAM看到 0x4001xxxx 就知道是外设调试的时候看一眼指针值就能判断它指向哪类资源。而且不同容量、不同型号的芯片都遵守同一套分区代码的可移植性更好。代价就是 0x00000000 这段留着怎么办ARM 给的方案是别名alias。2.2 复位那一刻CPU 其实是从 0x00000000 取向量的Cortex-M 的复位行为是固定的上电后从 0x00000000 读第一个 32 位字作为主栈指针 MSP 的初值从 0x00000004 读第二个字作为复位向量然后跳过去执行。这个流程不因为你把 Flash 放在 0x08000000 而改变。于是芯片厂商加了一层重映射逻辑由 BOOT 引脚决定 0x00000000 这段别名区映射到谁BOOT0 0映射到主 Flash0x08000000这是正常运行模式BOOT0 1、BOOT1 0映射到系统存储器也就是出厂 ROM bootloader串口下载就用这个BOOT0 1、BOOT1 1映射到内置 SRAM用于调试或特殊场景。所以当 BOOT0 接地时0x00000000 处的向量表和0x08000000 处的向量表读出来的是同一份数据物理上是同一块 Flash。2.3 说烧到 0 也能跑的时候其实在说什么理解了别名机制就能解释那句流传很广的话STM32 烧到 0 也能跑。这句话在运行视角下是对的——复位后 CPU 访问 0x00000000硬件帮你转到了 Flash。但在烧录视角下就不一定成立了Flash 控制器真实挂在 0x08000000 这一段调试器或下载工具给的 base address 该写 0x08000000写 0 的时候有的工具会替你做个地址换算有的会直接报地址非法还有的会老老实实往 0 写然后失败。这就解释了一个很常见的怪现象同一个 bin用 A 工具烧进去能跑换 B 工具就烧不进去或者烧进去不跑。不是文件坏了是两家工具对 base 0 的处理策略不一样。我的习惯是永远不给工具留猜测空间base 一律填物理地址 0x08000000省事。3. 0x6000 是怎么算出来的从 bootloader 的分区账本说起3.1 先把 bootloader 的实际体积量出来而不是拍脑袋0x6000 这个数字从来不是芯片规定的它是你自己切的分区边界。做 IAP 的时候Flash 会被切成两块低地址放 bootloader高地址放 APP中间那条线就是 APP 的烧录地址。这条线的位置怎么定先量 bootloader 的真实体积arm-none-eabi-size build/bootloader.elf # text data bss dec hex filename # 18236 112 2148 20496 5010 build/bootloader.elf这里 text data 18348 字节约 17.9KB。再看一眼段分布确认没有遗漏arm-none-eabi-readelf -S build/bootloader.elf | grep -E Nr|\.text|\.data|\.rodata然后加上预留量。预留量考虑三件事一是以后功能加需求bootloader 要不要支持更多命令、更复杂的校验二是擦除粒度APP 起点必须落在扇区边界上三是留一点缓冲别卡着 17.9KB 就划 18KB稍微改个编译选项就溢出了。17.9KB 加上余量和扇区对齐落到 24KB0x6000就是很自然的选择。3.2 0x6000 24KB一个被抄了很多遍的默认值0x6000 换算成十进制是 24576正好 24KB。这个数字在 IAP 例程里出现频率极高很多教程直接拿它当默认值于是流传开来。常见的偏移量对照如下偏移量大小典型场景0x20008KB极简 bootloader只做跳转和串口收包0x400016KB带 CRC 校验、简单协议解析0x600024KB最常见的默认值带 YMODEM/自定义协议、双备份标志0x800032KB带 AES 解密、压缩解包0xC00048KB功能较重的 bootloader或带 UI 的升级器0x1000064KB大容量型号上的分区方案注意0x6000 是相对 Flash 起点而言的偏移。如果 Flash 起点是 0x08000000那 APP 的绝对地址就是 0x08000000 0x6000 0x08006000。这两个数字在工程里会交替出现链接脚本、分散加载文件、IAP 跳转代码里通常用绝对地址 0x08006000而 OTA 包格式、分区表、升级协议里往往用相对偏移 0x6000。搞混了就是一个经典的 off-by-one 类错误。3.3 扇区对齐这件事比大小本身更重要这是我见过最容易被忽略、后果最严重的一点。STM32F1 系列的 Flash 页大小是 1KB 或 2KBF4 系列则是 16KB、64KB、128KB 混排的扇区。擦除的最小单位是页或扇区不是字节。假设你用 STM32F407扇区分布是扇区 0-3 各 16KB0x08000000-0x0800FFFF扇区 4 是 64KB0x08010000-0x0801FFFF扇区 5-11 各 128KB。如果你把 APP 起点定在 0x08006000落在扇区 1 内部bootloader 用掉了扇区 016KB和扇区 1 的前 24KB。升级的时候APP 区域要整体擦除重写擦除指令是按扇区来的。你只能擦扇区 1 整个 16KB可 bootloader 的代码就住在这个扇区里一擦就没了。正确做法是把 APP 起点对齐到扇区边界也就是 0x0801000064KB 处bootloader 独占扇区 0-3 共 64KB。所以定偏移量的正确顺序是先确认芯片的扇区划分再把 APP 起点对齐到扇区边界最后才反推出 bootloader 能用多少空间而不是反过来。这个顺序搞反了板子会在升级一次就变砖的循环里反复折磨你。3.4 VTOR 不动APP 一定跑飞地址切完还不算完。Cortex-M 的中断向量表默认在 0x00000000APP 搬到 0x08006000 之后向量表也跟着搬了但硬件不知道。必须手动告诉它/* APP 的 main 函数最开始进 main 之前最好就设好 */ SCB-VTOR 0x08006000; __DSB(); __ISB();或者改system_stm32f1xx.c里的宏#define VECT_TAB_OFFSET 0x6000这个值必须跟链接脚本的 ORIGIN 完全一致。它决定了 SysTick、串口、DMA 这些中断发生时CPU 去哪张表里找函数入口。VTOR 不改的表现很有辨识度程序能进 main能跑主循环但一开中断就进 HardFault或者干脆跳到一个莫名其妙的地址。还有个细节VTOR 要求向量表按表大小对齐至少 128 字节0x6000 这种整千地址天然满足不用担心。但如果谁把偏移定成 0x6100 这种值就得留意对齐问题了。4. 换颗芯片数字全变ESP32、AVR 与国产替代片的起跑线4.1 ESP32 的三个数字0x1000、0x8000、0x10000 是谁定的转到 ESP32 上你会发现烧录地址列表长这样esptool.py --chip esp32 write_flash --flash_mode dio --flash_freq 40m --flash_size detect \ 0x1000 bootloader/bootloader.bin \ 0x8000 partition_table/partition-table.bin \ 0xf000 phy_init_data.bin \ 0x10000 my_app.bin这四个地址各有各的来头没一个是随便写的0x1000ESP32 芯片内部固化的 ROM bootloader 约定上电后它从 Flash 的 0x1000 处找二级 bootloader 镜像。这个偏移是写死在 ROM 代码里的改不了。0x8000分区表的约定位置也是 ROM bootloader 写死的。分区表本身只有 3KB 左右后面留到 0x9000 是 NVS。0xf000PHY 初始化数据属于射频校准参数位置跟分区表方案绑定。0x10000第一个应用分区 app0 的默认起点这个是可配置的由分区表决定不是硬编码。这里的关键区别在于STM32 的 0x6000 是你自己切的可以改ESP32 的 0x1000 和 0x8000 是芯片 ROM 定的不能改只有 0x10000 是分区表说了算。补充一个容易踩的坑不同 ESP32 型号的 bootloader 偏移不一样。ESP32 和 ESP32-S2 是 0x1000而 ESP32-C3、ESP32-S3、ESP32-H2 这类 RISC-V 内核的型号bootloader 偏移是 0x0。同一个项目跨型号移植时如果手工写了烧录脚本这个值必须跟着改否则就是烧完没反应。4.2 怎么看 ESP32 的烧录地址这个问题的答案其实藏在你自己的 build 目录里根本不用上网查。几个可靠的办法方法一看 flash_args 文件。编译完成后build/flash_args里直接列着所有镜像和地址这是 idf.py 烧录时真正用的参数--flash_mode dio --flash_freq 40m --flash_size 2MB 0x1000 bootloader/bootloader.bin 0x8000 partition_table/partition-table.bin 0x10000 hello_world.bin方法二看分区表。partitions.csv里的 Offset 列就是各个分区的烧录地址# Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x6000, phy_init, data, phy, 0xf000, 0x1000, factory, app, factory, 0x10000, 1M,注意最后一行的 Offset 0x10000这就是你的 APP 该烧的地方。改了这个值烧录地址就跟着变所以改完分区表一定要idf.py fullclean再重新编译。方法三用 image_info 直接读镜像头。拿到一个来源不明的 app.bin想知道它是给哪个芯片、哪个地址编的用esptool.py image_info build/my_app.bin它会打印出镜像的 header magic0xE9、段数量、入口地址entry point通常是 0x4xxxxxxx 这样的 IRAM 运行地址、Flash 偏移等字段。这里有个特别值得强调的点入口地址和烧录地址是两回事。入口地址是代码运行时在内部 RAM 里的位置烧录地址是它在 Flash 里的存放位置两者差了十万八千里别看到 0x4008xxxx 就以为烧错了。方法四直接看烧录日志。idf.py flash跑的时候终端会一行行打印Writing at 0x00010000... (5 %)这就是实际写入地址最直观。4.3 AVR 的 bootloader 反而住在高地址前面讲的都是 bootloader 在低地址、APP 在高地址。AVR 反过来这个反差很有意思。AVR 芯片比如 ATmega328P的 Flash 从 0x0000 开始复位向量固定在 0x0000。但它的 bootloader 通常放在 Flash 的尾部位置由 BOOTSZ 熔丝位决定比如 32KB Flash 里 bootloader 占最高的 2KB也就是 0x7800-0x7FFF。上电时芯片从 0x0000 开始跑用户程序需要升级时再跳到高地址的 bootloader 区。所以如果你从 STM32 转过来做 AVR看到 APP 在 0x0000、bootloader 在高地址 的布局不要怀疑自己看错了两个平台的引导架构设计哲学确实不同。AVR 这种设计的历史原因是 Flash 早期读写不对称把很少被修改的 bootloader 放在高地址便于整片擦除用户区。4.4 国产替代片Flash 起点一样扇区划分可能完全不同GD32、APM32、AT32 这些 Pin-to-Pin 兼容的片子Flash 起点都是 0x08000000链接脚本基本不用改。但扇区划分经常不一样。我遇到过一回STM32F103 上是 1KB 一页换到某国产兼容片后变成了 2KB 一页bootloader 里那段按 1KB 循环擦除的代码直接把人家的数据擦掉了半页现象是升级后 APP 能跑但参数区全是乱码。查了两天才发现是页大小的问题。所以换芯片的第一件事永远是翻新片的参考手册看 Flash 章节把页/扇区大小记下来然后核对三处配置链接脚本的 ORIGIN、擦除代码里的页大小常量、APP 起点的对齐位置。5. 手把手定位你手上这个工程的烧录地址5.1 Keil MDK三处配置必须互相对齐Keil 里地址信息出现在三个地方必须一致第一处Options for Target - Target页里的 IROM1Start 就是烧录地址Size 是可写区间。这是编译和链接的依据。第二处Options for Target - Linker里如果勾了 Use Memory Layout from Target Dialog那分散加载文件是自动生成的如果自定义了.sct文件就得去那个文件里核对。第三处Options for Target - Debug - Settings - Flash Download里的 Programming Algorithm 起始地址。这一项如果和 IROM1 对不上下载器会告诉你地址超出算法范围。三处对齐之后Keil 生成的是.axf本质是 ELF里面带完整地址信息下载器能自己找到位置不会问你 base address。这也是为什么很多人用 Keil 下载从来没遇到过填地址这个问题——工具替你把这一步做掉了直到你换成 bin 手动烧录才暴露出来。5.2 GCC 工具链从 .ld 到 map 文件逐层确认GCC 环境下的确认链条更长但也更透明# 第一步看链接脚本里的 ORIGIN grep -A3 MEMORY linker/app.ld # 第二步看最终 ELF 的段地址 arm-none-eabi-readelf -S build/app.elf | head -20 # 第三步看 map 文件里向量的落点 grep -E \.isr_vector|__Vectors build/app.mapreadelf 输出里的.isr_vector段的 Addr 字段就是向量表的真实地址它应该和链接脚本的 ORIGIN 完全一致。如果不一样说明有别的脚本或编译选项覆盖了你的设置比如-Wl,--section-start之类的参数。5.3 IAR.icf 文件加工程选项双重检查IAR 的地址主要在.icf文件里但注意Project - Options - Linker - Config页里可以选择Linker configuration file是自动生成还是用自定义的。选了覆盖Override default工程里那份.icf才生效否则改了半天文件没用。还有个 IAR 特有坑.icf里的region定义和place at语句是两个层次region 划范围place at 定点。有时候 region 改对了但place at还写着老地址向量表就跑到别处去了。5.4 ESP-IDF四个文件交叉验证前面 4.2 讲过的四种方法其实指向四个文件build/flash_args、partitions.csv、build/partition_table/partition-table.bin、sdkconfig里面有CONFIG_PARTITION_TABLE_OFFSET默认 0x8000。这四处如果有任何一个被手工改过都会造成烧录地址混乱。我的习惯是升级 IDF 版本或换芯片型号后先跑一次idf.py fullclean idf.py build然后打开flash_args对一遍五秒钟的事能省几小时排查。5.5 只有一个 bin怎么反推它该烧哪这是最考验人的场景同事丢给你一个 my_app.bin没说地址。几个判断依据看文件大小。如果它接近整个 Flash 容量比如 490KB 左右很可能是带 bootloader 的整片镜像烧 0x08000000如果只有百来 KB大概是单独的 APP得烧在某个偏移上。看前几十个字节。如果是 ARM Cortex-M 的 APP偏移 0x6000 之前的空间在编译时不会被填充进 bin所以 bin 的第 0 个字节就是向量表的第一个字节也就是初始 MSP 值。STM32 的 SRAM 在 0x20000000 段所以前 4 字节通常是00 00 01 20小端表示 0x20010000这种形式。如果读出来的前 4 字节落在 0x08000000 段比如00 00 00 08那这份 bin 的起始地址多半就是 0x08000000。看第 5 到第 8 字节。那是复位向量的值应该落在 Flash 段内且是奇数Thumb 模式最低位为 1比如 0x08000C4D。这个值减去文件里的相对偏移就能反推出基准地址。这个方法不严谨但很实用我在没有文档的时候经常这么估。6. 地址填错之后的四种现场与排查链路6.1 现场一烧完板子彻底不启动连原来的功能也没了这是最典型的覆盖了 bootloader。症状是上电毫无反应串口不发任何数据调试器连上去看 0x08000000 处的内容发现是 APP 的向量表而不是 bootloader 的。排查链路把烧录工具的 base address 从 0 改成正确的 0x08006000重新烧 APP。如果 bootloader 已经被覆盖先用出厂 ROM bootloader拉高 BOOT0 上电或者调试器把 bootloader 重新烧回 0x08000000再烧 APP。预防手段很简单给 bootloader 加一段看起来就不像能跑的特征比如让它往串口固定打印一行版本号。烧错的时候你会立刻发现打印变了而不是等到产品出问题才发现。6.2 现场二能烧进去一跑就 HardFault症状是下载成功、校验通过但上电后立刻进 HardFault或者卡在启动文件里。第一嫌疑人是链接地址和烧录地址不一致。用调试器读一下 0x08000000 处的前两个字如果第一个字不是合法的 SRAM 地址说明向量表就没在正确位置。第二嫌疑人是 VTOR 没设或者设错。第三嫌疑人更隐蔽bootloader 跳转时用错了函数指针类型或者跳转前没关中断、没复位外设。6.3 现场三能进 main主循环也跑但一开中断就死这就是 VTOR 的锅几乎不用怀疑。分开验证的方法很简单让 APP 先把所有中断都关掉__disable_irq()跑一段纯计算逻辑如果能跑通再把中断打开一开就崩那就确认是向量表偏移问题。修的时候记住VTOR 必须在使能任何中断之前设置也就是main的第一行做这件事最稳妥更严谨的做法是在启动文件的SystemInit里就设好。6.4 现场四OTA 升级包里校验老是失败或者差一个固定偏移这类问题的根源通常是把相对偏移和绝对地址混用了。比如升级包结构里写了app_offset 0x08006000APP 里的解析代码却拿它当相对偏移用去flash_base 0x08006000处写数据一下写到 128MB 以外去了。还有一种情况是差 0x1000 这种整数那就往扇区边界和分包大小上想。很多升级协议要求按固定块大小传输如果偏移没对齐块大小每包都会多算或少算几个字节。6.5 一张排查顺序表遇到任何烧录地址疑似不对的问题我一般按这个顺序走从低成本的检查开始顺序检查项快速验证方式1烧录工具 base 是否与链接脚本 ORIGIN 一致打开工具配置和 .ld/.sct 对比2向量表前 4 字节是否是合法 SRAM 地址用 hexdump 看 bin 开头3VTOR 是否设成和 ORIGIN 相同的值全局搜索 SCB-VTOR 和 VECT_TAB_OFFSET4APP 起点是否对齐 Flash 扇区边界查芯片手册扇区表5bootloader 实际体积是否超出预留空间编译后看 size 输出6擦除代码的页大小是否与芯片一致对照参考手册 Flash 章节7跳转前的环境清理是否完整检查关中断、关外设、清标志位7. 几个我踩过的具体坑7.1 hex、bin、elf 三种格式对地址的理解完全不同Intel HEX 文件里每行都带地址字段:开头之后的第 3-6 个字符。所以用 hex 文件烧录时工具会自己解析地址不会问你 base address就算问了填错也不影响因为它以文件里的记录为准。bin 是纯数据必须手工提供地址。elf 带完整的段表和符号表调试器能自动定位是三种里信息最全的。这个差异造成的经典事故是同一个工程导出了 hex 和 binhex 烧一切正常bin 烧就出问题。因为 hex 里的地址是你编译时定的比如 0x08006000而 bin 的地址要靠你手工填。很多人用 hex 验证过就以为没问题转产时换成 bin 忘了填地址整批烧成砖。7.2 合并固件时的手工加偏移量产时经常要把 bootloader 分区表 APP 合并成一个整片镜像方便一次烧录。这时候合并工具需要你提供每一项的偏移# 用 srec_cat 把三个 bin 按指定地址拼起来 srec_cat bootloader.bin -Binary -offset 0x08000000 \ app.bin -Binary -offset 0x08006000 \ -o combined.hex -Intel注意-offset参数加的是绝对地址。这里最容易错的是漏了 Flash 基址只写了 0x6000结果 APP 被塞到了 0x6000 而不是 0x08006000合并出来的文件看着没问题烧进去就是一片空白。7.3 OTA 包里也要写地址而且必须是相对偏移升级包的头结构里一般有这几个字段magic、版本号、目标地址、长度、CRC。这里的目标地址应该写相对 Flash 起点的偏移也就是 0x6000而不是 0x08006000。理由是 OTA 固件应该跟芯片型号解耦——同一份升级包理论上烧到同系列不同容量的芯片上Flash 起点都是 0x08000000用相对偏移更安全。我在一个项目里见过写绝对地址的版本后来换了一颗 Flash 起点不同的国产芯片0x00000000整个升级包格式就得重做几十个已发货设备的升级链全断了。7.4 改分区表之后的连锁反应ESP32 项目里改分区表是最容易引发连锁故障的操作。你把 app0 的 Offset 从 0x10000 改成 0x20000不只是烧录地址变了还影响NVS 区域的位置、OTA 数据分区的位置、已烧录设备升级时的兼容性。已发货设备的分区表在老位置新固件按新地址找分区直接找不到。所以量产之后就不要动分区表了。真要动得设计一套分区表迁移机制那是另一个话题了工作量不比写业务代码小。8. 最后说几个实用习惯我现在的做法是在每个项目的 README 最前面固定写三行Flash 起点、bootloader 占多少、APP 烧录地址。这三个数字写在最显眼的地方任何人接手都不用再问。另外还在 bootloader 里加了个隐藏命令串口发特定字符串就能打印当前的分区信息包括 bootloader 自身大小和 APP 起点的预期值配合 APP 上报自己的链接地址一比对就知道有没有错位。还有个建议是别过度依赖工具的默认值。J-Flash、STM32CubeProgrammer、esptool 这些工具的默认行为各有各的逻辑有的会帮你重映射有的会直接报错。把默认值当成待确认项而不是肯定对能避开绝大部分地址相关的坑。至于 0、0x08000000、0x6000 这三个数字现在你再看它们应该清楚各自站在哪一层了0 是文件层面的偏移起点0x08000000 是硅片层面的物理起点0x6000 是你自己在 Flash 上画的那条分区线。搞清楚谁在说话地址这件事就不再神秘了。