烧录地址这个事,我前后踩过好几次坑才彻底捋顺。同一块板子,用 STC-ISP 下载程序,界面上那个地址框里填的是 0;换到 STM32,Keil 和 STM32CubeProgrammer 里满屏都是 0x08000000;再打开 ESP32 的编译日志,又蹦出 0x1000、0x8000、0x10000,有时候还会看到 0x6000 这种数字。三个地址长得毫无关系,可它们描述的是同一件事——这段数据要落到存储介质的哪一个位置。你要是只记结论,下次换个芯片照样懵;把地址背后的“语义”搞明白,以后不管换哪家平台,看一眼就能判断该填什么。
这篇内容我想按从业者的思路把这件事从头捋一遍:先讲清楚芯片里那套地址空间是怎么划的,再逐个拆解 0、0x08000000、0x6000 这些数字各自的来历,然后重点说说怎么看 ESP32 的烧录地址(这块问的人最多),最后给出改地址的实际操作和排查方法。不管你是刚上手单片机的新手,还是被 IAP、OTA 折腾过的老手,应该都能从里面找到自己需要的部分。
1. 三个地址背后其实是三种“地址语义”
1.1 从一颗芯片的存储地图说起
任何一颗 MCU,内部都有一张固定的“地址地图”。CPU 执行指令时,不管你是读代码、读变量还是读外设寄存器,走的都是同一套地址总线。芯片厂商在设计的时候,会把这 4GB(32 位机)的地址空间切成若干段:哪一段对应片内 Flash、哪一段对应 SRAM、哪一段对应外设寄存器、哪一段是保留的,全都写死在手册里,改不了。这张地图就是你判断“地址该填什么”的第一手依据。
以最常见的 Cortex-M 为例,它的地址空间大致是这样切的:0x00000000 到 0x1FFFFFFF 是 Code 区,主要放代码和常量;0x20000000 到 0x3FFFFFFF 是 SRAM 区;0x40000000 到 0x5FFFFFFF 是外设区;再往上还有外部 RAM、外部设备等区域。你会发现,Flash 的物理位置被安排在了 0x08000000 这个“很靠前但又不是最前”的地方,这个安排不是随便定的,后面会细说。
STC 这类 8051 内核的芯片,地图就简单得多:程序存储器空间是独立的 64KB,从 0000H 一直排到 FFFFH,复位之后 CPU 从 0000H 取第一条指令。所以对于 8051 来说,代码的“老家”就是 0 号地址,用户程序下载进去也是从 0 开始排布,STC-ISP 让你填 0 是完全合理的。而 ESP32 用的是 Xtensa 内核,Flash 并不直接挂在 CPU 的地址总线上,中间还隔了一层 MMU 和 Cache,所以你在不同的工具里看到的地址,含义可能完全不一样。
提示:判断一个地址该不该填,先问自己一句——这个数字是“CPU 眼里的地址”,还是“Flash 芯片内部的偏移”?这两者混在一起,是绝大多数地址填错的根源。
1.2 物理地址、别名地址、偏移地址
把上面那张地图再抽象一层,你会发现嵌入式里出现的地址其实只有三类语义。
第一类是物理地址,也就是芯片手册上白纸黑字写的那个位置,比如 Cortex-M 的片内 Flash 从 0x08000000 开始、SRAM 从 0x20000000 开始。这个地址是唯一的、固定的,编译器链接的时候要以它为基准,烧录器写 Flash 的时候也要认它。
第二类是别名地址(也叫映射地址、重映射地址)。芯片为了兼容历史设计或者方便启动流程,会把某一段物理存储“映射”到另一段地址上,让同一份数据可以用两个地址访问到。Cortex-M 的 0x00000000 就是典型的别名——它本身没有存储实体,具体映射到哪块物理存储,由 BOOT 引脚决定。8051 的 0x0000 也带有这种性质:它是复位向量入口,用户代码被安排从那里开始,但实际存储可能在片内 Flash 的另一段。
第三类是偏移地址。很多烧录工具和产线脚本并不关心芯片的物理基址,它们只关心“从存储器的第 0 字节往后数多少”。ESP32 的 esptool 就是这样,它的地址空间是从 Flash 芯片的第 0 字节开始算的,你填 0x10000,意思是从 Flash 起始位置往后 64KB 处开始写。nRF52 系列也是这个思路,Nordic 的 SDK 里 app 的起始地址写作 0x26000、0x27000,本质是“从 Flash 开头偏移多少”,只不过因为 nRF52 的 Flash 物理基址恰好是 0x00000000,偏移和物理地址重合了而已。
这三类语义对应到实际场景里,就变成了:编译器链接脚本里用的是物理地址或偏移地址,IDE 的下载配置里填的是物理地址,烧录脚本里填的通常是偏移地址。你拿偏移地址去改链接脚本,或者拿物理地址去喂 esptool,结果必然出错。
1.3 为什么同一份代码换个工具地址就变了
搞清楚了语义,再看“同一份代码、换个工具地址就变”这个现象就很好解释了。工具链在编译阶段生成的是可执行文件,文件内部的符号地址是按照链接脚本里的物理地址排布的;而烧录工具在写入阶段,面对的是存储介质本身的地址空间,它不认 CPU 的物理地址,只认介质内部的偏移。
举个具体的例子。你用 STM32CubeIDE 编译一份工程,生成的 .elf 里面,中断向量表被放在 0x08000000。这时候你打开 STM32CubeProgrammer 去下载,它显示的下载起始地址也是 0x08000000,这两个是一致的,因为它们说的都是物理地址。但如果你把同一份固件拿去用某些通用编程器(比如脱机烧录器)写入,脚本里可能出现的是 0x00000000,因为那台编程器是在直接操作 Flash 芯片,它眼里 Flash 就是从 0 开始的。
ESP32 更能说明问题。编译出来的 app.bin,在 CPU 的地址空间里它被映射到 0x400D0000 附近(指令 Cache 映射区),但你在 esptool 里烧录时填的却是 0x10000。同一份数据,三个地址:CPU 访问用 0x400D0000,链接脚本里用 0x400D0000(或者 IRAM 的某个值),烧录用 0x10000。这三个数都对,因为它们是三套不同的坐标系。
注意:看到别人给的地址先别急着抄,问清楚他是在哪一层说话。IAP 教程里给的 0x08006000、OTA 教程里给的 0x10000,语境不同,直接照搬到自己的工程里大概率跑不起来。
2. 逐个拆解:0、0x08000000、0x6000 是怎么来的
2.1 0x00000000:复位向量落在哪里,代码就从哪里算起
0 这个地址的来源最朴素——CPU 复位之后从哪里取第一条指令,代码就从哪里开始放。8051 架构复位后 PC 被清零,直接从 0000H 取值执行,所以用户程序的入口必须是 0,STC 的下载软件里让你填 0,顺理成章。老式的 PIC、AVR 也类似,复位向量都固定在地址 0 附近。
到了 Cortex-M 这里,情况稍微绕一点。芯片上电后,CPU 同样从 0x00000000 取向量表的前两个字:第一个字是初始 MSP 值,第二个字是复位处理函数的入口。但 0x00000000 本身不是存储实体,它是别名区。究竟读到哪里去,由 BOOT0/BOOT1 引脚(或者选项字节里的 BOOT 配置)决定:BOOT 配置为从主 Flash 启动时,0x00000000 被映射到 0x08000000;配置为从系统存储器启动时,映射到 0x1FFF0000 那段出厂固件;配置为从 SRAM 启动时,映射到 0x20000000。
这解释了一个很常见的困惑:为什么手册里说“从 0 开始启动”,可 Keil 里的 IROM1 却要填 0x08000000?因为“从 0 开始启动”说的是别名、是启动行为,而 Keil 里填的是链接地址、是物理实体。两份资料讲的是同一件事的两个侧面,都对。
还有一种情况是开发者主动把固件“搬”到了 0 地址去跑。比如某些带 Bootloader 的方案,用户程序被链接到 0x00000000 起始的位置,启动时由 Bootloader 把 Flash 里的内容复制到 RAM,再把 0x00000000 重映射到 RAM 区,然后跳转执行。这种做法在需要高速运行或者需要原地升级的场景里很常见,地址写 0 也是对的,只是背后的机制和直接跑 Flash 完全不是一回事。
2.2 0x08000000:Cortex-M 的 Flash 基址与它的别名区
0x08000000 这个数字,是 ARM 在定义 Cortex-M 地址空间时定下来的。Code 区从 0x00000000 到 0x1FFFFFFF,共 512MB,ARM 把它进一步切分:0x00000000 到 0x07FFFFFF 划给“别名区”,0x08000000 往后才是真正的代码存储区。芯片厂商把片内 Flash 放在 0x08000000 起始的位置,用户代码、常量、中断向量表都往这里放,链接脚本里的 FLASH ORIGIN 就写它。
为什么 ARM 要特意留出前面那 128MB 当别名区?核心目的是让启动流程更灵活。有了别名机制,同一份固件可以被映射到 0 地址运行,也可以用物理地址访问,Bootloader 和 App 之间的跳转、调试器读取内存、不同启动模式之间的切换都变得简单。这是 ARM 给整个生态留的一个设计接口。
在实际工程里,0x08000000 出现的位置非常固定:Keil 的 Options for Target → Target 页面里 IROM1 的 Start;STM32CubeIDE 里 .ld 文件的 MEMORY 段;STM32CubeProgrammer 的下载起始地址;还有反汇编窗口里一堆函数地址的前缀。你看到这一串数字,基本可以确定它指的是 Cortex-M 片内 Flash 的物理起始位置。
值得单独提一句的是 Flash 的“大小”和“起始地址”是绑在一起的。比如 STM32F103C8T6 的 Flash 是 64KB,那么地址范围就是 0x08000000 到 0x0800FFFF。你在链接脚本里写 LENGTH = 64K,编译器就会在这一段里分配空间,超了直接报 overflow。IAP 方案里把 App 的起始改成 0x08006000,LENGTH 就必须相应减去前面的 24KB,否则编译通过、运行翻车。
2.3 0x6000:一个“偏移量”,而不是一栋“门牌号”
0x6000 这个数字最容易让人困惑,因为它不带任何“基址”信息,裸一个。它的语义几乎永远是“偏移”,具体从哪里偏移,取决于上下文。
最常见的场景是IAP 升级里的 App 起始偏移。假设 STM32 的 Bootloader 功能占用了 Flash 前 24KB,0x6000 正好等于 24576 字节,那么 App 就应该从 0x08000000 + 0x6000 = 0x08006000 开始。你在链接脚本里写 ORIGIN = 0x08006000,在跳转代码里写 SCB->VTOR = 0x08006000,在烧录脚本里可能就简写成 0x6000。同一个位置,三种写法,说的是一回事。
第二种场景是ESP32 这类工具链的 Flash 偏移。esptool 的地址空间从 Flash 第 0 字节开始算,分区表里、烧录命令里写的都是偏移。0x6000 在 ESP32 的分区表里出现得非常频繁——它是 nvs 分区的默认大小(24KB)。注意这里 0x6000 是“长度”不是“地址”,这是它最容易让人栽跟头的地方。
第三种场景是某些芯片的 Bootloader 固定占用区。Nordic 的 nRF52832 里,SoftDevice 占住 0x0 到约 0x26000,App 从 0x26000 开始;nRF52840 的 App 从 0x27000 开始。虽然数字不是 0x6000,但道理完全一样:这个数字是“Bootloader 占了多少空间”算出来的偏移,不是芯片规定的固定值。
所以看到 0x6000,你得先判断它是长度还是位置。判断方法很简单——看单位、看上下文。分区表里排在 Offset 列后面、和 Size 列并列出现的,是长度;出现在烧录命令write_flash 0x6000 xxx.bin里的,是烧录起始位置;出现在链接脚本 ORIGIN 里的,是物理地址。
提示:我自己的习惯是,凡是在工程里写这种裸十六进制数字,后面一定跟一行注释标注来源,比如
0x6000 /* bootloader 占用 24KB, 0x08000000 + 0x6000 */。半年后再看,能省下半小时排查时间。
2.4 三种地址语义的对照表
| 地址形态 | 语义 | 典型出现位置 | 常见平台举例 |
|---|---|---|---|
| 0x00000000 | 复位向量地址 / 别名地址 | 启动配置、Bootloader 跳转、手动重映射 | 8051、STC、Cortex-M 别名区 |
| 0x08000000 | 片内 Flash 物理起始地址 | 链接脚本、IDE 下载配置、反汇编 | STM32、GD32、多数 Cortex-M |
| 0x00000000 | Flash 物理起始地址 | 链接脚本、烧录脚本 | nRF52 系列 |
| 0x6000 / 0x10000 | Flash 内偏移量 | esptool 烧录命令、分区表 | ESP32、ESP8266 |
| 0x6000 | 分区长度 | 分区表 Size 列 | ESP32 的 nvs 分区 |
这张表里我特意把两行 0x00000000 分开列,就是想说明一件事:数字一样,语义可能完全不同。Cortex-M 的 0x00000000 是别名,nRF52 的 0x00000000 是实打实的 Flash 物理起点。你得结合芯片型号去判断,不能只看数字。
3. 怎么看 ESP32 的烧录地址
3.1 ESP32 的 Flash 布局长什么样
ESP32 的 Flash 布局是整块芯片里最值得单独讲的部分,因为它把“偏移”这套语义用到了极致。一份典型的固件烧进去,Flash 里会被切成好几块,每块有自己的起始偏移。
默认布局大致是这样:0x1000 放二级 Bootloader(second stage bootloader,也就是 bootloader.bin);0x8000 放分区表(partition-table.bin);0xe000 放 boot_app0.bin(OTA 数据初始化文件);0xf000 放 phy_init_data.bin(射频校准数据);0x10000 起是应用程序(app)。除此之外,分区表里还会定义 nvs(非易失存储)、otadata、spiffs 等数据分区,它们的偏移都在分区表文件里写明。
这里有两个特别容易踩的点。第一,0x1000 这个起始位置是 Xtensa 内核的 ESP32 才有的,ESP32-C3、ESP32-S3、ESP32-C6 这些 RISC-V 内核的芯片,Bootloader 偏移是 0x0,不是 0x1000。你把 C3 的固件按 Xtensa 的地址去烧,Bootloader 写错位置,串口一片乱码。第二,0x8000 到 0x10000 这中间塞了好几个小文件,如果你用合并 bin 的方式烧录,顺序和地址必须完全对齐,错一个字节都可能起不来。
3.2 从编译产物里把地址“挖”出来
想知道自己的工程到底往哪些地址烧了东西,最靠谱的办法是看编译输出,而不是凭记忆。ESP-IDF 工程在 build 目录下会生成一个 flash_args 文件,内容就是完整的烧录地址和文件名列表,格式类似这样:
--flash_mode dio --flash_freq 40m --flash_size 4MB 0x1000 bootloader/bootloader.bin 0x8000 partition_table/partition-table.bin 0xf000 phy_init_data/phy_init_data.bin 0x10000 myapp.bin这个文件是 esptool 直接读取的,你执行idf.py flash的时候,它就是把这份参数原样传给 esptool。所以想确认地址,打开它看一眼就知道了,比翻文档快得多。
Arduino-ESP32 环境稍微麻烦一点,它不生成 flash_args。办法是打开“文件 → 首选项 → 显示详细输出 → 编译”,然后看编译日志的末尾,那里会打印一条完整的 esptool 命令,形如:
esptool.py --chip esp32 --port COM3 --baud 921600 --before default_reset --after hard_reset write_flash -z --flash_mode dio --flash_freq 80m --flash_size detect 0xe000 boot_app0.bin 0x1000 bootloader_dio_80m.bin 0x10000 sketch.bin 0x8000 partitions.bin这条命令里的地址就是最终烧录地址,照抄就行。
还有一种方法是从分区表本身读。idf.py partition-table会打印当前分区表的内容,包括每个分区的名字、类型、子类型、偏移和大小。如果你用的是 PlatformIO,执行pio run -t partition也能达到类似效果。分区表二进制文件本身也能反解,用 IDF 自带的脚本:
python $IDF_PATH/components/partition_table/gen_esp32part.py build/partition_table/partition-table.bin输出会是一张 CSV 表格,Offset 列就是每个分区的烧录偏移,一目了然。
3.3 用 esptool 读取和校验实际烧录内容
编译产物看完了,还得确认板子里实际烧的是什么。esptool 提供了一组读操作,可以把你关心的区域读回来做比对。
读 Flash 容量和型号:
esptool.py --chip esp32 --port /dev/ttyUSB0 flash_id读分区表区域,验证板子里的分区表和本地文件是否一致:
esptool.py --chip esp32 --port /dev/ttyUSB0 read_flash 0x8000 0xC00 partition_table_read.bin读 app 区域的前 64 字节,看看向量表、镜像头是否正常:
esptool.py --chip esp32 --port /dev/ttyUSB0 read_flash 0x10000 0x40 app_head.bin读回来的文件用十六进制工具打开,如果是合法镜像,开头能看到类似 0xE9 的 magic 字节(ESP32 镜像的标识),紧接着是段数、SPI 模式、芯片型号等字段。如果读出来全是 0xFF,说明这个地址上根本没烧东西,或者烧到了别的位置。
这套读校验的方法在排查“固件烧了但起不来”类问题时特别管用。我遇到过好几次,编译日志显示烧录成功,但串口就是没输出,最后用 read_flash 一读发现 app 区域是空的——原因是有个自定义分区表把 factory 分区的偏移改到了别处,而 esptool 还在按老的 0x10000 烧。
3.4 不同型号 ESP 芯片的地址差异
ESP32 家族型号多,地址习惯也不一样,我把常见的几款整理了一下:
| 芯片 | 内核 | Bootloader 偏移 | 分区表偏移 | App 默认偏移 |
|---|---|---|---|---|
| ESP32 | Xtensa | 0x1000 | 0x8000 | 0x10000 |
| ESP32-S2 | Xtensa | 0x1000 | 0x8000 | 0x10000 |
| ESP32-S3 | Xtensa | 0x0 | 0x8000 | 0x10000 |
| ESP32-C3 | RISC-V | 0x0 | 0x8000 | 0x10000 |
| ESP32-C6 | RISC-V | 0x0 | 0x8000 | 0x10000 |
| ESP8266 | Xtensa | 0x0 | 无独立分区表 | 0x1000(非 OTA) |
这张表里有几个点值得强调。ESP32-S3 虽然是 Xtensa 内核,但 Bootloader 偏移是 0x0,和老的 ESP32 不一样,这点经常被忽略。ESP8266 更特殊,它没有 ESP32 那套分区表机制,非 OTA 模式下 app 直接从 0x1000 开始;如果要做 OTA,user1.bin 放在 0x1000,user2.bin 放在 0x101000,这些地址是 SDK 约定好的,不能随便改。
另外 Flash 大小也会影响布局。4MB 的模组和 8MB 的模组,分区表里的 spiffs 或 fatfs 分区偏移和大小完全不同。你在别人工程里抄来的分区表,跑到自己板子上很可能越界,越界的直接后果是烧录时报“写超出 Flash 范围”,或者运行时读数据读到乱码。
4. 地址写错会怎样:典型故障与排查实录
4.1 症状对照表
地址错了,表现出来的症状其实挺有规律。我按自己遇到过的情况整理了一张对照表,遇到问题时可以先对号入座。
| 症状 | 大概率原因 | 快速验证方法 |
|---|---|---|
| 串口完全无输出,复位也没反应 | Bootloader 或向量表烧错位置 | read_flash 读起始区看是否为空 |
| 串口打印乱码,但有规律 | Flash 模式或频率配置与地址不匹配 | 对比 flash_args 里的 flash_mode |
| 打印了启动日志但停在某一行 | 分区表偏移错误,找不到 app | 读 0x8000 解析分区表 |
| 程序能跑,但一进中断就死 | 中断向量表偏移没同步修改 | 检查 SCB->VTOR 设置 |
| OTA 升级后无法启动 | 分区表中 app 分区偏移配置错误 | 解析分区表,核对 ota_0 偏移 |
| 编译通过,烧录报错 | 链接地址超出 Flash 物理范围 | 核对 IROM 起始和长度之和 |
这张表里,“程序能跑但一进中断就死”这一条是我印象最深的。那会儿做 STM32 的 IAP,Bootloader 部分搞定了,App 也跳过去了,主循环打印正常,但只要一触发串口中断就直接卡死。查了一整天,最后发现是 App 的中断向量表还指向 0x08000000,而 App 实际被烧到了 0x08006000,中断一触发,CPU 跳到 Bootloader 的向量表里去执行了,当然死。
4.2 排查思路:从启动链路倒着走
地址类问题排查,我习惯按启动链路倒着走一遍,效率最高。
第一步,确认芯片复位后从哪里启动。查 BOOT 引脚或选项字节的配置,确认是主 Flash、系统存储器还是 SRAM。这一步能排除掉“根本不从 Flash 启动”这类低级错误。
第二步,确认第一段被执行的代码在不在预期位置。用调试器连上去,复位后暂停,看 PC 落在了哪里,反汇编窗口显示的地址是不是你预期的 Bootloader 入口。如果 PC 跑到了 0x1FFF 开头,那就是启动模式配错了。
第三步,确认向量表是否搬到了正确的地方。Cortex-M 里用SCB->VTOR寄存器控制向量表基址,Bootloader 跳转前和 App 的 main 函数开头都应该把它设对。这一条如果漏了,程序表现得像“重启就正常、进中断就崩”。
第四步,把存储内容读回来比对。不管是 STM32 用 STM32CubeProgrammer 的读取功能,还是 ESP32 用 esptool 的 read_flash,核心思路都是把芯片里的实际内容和编译产物做字节级比对。对不上就说明烧录地址错了。
提示:排查这类问题时,先把“地址链路”在纸上画出来——复位入口 → 向量表 → Bootloader 起始 → 跳转目标 → App 起始 → 中断向量表。每一环都标上实际的地址值,哪一环对不上,问题就在那一环。
4.3 几条用真金白银换来的避坑心得
第一条,链接脚本和烧录脚本是两个独立的配置源,必须手工保持同步。我见过太多工程,改了 .ld 文件里的 ORIGIN,却忘了改产线烧录脚本里的起始地址,结果实验室里跑得好好的板子,一上产线就成批起不来。解决方案是搞一个单一数据源,比如用 Makefile 变量或者 CMake 的 configure_file,让两处引用同一个值。
第二条,改 App 起始地址时,别忘了改向量表偏移和栈顶设置。STM32 工程里通常有这么几处要同步改:链接脚本的 FLASH ORIGIN、system_stm32xxx.c 里的 VECT_TAB_OFFSET、启动文件里的栈顶位置(如果用了分散加载)。少改一处,就是上面说的“进中断就死”。
第三条,ESP32 自定义分区表时,Size 和 Offset 的关系要算清楚。分区表允许把 Offset 留空让工具自动计算,但前提是各个分区大小明确、不重叠。手动填 Offset 的时候一定要确保“上一个分区的 Offset + Size ≤ 下一个分区的 Offset”,不然工具可能不报错,但运行时会踩内存。
第四条,不同批次的模组 Flash 容量可能不一样。采购为了省钱,同一款产品不同批次用了 4MB 和 8MB 两种模组,如果你的分区表写死了偏移和大小,遇到小容量模组就会烧录失败。稳妥做法是把分区表大小控制在最小容量以内,或者用--flash_size detect让工具自动识别。
5. 实操:自己动手改一次烧录地址
5.1 STM32 的 IAP 分区与链接脚本修改
假设我们做一个最简单的 STM32F103 IAP 方案:Bootloader 占前 24KB,也就是 0x6000;App 从 0x08006000 开始,可用的 Flash 是 64KB 减去 24KB,即 40KB。
先改链接脚本。GCC 工具链下的 .ld 文件这样写:
MEMORY { FLASH (rx) : ORIGIN = 0x08006000, LENGTH = 40K RAM (rwx) : ORIGIN = 0x20000000, LENGTH = 20K }如果你的工程是从 0x08000000 的模板改过来的,注意把 .isr_vector、.text、.rodata 这些段的归属确认一遍,确保它们都落在 FLASH 这个 region 里。
然后处理向量表。最直接的办法是在 App 的 main 函数开头手动设置:
int main(void) { SCB->VTOR = 0x08006000; __enable_irq(); /* 其余初始化 */ }如果用的是 ST 的 HAL 库,也可以改 system_stm32f1xx.c 里的 VECT_TAB_OFFSET 宏,效果一样,但要注意这个文件可能被 CubeMX 重新生成覆盖,改完最好在项目说明里记一笔。
Bootloader 那边要负责跳转。跳转前必须做几件事:关闭所有中断、关闭外设时钟、把 App 的栈顶地址读出来赋给 MSP、然后把 App 的复位入口地址赋给 PC。代码大概长这样:
typedef void (*pFunction)(void); pFunction jump_to_app; uint32_t app_msp = *(volatile uint32_t *)0x08006000; uint32_t app_entry = *(volatile uint32_t *)0x08006004; __set_MSP(app_msp); jump_to_app = (pFunction)app_entry; jump_to_app();烧录的时候,Bootloader 烧到 0x08000000,App 烧到 0x08006000,两个 bin 分开烧,不要合并。用 STM32CubeProgrammer 的话,在下载界面里手动指定起始地址即可。
5.2 ESP32 自定义分区表与偏移
ESP32 这边改地址主要是改分区表。在工程根目录建一个 partitions.csv,内容示例:
# Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x4000, otadata, data, ota, 0xd000, 0x2000, phy_init, data, phy, 0xf000, 0x1000, factory, app, factory, 0x10000, 1500K, storage, data, spiffs, , 500K,几点说明。nvs 放在 0x9000,是因为 0x8000 到 0x9000 那 4KB 要留给分区表本身;otadata 放在 0xd000,是两个 OTA 槽位的状态记录;factory 的 Offset 留空,让工具自动计算到 0x10000,前提是前面几块加起来正好到那里。
改完分区表,在工程配置里指定:
idf.py menuconfig # Partition Table -> Custom partition table CSV -> partitions.csv编译之后,build 目录下的 flash_args 会自动更新成新的地址,idf.py flash就直接按新布局烧录。如果要生成出厂合并固件,用 esptool 的 merge_bin:
esptool.py --chip esp32 merge_bin -o merged_factory.bin \ --flash_mode dio --flash_size 4MB \ 0x1000 build/bootloader/bootloader.bin \ 0x8000 build/partition_table/partition-table.bin \ 0x10000 build/myapp.bin生成的 merged_factory.bin 可以直接从 0x0 一次性烧进去,产线量产的时候省事。
5.3 合并 bin 与批量烧录的注意事项
合并 bin 看起来方便,但有几个坑得提前说清楚。
合并的时候,各段的地址顺序必须是从小到大排列,esptool 会按顺序写入并在中间填充 0xFF。如果你把地址顺序写反了,比如先写 0x10000 再写 0x1000,工具会报错或者生成一个错位的文件。另外,合并的 bin 里不会包含 0x0 到第一个段之间的内容,如果你把合并 bin 当作“从 0 开始的完整镜像”去烧,0x0 到 0x1000 那段会被写成 0xFF 或者干脆不写,具体行为取决于工具和参数。
产线批量烧录还有一点要注意:不同批次的 Flash 型号可能不同,写入速度、擦除时间会有差异。建议在烧录脚本里显式指定烧录频率和 Flash 模式,不要完全依赖自动检测。--flash_mode dio --flash_freq 40m --flash_size detect这组参数兼容性最好,虽然慢一点,但胜在稳定。
提示:产线脚本里加一段校验逻辑,烧完之后用 read_flash 读回 app 区域的前 1KB,和源文件做哈希比对。这一步能挡掉相当比例的“烧录成功但实际没写进去”的偶发故障。
6. 关于地址这件事,我个人的几点体会
做了这么多年嵌入式,我越来越觉得“地址”这个词被用得太随意了。同一个数字,有人指的是 CPU 访问地址,有人指的是 Flash 芯片内部偏移,有人指的是相对 Bootloader 的偏移,还有人指的是分区长度。大家嘴上说的都是“地址”,脑子里想的却是不同的东西,沟通成本就是这么来的。
我自己现在的习惯是,任何一处涉及地址的配置,都强制带上三样信息:基准是什么(Flash 起始还是 CPU 地址空间)、是绝对位置还是偏移、单位是字节还是 KB。写进注释里,也写进团队文档里。看起来很啰嗦,但省下的排查时间远远超过写注释的那几分钟。
另外想说一句,工具链版本升级时,地址相关的默认值可能会变。我遇到过某次 IDF 升级之后,默认分区表的 nvs 大小从 0x6000 变成了别的值,之前写死的烧录脚本直接失效。所以每次升级工具链,顺手确认一遍 flash_args 和分区表,这个动作花不了一分钟,能避免很多莫名其妙的“以前好好的,现在起不来”。
最后一个实操小技巧:如果你手头有一块跑得好好的板子,想知道它的地址布局到底长什么样,不用去翻源码,直接用 esptool 读分区表、用 STM32CubeProgrammer 的读取功能扫一遍 Flash,用十六进制工具找镜像头,比任何文档都直接。板子里实际跑着的东西,才是唯一不会骗你的参考。