ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

烧录地址详解:0x08000000、0与0x6000的区别及MCU/SoC差异

烧录地址详解:0x08000000、0与0x6000的区别及MCU/SoC差异 烧录地址这件事很多刚入行或者做了几年产品但没深究过的朋友看到 0、0x08000000、0x6000 这三个值同时出现在文档里时确实容易懵。我当年也被这个问题卡过同一块板子一会儿在 Keil 里下程序填 0x08000000一会儿用命令行工具烧 bootloader 填 0一会儿调 ESP32 又看到文档让你写 0x6000。这三个地址到底谁说了算今天就把这个事从头到尾捋清楚看完你以后看到任何烧录地址都不会再怵。先说结论烧录地址不是烧录工具随意定的它是芯片设计者定好的内存映射规则和软件工程组织方式共同决定的结果。0x08000000 是 STM32 这类 MCU 内部 Flash 的起始地址0 则是 CPU 启动时默认取向量表的地址很多时候也是 BootROM 或系统存储器的映射起点而 0x6000 这种相对较小的值通常不是绝对地址而是 Flash 里的一个偏移量比如分区表的偏移、某个数据分区的起始位置。这三者出现在不同场景里代表的事情完全不同。这篇文章适合这几类人看正在用 STM32、ESP32、GD32、ESP8266 做开发但搞不清下载地址怎么填的被官方烧录工具里的 Offset 字段搞糊涂的以及想弄明白 bootloader 和 app 之间地址怎么规划的人。我会把三种典型值的来源、计算方式、查看方法一次讲透最后还会附上我实际踩过的坑。1. 内容整体设计与思路拆解1.1 为什么会有这么多不同的烧录地址要理解这个问题的本质得先搞明白“烧录地址”到底指什么。严谨地说烧录地址是“你要写入的数据在目标存储介质中的位置”。Flash、EEPROM、外扩 NOR Flash物理上各有各的起始地址。而“存储介质”又分很多种单片机内部 Flash、芯片外挂的 SPI Flash、SD 卡、eMMC。每种的地址映射规则都不一样。举个例子STM32F103 的内部 Flash 起始地址是 0x08000000这是芯片数据手册里 Memory Map 写死的用户程序必须从这里开始存放或者至少向量表必须能被 CPU 在启动时从这里读到。至于为什么是 0x08000000 而不是 0x00000000跟 ARM Cortex-M 内核的设计有关系内核固定从 0x00000000 读取向量表但芯片厂商可以把 Flash 映射到这个地址段比如通过 boot 引脚或者 Option Byte 控制别名映射也可以让 Flash 待在 0x08000000然后通过硬件机制在启动时把 0x00000000 别名到 Flash 上。STM32 的做法是后者所以你在烧录工具里看到的地址是 Flash 的物理地址 0x08000000。而在 ESP32 这类无线 SoC 上情况完全不同。ESP32 没有把 Flash 映射到统一的内存地址空间里让用户直接操作它有一个 Cache 映射机制但实际下载时用的是 SPI Flash 控制器操作的是 Flash 的偏移地址。所以乐鑫的烧录工具 esptool 里写的地址是“相对于 Flash 起始位置的第几个字节”比如 0x10000、0x9000、0x6000这些不是 CPU 的内存地址而是 Flash 分区表中的偏移量。这就是为什么你会看到很小的地址值——它们是偏移不是绝对地址。很多人的误区在于用 STM32 的思维去理解 ESP32拿着绝对地址去找分区结果怎么都对不上。两种芯片的设计哲学不一样地址的含义自然也不同。1.2 三种地址值分别对应什么场景为了快速建立直觉可以把这三个值先对号入座0x08000000STM32、GD32、APM32 等 Cortex-M 内核 MCU 的内部 Flash 起始地址烧录工具里填的“下载起始地址”也是大多数 ARM MCU 交叉编译链接脚本里 Flash 段的 LMA加载地址。0通常是 BootROM 的起始地址或者系统存储器System Memory映射到 0x00000000 的情况也可能是某些烧录工具在“相对偏移”模式下显示出来的初始值。在不同语境下含义不同要具体看工具。0x6000典型出现在 ESP32/ESP8266 的烧录命令或分区表里它不是一个 CPU 内存地址而是 Flash 内部的偏移量。比如 ESP32 的 NVS 分区或者 PHY init data 分区可能从 0x6000 开始。从这里能看到一个规律地址值的大小不在于是不是“正规”地址而在于芯片设计者准备让 CPU 怎么访问存储介质。有的芯片把 Flash 映射进统一内存地址空间MCU 风格有的芯片用独立的 Flash 控制器按偏移访问SoC 风格。后面我会详细展开这两种风格下的地址规划方式。1.3 选对地址为什么直接影响程序能不能跑起来烧录地址没填对最常见的表现就是烧录时校验通过但一上电程序根本不运行或者 bootloader 能跳转但 app 死活起不来更隐蔽的是程序能跑但偶尔死机、数据错乱。原因在于烧录地址决定了两个关键点一是 CPU 启动时能不能在正确的位置找到向量表二是链接器生成的程序地址和实际存放地址是否一致。前者错了CPU 第一条指令就取错后者错了程序内部跳转全是乱跳。可以说地址填对是程序能跑的前提比代码逻辑优先级更高。所以这篇文章里我不光讲地址本身还会讲怎么通过工具反查地址、怎么通过链接脚本确认地址从根上解决这个困扰。2. 三种地址值的详细解析与实操要点2.1 0x08000000 是从哪来的为什么 STM32 烧录要填它打开 STM32F103 的数据手册翻到 Memory Map 那一页你会看到 0x08000000 到 0x0801FFFF512KB 版本这一段标注为 Flash。这个地址是由芯片厂商设计时定死的你没法改。烧录器ST-Link、J-Link、DAP-Link往这个地址写数据本质上是操作 Flash 控制器按照页/扇区擦写。那为什么不能从 0x00000000 开始烧因为 STM32 的 0x00000000 在启动时是“别名区”它本身不一定是物理存储介质。芯片上电后根据 BOOT0 引脚的电平部分系列还有 BOOT1决定 0x00000000 映射到哪一块物理存储BOOT0 拉低从主 Flash 启动0x00000000 被映射到 0x08000000 起始的 Flash 区域。也就是说CPU 实际上是从 0x08000000 开始取指但地址总线看到的是 0x00000000。BOOT0 拉高、BOOT1 拉低从系统存储器启动0x00000000 映射到内置 BootROM里面是出厂烧录的 bootloader一般用于串口下载。BOOT0 拉高、BOOT1 拉高从内部 SRAM 启动0x00000000 映射到 SRAM 起始地址常用于调试。所以你会看到即便 STM32 支持从 0x00000000 “逻辑启动”但真正程序存放的物理地址是 0x08000000。IDE 和烧录工具里让你填的下载地址指的就是物理 Flash 地址。Keil 的 Target 选项卡里我一般填 0x08000000IAR 的 Download 选项卡里也是 0x08000000。烧录器会直接访问这个地址不受 BOOT 引脚影响。这里有一个很实用的经验如果你用 ST-Link 给 STM32 下载程序无论 BOOT0 是拉高还是拉低烧录都能成功因为烧录器不进 BootROM它直接通过调试接口写 Flash。但是写完以后复位运行如果 BOOT0 拉高程序还是不会从 Flash 启动这是很多新手第一次遇到“烧录成功但运行失败”的头号原因。2.2 “0”是什么情况下出现的烧录地址“0”这个值出现的场景比较多要分情况看第一种BootROM 的入口。很多 Cortex-M 芯片从系统存储器启动时0x00000000 就是 BootROM 的起始地址。但这种情况一般在工厂量产或者串口 ISP 下载时才有意义普通开发者不太会直接往里面烧东西因为 BootROM 出厂就写好了用户改不了。第二种烧录工具里的“偏移地址 0”。一些烧录软件在配置界面里会让你填 Program Address / Flash Offset默认值是 0。比如 J-Flash 新建工程时如果选的芯片内部 Flash 起始地址是 0x08000000你填 0 表示“从 Flash 起始位置开始烧”实际物理地址还是 0x08000000。这个“0”不是绝对地址而是偏移量。不少人在这里被误导以为填 0 就是往 0 地址写其实不是。第三种ESP32 的 bootloader 烧录地址。虽然 esptool 的命令里 bootloader 通常写 0x1000ESP32 传统单核模式或 0x0ESP32-C3 等较新芯片的 ROM 启动模式但这里的 0 指的是 Flash 偏移 0也就是 Flash 的第一个扇区。0x00000000 的绝对地址如果换算成 ESP32 的 Cache 映射区大概在 0x3F400000 附近但这不是你烧录时要关心的地址。第四种数据文件比如 MAC 地址、校准参数从 Flash 头部开始存放Offset 就是 0。这种情况在工厂产线里很常见测试工装会把参数写在 Flash 开头软件里读 0x00000000 开始的一段区域。注意如果 Flash 头部放了 bootloader就不能同时用来放参数两者会冲突。所以看到“0”的时候先问一句这个 0 是绝对地址还是偏移量如果是偏移量基地址是多少搞清楚了后面的事就顺了。2.3 0x6000 是怎么算出来的为什么 ESP32 要这么写0x6000 这个值最常出现在 ESP32 的烧录命令或者分区表里。我用一个实际的例子来说在 ESP32 的默认分区表partitions.csv里常见布局是这样# Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x5000, otadata, data, ota, 0xe000, 0x2000, app0, app, ota_0, 0x10000, 0x200000, app1, app, ota_1, 0x210000,0x200000,注意 NVS 分区从 0x9000 开始而早期 ESP8266 或部分项目里 phy_init 或者 RF_CAL 分区可能从 0x6000 开始。0x6000 不是随便写的它一般紧跟在 bootloader 之后。ESP32 的 bootloader 放在 Flash 偏移 0x10004096长度通常不超过 0x500020480所以下一个分区从 0x6000 开始刚好错开中间留了一点余量。老 ESP8266 的 bootloader 有些放在 0x00000但大小约 0x6000 左右于是后面的用户参数区从 0x6000 开始这其实是“分区表里算出来的地址”。如果你手动烧录 ESP32命令往往长这样esptool.py --port /dev/ttyUSB0 write_flash 0x1000 bootloader.bin 0x8000 partitions.bin 0xe000 boot_app0.bin 0x10000 app.bin这里的 0x1000、0x8000、0xe000、0x10000 分别是 bootloader、分区表、OTA 信息、app 分区的 Flash 偏移。0x6000 如果出现一般是某个参数分区或者出厂数据分区的偏移。乐鑫默认的 bootloader 编译出来大小约 0x2000~0x5000如果你发现 bootloader.bin 实际大小超过 0x6000那就不能把下一个分区放在 0x6000得往后挪否则会互相覆盖。这就是为什么 0x6000 不是固定不变的它取决于前面放了多少内容。很多人在网上看到 0x6000 就照抄结果 bootloader 版本升级、体积变大把后面的分区覆盖了系统就出现诡异问题。正确做法是每次编译后看生成的 partition table或者用 esptool 读取 flash 的当前分区信息来确认。2.4 ESP32 烧录地址到底怎么查回到热搜词“怎么看 ESP32 的烧录地址”。这个问题在社区里被问了很多次因为 ESP32 不像 STM32 那样在 IDE 里直接填一个绝对地址就行它的地址藏在分区表、编译输出、编程工具参数三个地方得自己组合起来看。第一个地方分区表 CSV。在 ESP-IDF 工程里打开 partitions.csvType 为 app 的分区 Offset 列就是 app 的烧录偏移。默认一般是 0x10000但如果你用了自定义分区表或者启用 OTA可能变成 0x20000、0x100000 等。第二个地方编译输出。ESP-IDF 编译结束后终端会打印如下一行App app is the app partition, offset: 0x10000或类似提示。有些版本还打印Project build complete. To flash, run: idf.py -p PORT flash这说明默认烧录配置已经根据分区表算好了不需要你手动填。如果你用 esptool 手动烧可以在 build 目录下找到flash_args文件里面就是完整的烧录参数包括每个文件的地址。第三个地方用 esptool 从芯片反读。如果板子上已经烧过程序你可以先读出来看看内容分布esptool.py --port /dev/ttyUSB0 read_flash 0x00000 0x100000 flash_dump.bin然后用十六进制编辑器打开 dump 文件通常在 0x1000 位置能看到 ESP32 的 bootloader 魔数在 0x8000 位置能看到分区表数据。用partition_table解析工具也可以。反读的方法在排查“程序到底烧到哪了”时特别有用。第四种用 idf.py 菜单配置。运行idf.py menuconfig进入 “Serial flasher config” 菜单里面有 “Flash size”、“Flash SPI mode” 等配置但不会直接显示烧录地址。真正决定地址的是 partition table 选项里选择的 CSV 文件。选中哪个分区表app 的偏移就在那个 CSV 里。这一步很多人找不到因为在 menuconfig 里改的是“选哪个分区表文件”地址本身不在菜单里显示。结合这几个方式你就能完整回答“ESP32 的烧录地址是多少”这个问题。简单说bootloader 在 0x1000、分区表在 0x8000、app 在 0x10000默认单 OTA 分区情况下其他分区按 CSV 定义来。如果你想确认自己手里这块板子的实际布局最快的方式是执行一次idf.py flash看它打印出来的命令参数那是最权威的答案。3. 实操过程与核心环节实现3.1 通过 Keil 查看和设置 STM32 烧录地址Keil 是 STM32 开发者最常用的 IDE。很多人知道在 Options for Target 里能找到烧录地址但没细看它与链接脚本、烧录算法之间的关系。我按顺序带你过一遍标准设置步骤。第一步打开工程点击魔法棒按钮Options for Target进入 Target 选项卡。这里 IROM1 的起始地址Start就是程序的存放地址默认新建 STM32 工程时芯片包会自动填成 0x08000000大小根据芯片 Flash 容量填比如 0x80000512KB。这个 Start 值和后面烧录地址是挂钩的。第二步进入 Utilities 选项卡点 Settings 按钮。这里能看到烧录算法的 Flash Download 配置。在 Programming Algorithm 区域有一个 Start 地址通常也是 0x08000000Size 与 Flash 容量一致。烧录器在擦除和写入时严格按照这里填的地址范围操作。如果你在这里填了 0x08000000但 Target 选项卡里 IROM1 填的是 0x08010000比如你要把 app 放到偏移 64KB 的位置两者就会出现不一致。老实说Keil 的这种设计确实容易让人搞混前者是算法能烧写的 Flash 范围后者是程序的实际加载地址。一般来说从 0x08000000 开始烧就按默认配置除非你是做 bootloader app 架构app 的 IROM1 才会改但 Flash Download 的算法范围可以保持覆盖整个 Flash。第三步编译后点 Download烧录器会按照 Utilities 设置里的算法对 0x08000000 开始的区域做擦除和写入。烧完以后程序复位运行CPU 在 0x08000000 找到向量表开始执行。实际中我遇到过的典型错误是改了 IROM1 起始地址却没有改中断向量偏移VECT_TAB_OFFSET导致程序虽然烧到了 0x08010000但启动后 CPU 还是从默认的 0x08000000 找向量表自然跑不起来。这个问题不是烧录地址本身填错而是软件侧没有同步调整但很多人误以为是烧录地址填错了所以排查时走了很多弯路。3.2 用 STM32CubeProgrammer 验证实际烧录结果除了 IDE 里烧录ST 官方的 STM32CubeProgrammer 也是查烧录地址的利器。它能看到更详细的信息适合验证问题。用 ST-Link 连接板子打开 STM32CubeProgrammer点 Connect。在右上角的 Memory 显示区域手动输入地址 0x08000000然后 Read你会看到 Flash 开头的数据。如果程序正确烧录这里应该是合法的 ARM 向量表0x08000000: 0x20020000 0x08000519 0x08000601 ...第一个 32 位字是初始栈指针SP值一般在 SRAM 地址范围内第二个字是复位向量Reset_Handler值落在 Flash 地址范围内。看到这两个值基本可以确认程序烧录位置和链接地址一致。这个工具另一个实用功能是 Option Bytes 查看。在 STM32CubeProgrammer 左侧选择 Option Bytes能看到 nBOOT0、nBOOT1 等配置位。有些芯片出厂默认 nBOOT0 是 0意思是 BOOT0 引脚电平不生效固定从主 Flash 启动有些复位值设置不同。如果你发现程序烧进去但不运行可以先来这里看一眼启动配置九成问题都能定位。我在给客户做产线支持时经常先让产线兄弟发一张 Option Bytes 截图比让他们描述现象快得多。3.3 用 esptool 完整烧录并验证 ESP32 的分区布局ESP32 的烧录操作比 STM32 要“命令行化”一些。用 esptool 手动烧录时我一般会按这个流程走保证不出错先擦除整片 Flash确保没有残留的旧 bootloader 或分区表影响判断esptool.py --port /dev/ttyUSB0 erase_flash然后依次烧录。先烧 bootloaderesptool.py --port /dev/ttyUSB0 write_flash 0x1000 build/bootloader/bootloader.bin再烧分区表esptool.py --port /dev/ttyUSB0 write_flash 0x8000 build/partition_table/partition-table.bin接着烧 appesptool.py --port /dev/ttyUSB0 write_flash 0x10000 build/app.bin如果工程里还有其他分区比如 NVS、存储、自定义数据分区也需要按分区表偏移逐个烧录或者先用擦除操作清掉再让程序初始化时重建。注意这里 0x1000、0x8000、0x10000 是 Flash 偏移不是芯片 RAM 地址也不是 ROM 地址。无数人第一次接触时把 0x10000 当成“内存地址”然后到处找 0x10000 为什么对应不上代码里的某个变量这是概念没切换过来。烧录完成后可以读回分区表区域检查内容esptool.py --port /dev/ttyUSB0 read_flash 0x8000 0x1000 partition_table_dump.bin用 ESP-IDF 里的gen_esp32part.py工具解析python gen_esp32part.py partition_table_dump.bin能看到实际烧录进去的分区表内容包括每个分区的偏移、大小、类型。这个方法在怀疑分区表被覆盖或烧错时非常可靠比看编译日志直观得多。3.4 从反汇编提取真实地址链接关系的技巧有时候光看烧录地址还不够你还得确认“程序里的函数放置位置”和“烧录地址是否匹配”。这种场景在调试 bootloader 跳转 app、或分析现场固件时特别有用。以 STM32 为例编译生成的 axf/elf 文件可以用fromelf或objdump反汇编。在 Keil 里你可以用命令行fromelf --text -c -d build/app.axf app_disasm.txt打开反汇编文件看复位向量处的内容比如Reset_Handler 0x08000518: ldr r0, [pc, #16] 0x0800051c: bl 0x08000210说明复位后第一条指令在 0x08000518。如果这个地址和你在烧录工具里设置的起始地址 0x08000000 属于同一个段差一个固定偏移就说明链接地址和烧录地址是配套的。如果反汇编出来的地址是 0x08010000 开头但烧录工具填的起始地址是 0x08000000那么必然跑不起来——除非 bootloader 或硬件做过重映射。ESP32 侧更简单出问题的场景主要在 OTA 时。当你用 OTA 方式升级 app 时新固件被写入另一个分区比如 app1偏移 0x210000然后 bootloader 根据 OTA 数据选择从 app0 还是 app1 启动。这时你如果想验证当前运行的是哪个分区可以读一下 OTA data 分区的内容确认里面记录的分区编号。烧录地址和运行选择的对应关系在 OTA 方案里尤其重要因为只烧 app0 的偏移而忘记烧 OTA data可能永远从 app1 启动导致你总感觉“新固件没烧进去”。4. 常见问题与排查技巧实录4.1 烧录成功但上电不运行这是我遇到最多的问题没有之一。现象是烧录器提示 Download 成功校验也通过但板子上电就是没反应。排查步骤我按优先级排列第一确认 BOOT 引脚状态。STM32 的 BOOT0 如果被拉高上电后进的是 BootROM不是主 Flash。用万用表量一下 BOOT0 引脚电平正常情况下应接近 GND。有次我调试一块定制板原理图上 BOOT0 画的是下拉但 PCB 贴片时电阻焊错位置导致 BOOT0 悬空上电偶尔进 BootROM程序时好时坏折腾了很久。第二看复位引脚。如果有外部看门狗或者复位芯片确认它没有在反复复位芯片。用示波器抓 NRST 引脚如果是低电平脉冲周期出现说明有东西在复位。我遇到过客户板子上的复位电容过大导致上电复位时间太长程序还没来得及跑就被拉死。第三检查烧录算法配置的起始地址。有些工程被人改过Flash Download 里的 Start 地址不是 0x08000000而是 0x08010000用户程序却在 0x08000000 编译这时虽然显示“烧录成功”但实际写入位置和程序预期的链接位置不一致。用 STM32CubeProgrammer 读一下 0x08000000 前 16 字节如果全是 0xFF 或者乱码八成就是这个原因。第四检查向量表偏移。如果程序加了 bootloaderapp 的链接地址被改到 0x08010000但没有设置 VECT_TAB_OFFSET0x10000CPU 启动后还是从 0x08000000 取向量表而这在 bootloader 方案里可能是空的或者只有 bootloader 的向量表。这时一个常见的“伪现象”是bootloader 能跑但跳转 app 时死机。4.2 ESP32 烧录时报错 “Invalid head of packet” 或芯片反复重启给 ESP32 烧录时最让人抓狂的一个错误是A fatal error occurred: Invalid head of packet (0x34)这个错误绝大多数情况不是烧录地址的问题而是串口连接不稳定或者 GPIO0 没有拉低。ESP32 进入下载模式需要 GPIO0 在复位时为低电平很多开发板有自动下载电路串口工具会控制 DTR/RTS 来切换但如果线材不好或者 USB 转串口芯片供电不稳会导致握手失败。处理方法换一根短一点的 USB 线或者手动把 GPIO0 拉低再复位。别急着怀疑地址填错。但有一种情况确实与地址有关如果你用 esptool 往 ESP32 的 0x0 地址烧录了一个非法的 bootloader芯片上电后无法识别头部格式会不停进入下载模式或反复重启表现也很像“通讯错误”。所以排查这个报错时建议第一步先esptool.py read_mac或esptool.py flash_id如果这两个命令能正常返回说明芯片通讯没问题问题大概率出在烧录内容或地址上。如果连 flash_id 都报错优先检查串口和 GPIO0。4.3 烧录地址覆盖导致分区数据丢失这个坑我踩得特别深。有次给 ESP32 项目调 WiFi 校准数据反复烧录了几十次突然发现 WiFi 连接不稳定、射频指标异常。排查到最后发现NVS 分区里的校准数据被冲掉了。原因是那次手动烧录时我把 bootloader 的偏移从 0x1000 改成了 0x0bootloader 变大后直接盖到了原先放 NVS 的 0x9000 区域。之后每次启动NVS 读出来全是坏数据WiFi 校准失败射频性能自然劣化。这里给所有做 ESP32 量产的朋友一个建议批量烧录时不要手动敲地址用idf.py flash或者esptool.py --flash_mode dio write_flash配合 flash_args 文件。如果不小心需要手动指定地址烧录前先用esptool.py --port /dev/ttyUSB0 read_flash 0x0 0x1000 dump.bin看一眼 Flash 开头内容确认没有放重要数据。另外NVS 分区被破坏后最简单的修复方式是执行esptool.py --port /dev/ttyUSB0 erase_region 0x9000 0x5000然后再跑一次程序让 NVS 重新初始化。别整片擦除否则 bootloader 和分区表也没了还得重新烧费时间。4.4 地址填对但校验失败有时候烧录器提示校验失败但不是地址错而是 Flash 本身有问题。比如 STM32 的某个扇区损坏或者 Flash 写保护被打开。STM32 的读保护RDP一旦设置为 Level 1调试接口的读写会受到限制烧录时可能表现为“能连上但写不进去”。处理方法用 STM32CubeProgrammer 切到 Option Bytes把 RDP 降级到 Level 0。注意降级操作会触发整片擦除所以先备份别保存重要数据在芯片里。ESP32 上偶尔也会遇到校验失败常见原因是电压不稳导致 Flash 写入错误。ESP32 对供电要求比较高尤其是射频开启瞬间电流很大如果 USB 口供电不足烧录途中电压跌落写入就会出错。排查方法换一个有独立供电的 USB Hub或者用 5V 2A 适配器给开发板供电再烧录。另一个原因是 Flash 的 SPI 模式不匹配比如芯片是 DIO 模式但烧录时强制用了 QIO旧固件或出厂设置可能不认出现写入后校验不一致。用esptool.py --flash_mode dio强制指定 DIO 模式通常能解决。4.5 常见问题速查表问题可能原因解决方法STM32 烧录成功不运行BOOT0 拉高量引脚电平确保 BOOT00STM32 烧录成功不运行向量表偏移未设置设置 VECT_TAB_OFFSET 并在代码里调用STM32 烧录校验失败Flash 写保护Option Bytes 里解除保护STM32 烧录校验失败烧录算法起始地址错误检查 Flash Download 里的 StartESP32 烧录报 Invalid headGPIO0 未拉低或串口不稳检查 USB 线、GPIO0 状态ESP32 烧录后 WiFi 异常NVS 分区被覆盖erase_region 0x9000 0x5000 后重跑ESP32 校验失败供电不足换独立供电加粗电源线ESP32 手动烧录后起不来分区表偏移与 flash_args 不一致用 idf.py flash 自动生成参数任何芯片烧录地址填错概念混淆绝对地址 vs 偏移先查 Memory Map/分区表再填这张表我是在带新人时整理出来的基本覆盖了 80% 的烧录地址问题。剩下 20% 多半是芯片本身硬件问题那就要靠示波器和逻辑分析仪深入查了。5. 从原理到实战的完整认知框架5.1 理解地址的两个核心区别绝对地址和偏移量学过一段时间嵌入式后你会发现很多混乱源于没有区分“绝对地址”和“偏移量”。STM32 烧录地址 0x08000000 是绝对地址它对应物理 Flash 在整个芯片内存映射空间的位置而 ESP32 烧录时写的 0x10000、0x6000 这类是 Flash 偏移量它们对应的是“Flash 这个独立芯片内部从 0 开始数的位置”不是 CPU 直接寻址的内存地址。这两种概念在设计上有一个本质区别绝对地址和 CPU 的寻址空间直接关联改动它意味着 CPU 从哪个地址取指令偏移量则更像是“文件系统里的目录位置”它负责管理 Flash 这个外设资源CPU 需要通过 SPI 控制器或 Cache 映射间接访问。所以你会发现在 STM32 上改烧录地址往往要同步改链接脚本和启动代码而在 ESP32 上改烧录地址主要改的是分区表和 esptool 命令参数。前者牵连到代码运行视图后者牵连到存储布局视图。拿一个生活化的类比来说STM32 的 Flash 地址就像你家的门牌号每个房间有固定门牌CPU 靠门牌直接上门ESP32 的 Flash 偏移像图书馆的索书号书的位置按编号顺序排布读者CPU得先通过目录分区表找到它在哪个书架第几层然后再去取。不能说门牌号是“正确”地址而索书号不是只是系统设计不同。5.2 怎么判断你该填哪种地址拿到一个新项目或者新芯片时先问三个问题第一芯片是不是 Cortex-M 内核并具有统一内存映射的 Flash是的话烧录地址大概率是 Memory Map 里 Flash 的起始绝对地址比如 0x08000000、0x00000000部分 Cortex-M0 芯片 Flash 就在 0 地址开始、0x10000000有些芯片 Flash 映射到其他位置。去数据手册的 Memory Map 章节找到 Flash 那一段起点就是烧录地址。第二芯片是不是带外部 SPI Flash 的无线 SoC比如 ESP32、ESP8266、nRF52840 外挂 Flash 方案、部分国产 WiFi 芯片这些没有把 Flash 完全映射进 CPU 寻址空间或者映射方式和 MCU 很不一样烧录工具操作的是 Flash 偏移。去编译产物的分区表或下载脚本里确认偏移值。第三IDE 或者烧录工具里有没有让你填 “Offset” 而不是 “Address”如果有说明工具已经默认了基地址你填的是偏移。这时候要搞清楚基地址是什么否则很容易把 0x10000 当成直接寻址地址填进去。记住这三步遇到任何新芯片都不会慌。其实搞嵌入式的很多时候不是技术难度大而是“同一件事在不同厂商语境里有不同叫法”。烧录地址、编程地址、Flash Offset、APP 起始地址说的可能是同一个东西也可能不是必须在具体芯片的具体上下文里去验证。5.3 烧录地址与启动流程的关系地址不是孤立存在的它和芯片的启动流程直接绑定。STM32 的启动流程是上电后 CPU 从 0x00000000 读初始栈指针SP从 0x00000004 读复位向量PC然后跳过去执行。由于 0x00000000 在 BOOT00 时被硬件映射到 0x08000000所以实际上读的是 Flash 里的前 8 个字节。这就是为什么你烧录时程序的开头必须包含一个合法的向量表而且必须放在烧录起始地址处。如果你做了 bootloader app 架构app 的烧录地址是 0x08010000那么 app 的向量表就在 0x08010000。但 CPU 从 bootloader 跳转过来时还是默认从 0x00000000 取向量表所以 app 代码里必须设置 SCB-VTOR 0x08010000把向量表偏移重定位到 app 的位置。这一步没做跳转过去后第一个中断就能把程序搞飞。很多开发者理解的“烧录地址”只是下载时填的地址但完整的地址规划还要考虑到运行时 CPU 从哪里读向量表。ESP32 的启动流程就完全不一样。芯片上电后先运行芯片内部 ROM 里的第一级 bootloader它根据 eFuse 和 strapping pin 决定从 SPI Flash 的哪个偏移加载第二级 bootloader。ESP32 的二级 bootloader 默认从 Flash 偏移 0x1000 加载它解析分区表找到类型为 app 且具备启动标志的分区然后把 app 加载到 RAM 或者直接在 Flash 上执行XIP。这个过程中Flash 偏移 0x1000 是 ROM 固化代码约定的不能随便改除非你在编译 bootloader 时同时改掉这个约定。所以 ESP32 的 0x1000、0x8000 这些偏移是“bootloader 链式加载”的产物和 STM32 那种“CPU 直接硬件取向量表”截然不同。设计启动流程时烧录地址不是一个孤立的值而是整条启动链路上每一环的坐标。你在哪个环节填错了哪个环节就会断程序就起不来。5.4 地址规划的经验法则和工具链配合做产品级固件时我建议把地址规划当成“内存地图”来设计而不是每次烧录现想。具体做法是维护一份文档列清楚每段 Flash 的用途、偏移、大小然后让 IDE 配置、链接脚本、烧录脚本三处保持一致。对 STM32 类 MCU我通常在工程里这样规划区域地址范围大小内容Bootloader0x08000000 – 0x0800FFFF64KB升级引导代码App0x08010000 – 0x0807FFFF448KB用户应用参数存储0x08080000 – 0x08080FFF4KB校准参数/序列号App 的链接脚本里 IROM1 Start 填 0x08010000代码里设置 VECT_TAB_OFFSET0x10000量产烧录脚本里 bootloader 和 app 分开烧到对应地址。这套规划下来后续 OTA 升级只要按固定槽位写入就行不会乱。对 ESP32 类 SoC用分区表 CSV 做规划# Name, Type, SubType, Offset, Size nvs, data, nvs, 0x9000, 0x5000 otadata, data, ota, 0xe000, 0x2000 phy_init, data, phy, 0xf000, 0x1000 factory, app, factory, 0x10000, 0x200000如果项目要支持 OTA就把 factory 改为两个 ota 分区或者保持 factory 一个 ota 分区。所有分区的 Offset 加起来不能超过 Flash 总容量同一地址不能给两个分区用。设计好后把 CSV 纳入版本管理改地址时同步改代码里的宏和外部烧录脚本。一致性是地址管理最重要的原则。6. 我的实践心得与建议6.1 不要依赖记忆用工具和文件固化地址信息干了这么多年我发现很多地址相关的问题都是“拍脑袋填出来的”。你问同事为什么填 0x08010000他说看网上这么写的你问为什么 ESP32 的 app 是 0x10000他说大概是这样。这些答案经不起推敲。正确的做法是所有地址必须能溯源。STM32 的地址可以溯源到数据手册 Memory Map 和链接脚本ESP32 的地址可以溯源到分区表 CSV 和 bootloader 的编译配置。我建议团队的每个工程都放一个 README 或 flash_layout.md 文件把地址规划、烧录命令、注意事项写清楚。产品换人维护时这个文件比任何口口相传都可靠。我自己就有个习惯拿到一个陌生板子第一件事不是写代码是先读一遍 Memory Map 和启动配置把地址画成一张表贴在工作笔记里。6.2 使用 idf.py 自动生成 flash_args 避免手动填错ESP32 项目里最安全的方式永远是用idf.py flash。它会自动读取分区表、bootloader 编译结果和 sdkconfig 里的 flash 配置生成正确的烧录参数并执行。如果你必须用 esptool 手动烧录也建议从build/flash_args文件里复制参数而不是手敲esptool.py --chip esp32 --port /dev/ttyUSB0 --baud 460800 --before default_reset --after hard_reset write_flash --flash_mode dio --flash_freq 40m --flash_size detect 0x1000 build/bootloader/bootloader.bin 0x8000 build/partition_table/partition-table.bin 0x10000 build/app.bin这里的--before default_reset --after hard_reset是 esptool 控制芯片复位进入下载模式的参数--flash_mode dio和--flash_freq 40m指定 Flash 通信模式--flash_size detect让工具自动识别 Flash 容量。如果这些参数和 sdkconfig 里的配置不一致即使地址填对了也可能出现烧录后启动异常。最典型就是--flash_mode与分区表烧录时的模式不一致导致 bootloader 在运行阶段读 Flash 报错。6.3 逻辑分析仪和示波器才是地址问题的最终裁判软件层面查不出问题时别死磕配置上硬件工具看信号。比如 ESP32 烧录失败你可以用逻辑分析仪抓 SPI Flash 的 CLK、MOSI、MISO、CS 引脚看看烧录过程中有没有正常的 SPI 通信波形。如果芯片根本没进入下载模式SCK 上可能只有零星波形没有持续时钟。同理STM32 启动失败时可以用示波器抓 NRST 和供电判断是否有反复复位。我遇到过最诡异的一次问题STM32 烧录地址、代码、启动配置全对程序还是随机死机。最后用示波器抓到 3.3V 电源上有周期性的跌落恰好发生在 Flash 擦写瞬间是电源模块的负载调整率太差导致的。这种问题光看烧录地址和配置永远找不到答案必须从硬件信号入手。所以我常说地址问题多半是软件和配置问题但排查到最后一层一定是硬件问题工具链要备齐。6.4 踩过的三个印象最深的坑第一个坑量产烧录时用了“通用固件”和“参数固件”两个文件先后烧录时把顺序写反先写了参数后写了 bootloader结果参数覆盖了 bootloader 的前 4KB整批板子无法启动。从那以后我在产线烧录脚本里加了严格的地址校验每个文件烧完立刻读回比对并且把脚本参数做成固定模板不允许手工改动。第二个坑ESP32 的 OTA 升级时app 烧到了 app0 分区但 OTA data 分区里记录的是 app1bootloader 每次都尝试从 app1 启动结果老是回退到 factory 或直接起不来。排查了很久才发现是 OTA data 的内容被旧的测试代码写坏了。现在的做法是 OTA 测试前先擦除 otadata 分区保证干净的初始状态。第三个坑GD32 和 STM32 的烧录地址看起来一样都是 0x08000000但两者的 Flash 扇区大小和写保护机制不同用 ST-Link 默认配置烧 GD32 时偶尔出现校验失败。换了 DAP-Link 或者调整烧录算法里的扇区大小后正常。这提醒我即使引脚兼容的国产芯片也不能默认所有行为完全一致地址规划前还是得看对应芯片手册。6.5 最后一个小技巧烧录前先读再写不管用哪种工具我建议烧录前养成一个习惯先读一下目标地址区域的内容确认是空白或者可覆盖的旧数据再执行写入。用 STM32CubeProgrammer 可以读指定地址区域用 esptool 可以 read_flash过程只要几秒钟却能避免很多“覆盖重要数据”的惨案。另外如果条件允许给每个工程配一个烧录后自动校验的 CI 脚本。每次构建完自动执行一次烧录和读回校验把校验结果作为发布固件的前置门槛。这套流程看起来繁琐但能拦截掉大量因为地址配置错误、链接脚本漂移、分区表被改导致的问题。固件发布前多一道校验产线返工就少一片板子。
返回列表