开发板调通那一刻当然爽,但真正让一个嵌入式项目节奏失控的,往往不是代码编译不过,而是“把固件烧进去”这个人人以为没技术含量的动作。我们项目里用的 ESP32-S31 模组(内部代号 S31,兼容 S3 系列)在交付给现场同事和产线师傅时,就曾反复栽在同一个地方:手动输命令烧录,地址填错、COM 口选错、镜像文件版本混搭,任何一步歪了,烧录就失败,而且失败报错又冷又抽象,没接触过 esptool 的人根本无从下手。
所以我花了两天时间,把整个烧录流程固化成了一个真正意义上的 Windows 一键烧录包:里面放好了完整镜像、esptool 运行环境、写好了所有地址参数和数据边界检查逻辑,双击 bat 就能完成从连接确认到烧录校验的全过程。这篇文章就来拆解这个烧录包是怎么做的,重点讲三件事——完整镜像到底包含哪些文件、esptool 在 Windows 上怎么干净地跑起来、以及每一处 flash 烧录地址背后的“数据边界”到底指的是什么。
1. 为什么要给 S31 项目专门做一份 Windows 一键烧录包
1.1 手动烧录流程里隐藏着太多“你以为不会错”的环节
先复盘一下最原始的手动烧录场景。在开发机上,烧录固件通常就是几条命令:
esptool.py --chip esp32s3 -p COM5 -b 460800 --before default_reset --after hard_reset write_flash --flash_mode dio --flash_size 8MB --flash_freq 80m 0x0 bootloader.bin 0x8000 partition-table.bin 0x10000 app.bin这条命令看起来简单,但每一段都承担着一个独立的出错点:-p COM5要猜串口号,0x0、0x8000、0x10000这几个偏移地址必须和分区表严格一致,--flash_size如果和芯片容量不匹配,很可能烧进去了但启动不了。更隐蔽的是,bootloader.bin、partition-table.bin和app.bin必须来自同一次编译,否则 bootloader 和 app 之间的接口约定不一致,表现出来就是板子反复重启、串口刷日志。
对于开发者本人,这些约束都是肌肉记忆。但项目进入联调、试点、量产阶段后,烧录动作的执行人往往从写代码的人换成了现场工程师或者产线师傅。他们有另外一套操作习惯:不关心 esptool 的版本,不区分--flash_mode是 dio 还是 qio,只知道双击 exe 或 bat 就应该把活干完。你如果要他们手动执行这一长串命令,那基本等于在给项目埋雷。
1.2 一键烧录包的本质是把“操作规范”固化成一个可交付物
我把这次改造的目标定为:任何拿到烧录包的人,不需要安装额外的编程环境,不需要懂 flash 布局,只要电脑能识别到板子的串口,双击一下就能烧录成功。这背后隐藏的另一个好处是“可追溯”——烧录脚本里写死了镜像版本和地址参数,现场同事不会因为你口头说“这次固件在某某文件夹里”就拿错版本。
为了做到这一点,烧录包需要具备四个前提:
- 镜像文件齐全且版本一致;
- esptool 运行环境不依赖目标电脑的全局安装;
- 烧录地址由脚本保证,不允许人为输入;
- 失败时有清晰的提示,而不是抛出一串让人看不懂的 esptool 异常。
这四个前提分别对应文章接下来四个部分的内容:完整镜像、esptool 准备、数据边界逻辑、脚本实现。
2. 完整镜像的组成:bootloader、分区表、app 和 ota_data 的来龙去脉
2.1 一次编译后真正需要烧进去的文件有哪些
“完整镜像”这个词很多人理解得过于简单,以为就是项目生成的单个 app.bin。实际上,对于 ESP32-S31 这类芯片,一次完整烧录通常包含三到四个文件,它们各自承担不同职责:
| 文件 | 典型编译产物路径 | 作用 | 默认建议偏移 |
|---|---|---|---|
| bootloader.bin | build/bootloader/bootloader.bin | 芯片上电后最先执行的引导程序,负责初始化 flash 和加载分区表 | 0x0 |
| partition-table.bin | build/partition_table/partition-table.bin | 定义了 flash 上各个分区的边界:app、NVS、OTA、存储区等 | 0x8000 |
| app.bin | build/项目名.bin | 主应用程序固件,也就是你的业务代码本体 | 0x10000(单 app 时) |
| ota_data_initial.bin | build/ota_data_initial.bin | 仅在启用 OTA/双 app 分区时需要,用来标记当前启动的是哪个 app | 0xd000 |
让我逐一解释为什么每个文件都不可或缺。bootloader.bin是芯片启动链路的第一级,它从 flash 地址 0x0 开始执行,负责把分区表读取出来、验证 app 镜像、然后跳转。如果你只烧 app 不烧 bootloader,最常见的现象是板子完全没反应,或者反复打印类似“invalid header”的启动日志。partition-table.bin则是一份 flash 空间分配地图,它的偏移通常固定在 0x8000,bootloader 会固定从这个地址去读它。
ota_data_initial.bin容易被忽略,但对使用双 OTA 分区的项目特别关键。系统需要记录“当前应该从哪个 app 分区启动”这个状态,而这个状态存放在 ota_data 分区。首次烧录如果不烧初始状态文件,某些配置下会出现系统不知道启动哪个 app 的问题。如果你的项目没有启用 OTA,单 app 布局,那么可以不烧这个文件。
2.2 版本一致性:完整镜像的灵魂约束
比文件缺失更容易踩的坑是版本混搭。我知道很多开发者的习惯是:bootloader 用以前编译的,partition-table 也感觉“没改过”,只更新 app.bin。这在项目初期可能没事,但风险在于——分区表变了而 bootloader 没跟着变、bootloader 变了而 app 里依赖的启动参数没跟上,这类问题表现出的症状非常难排查,因为启动日志看起来像是正常的,却总在某些典型功能上诡异失效。
最稳妥的规则是:一键烧录包里的 bootloader、partition-table、app 必须来自同一个 build 目录,也就是同一次编译的产物。不要从不同日期、不同分支的编译目录里混着拿。我建议在把镜像文件放进烧录包之前,看一眼每个文件的生成时间戳,确保它们属于同一次构建。
2.3 分区表文件直接决定“数据边界”长什么样
分区表(partitions.csv)是完整镜像里最容易理解错的部分,它本质上是一张“flash 空间划分清单”。举个例子,一份常见的 8MB flash 双 OTA 分区表如下:
# Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x5000, otadata, data, ota, 0xe000, 0x2000, app0, app, ota_0, 0x10000, 0x180000, app1, app, ota_1, 0x190000, 0x180000, storage, data, fat, 0x310000, 0x500000,这张表就是你烧录时的“数据边界宪法”——每个 bin 的烧录地址和长度必须落在这张表划定的范围内,不能越界。后面第四章我会再展开讲这个“边界”的本质。
2.4 准备好一份“可交付”的镜像目录
为了保证现场不会拿错文件,我会在烧录包里强制规定一个目录结构:
s31-flash-package/ ├── flash_all.bat ├── README.txt ├── esptool/ │ └── esptool.exe └── firmware/ ├── bootloader.bin ├── partition-table.bin ├── app.bin └── ota_data_initial.bin一个目录对应一个版本,目录名里加上日期和版本号,比如s31-flash-v1.4.0-20250601。这样现场同事拿到压缩包解压出来的东西,天然就是一份自洽的完整镜像。
3. Windows 下 esptool 的两种准备方式:干净环境优先与独立程序兜底
3.1 方案对比:Python 环境与独立可执行文件
Windows 下使用 esptool,务实路线只有两条。第一条是装 Python,然后用pip安装 esptool,日常通过esptool命令操作。第二条是直接拿 esptool 的独立 Windows 可执行文件(esptool.exe),放在烧录包目录里,完全不依赖目标电脑的 Python。
我强烈建议一键烧录包采用第二条路线。原因很简单:你不能要求现场工程师的电脑里有一个干净的 Python 环境。有时候他们机器上装了各种软件,Python 版本混乱、PATH 被改过、pip指向了某个奇怪的虚拟环境——这些都会让你的烧录脚本死得不明不白。独立 exe 虽然体积大了一些,但换来的是“拿到就能跑”的确定性。
如果你还是倾向于 Python 路线,至少要在脚本里做好兼容处理。比如先探测esptool命令是否存在,不存在就尝试python -m esptool,再不行就尝试py -m esptool。Windows 上python和py有时指向不同的解释器,这是很常见的坑。
3.2 驱动这一关过不去,一切白搭
在 Windows 上跑 esptool,还有一个物理层前提:系统必须能识别到板子的 USB 转串口设备。S31 开发板大多数方案都外挂了 USB 转串口桥芯片,但不同批次、不同厂家的板子用的桥芯片可能不一样,对应驱动也不同。常见的情况是:设备管理器里能看到一个带黄色感叹号的“未知设备”,或者根本没有任何新设备出现。
排查这类问题,方法可以很简单:打开设备管理器,拔插一次 USB 线,看“端口 (COM 和 LPT)”下有没有多出一个 COM 口。没有的话,问题基本锁定在驱动,而不是 esptool 本身。装好桥驱动之后,再确认串口号,别凭感觉猜 COM5 还是 COM8。
3.3 验证 esptool 是否可用:先跑版本命令
不管用哪种方式准备 esptool,最好在打包之前做一次最小验证。在命令行里执行:
esptool version对于独立 exe,则是:
esptool.exe version正常情况下会输出类似esptool v4.x的信息。如果连版本都打印不出来,那脚本写得再好也白搭。另外提醒一句,esptool 的不同大版本之间,命令行参数有过变化,比如--chip esp32s3这个参数在旧版本里可能不支持或者行为不同。所以烧录包里一定要固定一个版本的 esptool,不要让它去调用系统里可能存在的其他版本。
4. 烧录地址背后的数据边界:为什么每个 bin 都站在指定的偏移上
4.1 flash 不是磁盘:擦写粒度和寻址边界决定一切
很多刚接触固件烧录的开发者会把 flash 想象成硬盘,以为随便找个地址写进去就行。实际情况完全不是这样。ESP32-S31 外接的 SPI flash,最小的可擦除单位是一个扇区(通常 4KB),而 flash 控制器读写的时候还涉及缓存映射的对齐要求。更关键的是,芯片上电后,CPU 的指令总线是直接从 flash 映射过来的,这就要求 app 镜像在 flash 里的位置必须是某些边界对齐的。
这不是纯理论问题。如果 app.bin 被烧到一个未对齐的地址,轻则启动异常,重则直接 panic。所以你在 esptool 命令里看到的0x10000这类偏移,不是随便定的整数,而是芯片架构和分区表共同约束下的合法位置。
4.2 一张表看懂典型地址布局和边界含义
以 8MB flash 的单 app 布局为例,烧录地址分布通常是这样的:
| 偏移地址 | 扇区边界 | 内容 | 边界含义 |
|---|---|---|---|
| 0x0000 | 第 0 扇区 | bootloader | flash 起始,必须从这里开始 |
| 0x8000 | 第 8 扇区 | partition-table | 偏移是 4KB 的整数倍,bootloader 固定地址读取 |
| 0xd000 | 第 13 扇区 | ota_data(可选) | 记录当前 OTA 状态 |
| 0x10000 | 第 16 扇区 | app 主分区 | 缓存映射要求 app 起始偏移对齐到 64KB 边界常见做法 |
| ... | ... | 其他分区 | 严格按分区表排列 |
如果使用双 OTA 布局,一般会有两个 app 分区(ota_0 和 ota_1),它们的大小必须完全一致,地址不重叠。这背后的原因是:OTA 升级的原子性依赖于“随时知道哪个分区是可启动的”,如果两个分区大小不一致,升级过程无法保证安全切换。
4.3 esptool 并不知道你的分区表:数据边界是把双刃剑
这里必须点破一个关键事实:esptool 在write_flash的时候,并不会主动去读你的分区表,它只按你给的地址和文件长度去写。也就是说,你用 esptool 写0x8000 partition-table.bin,它不会帮你检查这个偏移是不是分区表该在的位置;你如果把 app.bin 的地址写成了0x8000,esptool 照样会执行,然后你的分区表就被覆盖了。
“数据边界”这四个字,暴露的正是这个风险面:esptool 负责按地址写数据,但谁负责保证地址正确?答案是你,是你的脚本,是你的烧录包。
所以在设计一键烧录脚本时,我主动把“边界安全”做了进去。具体手段包括:脚本内置地址常量,不允许人为输入;执行前检查每个 bin 文件的大小是否超出预期分区范围;对--flash_size参数做硬编码,避免与板载 flash 容量不一致。这些检查在手动命令模式下都是形同虚设的,但在封装好的烧录包里就是必须有的防线。
4.4 一个容易忽略的边界:镜像长度不能越过分区尾部
还有一种边界问题更隐蔽——镜像文件本身不能超过它所属分区的大小。比如分区表里给 app0 划分了 0x180000(1.5MB)空间,但编译出的 app.bin 有 1.6MB,这时候即使烧录地址 0x10000 正确,镜像的后半段也会溢出到下一个分区。在量产阶段,这种问题尤其危险,因为你可能升级了功能代码导致固件变大,却没同步调整分区表。
对一键烧录包来说,最实用的做法是让脚本在烧录前用for %%A in (app.bin)读取文件大小,然后和分区极限值做比较,超了就拒绝烧录并提示。如果你愿意,还可以用esptool image_info读取 app 镜像内部信息来校验它是否与分区表匹配,但这需要额外写一段逻辑,我在后面第七章会展开。
5. 一键脚本的完整实现:从端口探测到结果回显
5.1 完整 bat 脚本
下面是我在 S31 项目里实际用到的一键烧录脚本,去掉了和具体项目强相关的名称,保留了完整的骨架逻辑。
@echo off setlocal EnableDelayedExpansion chcp 65001 >nul cd /d %~dp0 REM ========== 配置区 ========== set "ESPPORT=" set "FLASH_BAUD=460800" set "FLASH_MODE=dio" set "FLASH_FREQ=80m" set "FLASH_SIZE=8MB" set "CHIP=esp32s3" set "LOG_FILE=flash_log.txt" REM ========== 查找 esptool ========== set "ESPTOOL=%~dp0esptool\esptool.exe" if not exist "%ESPTOOL%" ( where esptool >nul 2>nul if not errorlevel 1 ( set "ESPTOOL=esptool" ) else ( python -m esptool version >nul 2>nul if not errorlevel 1 ( set "ESPTOOL=python -m esptool" ) else ( echo [错误] 找不到 esptool,请确认 esptool.exe 已放在 esptool 目录下。 pause exit /b 1 ) ) ) echo 使用的烧录工具: %ESPTOOL% %ESPTOOL% version >nul 2>nul if errorlevel 1 ( echo [错误] esptool 运行异常,请重新获取烧录包。 pause exit /b 1 ) REM ========== 检查完整镜像文件 ========== set "FW_DIR=%~dp0firmware" set "FW_FILES=bootloader.bin partition-table.bin app.bin ota_data_initial.bin" for %%F in (%FW_FILES%) do ( if not exist "%FW_DIR%\%%F" ( echo [错误] 缺少镜像文件: %%F set "MISSING=1" ) ) if defined MISSING ( echo 请确认烧录包完整后重试。 pause exit /b 1 ) REM ========== 选择串口 ========== if "%ESPPORT%"=="" ( echo 当前可用的串口: mode | findstr /r "COM[0-9]*" set /p ESPPORT=请输入正确的 COM 口号 ^(例如 COM5^): ) echo 使用串口: %ESPPORT% REM ========== 执行烧录 ========== echo 开始烧录,请勿拔掉 USB 线... ( %ESPTOOL% --chip %CHIP% -p %ESPPORT% -b %FLASH_BAUD% --before default_reset --after hard_reset write_flash --flash_mode %FLASH_MODE% --flash_freq %FLASH_FREQ% --flash_size %FLASH_SIZE% 0x0 "%FW_DIR%\bootloader.bin" 0x8000 "%FW_DIR%\partition-table.bin" 0xd000 "%FW_DIR%\ota_data_initial.bin" 0x10000 "%FW_DIR%\app.bin" ) > "%LOG_FILE%" 2>&1 set "FLASH_RC=%ERRORLEVEL%" if %FLASH_RC% equ 0 ( echo. echo 烧录成功!板子应已自动重启。 ) else ( echo. echo [错误] 烧录失败,详细日志见 %LOG_FILE%。 type "%LOG_FILE%" ) echo. pause5.2 脚本里的每个关键判断,为什么必须这样写
先说目录切换。开头的cd /d %~dp0确保脚本无论从哪个路径被双击,工作目录都切到脚本所在目录。这一点特别重要,因为现场同事很可能把烧录包解压到了带有中文或空格的路径下,如果脚本里用相对路径而不先切目录,镜像文件很可能找不到。
然后是查找 esptool 的顺序:优先使用包内自带的esptool.exe,其次才是系统命令和 Python 模块。这是我在前面第三章说的“独立程序优先”原则的代码化。脚本里对每个候选都做了版本探测,不是找到就盲目用,因为一个无法运行的 esptool 比没有 esptool 更耽误时间。
镜像完整性检查用了for循环,把bootloader.bin、partition-table.bin、app.bin、ota_data_initial.bin四个文件一一检查。有读者可能问:为什么不加 app.bin 的大小检查?因为脚本里目前没有内置分区大小常量,如果你要加,可以在配置区加一个set "APP_MAX_SIZE=1572864"之类的值,再用for %%A in ("%FW_DIR%\app.bin")读取字节数做比较。这一步在生产环境极为有用,我在第七章会补充。
选串口的部分,mode命令列表系统里的串口,然后让用户输入。这比在脚本里写死一个 COM 口要务实得多,因为不同电脑、不同 USB 接口下,板子分配到的串口号完全不一样。当然,如果你有固定的一台烧录台,也可以在配置区把ESPPORT写成固定的。
5.3 为什么要用日志重定向和错误码判断
注意我把整个 esptool 命令的输出重定向到了flash_log.txt。这不是为了省屏幕空间,而是为了让失败的时候有一个持久化的、可以回传的日志文件。esptool 的输出信息量很大,包括连接超时、镜像校验失败、flash 加密状态等,现场工程师看到那一屏英文报错往往会一头雾水,但把flash_log.txt发回给开发端,问题定位就会快很多。
同时,我用%ERRORLEVEL%判断烧录是否成功,然后分别给用户明确的“成功/失败”提示。这里的细节在于我把 esptool 命令放在括号里执行,并把错误码及时存入FLASH_RC变量——因为if errorlevel是即时判断,如果不先存起来,后面执行echo或者其他命令时,ERRORLEVEL可能已经被改变了。
6. 烧录失败时的报错语义与排查顺序
6.1 最常见的几条报错到底在说什么
无论脚本写得多完善,现场总会遇到硬问题。esptool 的报错格式比较固定,我这里把最常遇到的四条摘出来,配上它们的真实含义和排查动作:
| 报错特征 | 实际含义 | 优先排查方向 |
|---|---|---|
Failed to connect to ESP32-S3: No serial data received | esptool 没有收到芯片的下载模式握手信号 | 板子没进入下载模式,或 TX/RX 接错,或串口选错 |
A fatal error occurred: Timed out waiting for packet header | 握手之后数据包超时 | 波特率过高、USB 线质量差、供电不足 |
A fatal error occurred: Packet content transfer stopped | 传输中途中断 | 接触不良、供电波动、宿主机 USB 节能策略 |
File ... exceeds flash size或is not within the partition | 地址/长度越过 flash 边界 | 镜像大小或烧录地址配置错误 |
第一行报错出现频率最高。它通常不是环境问题,而是操作时序问题:ESP32 系列芯片默认从 flash 启动,只有在特定条件下才会进入下载模式。进入下载模式的标准操作是:按住 BOOT 键(对应 IO0),保持按住,按一下 EN 键复位,然后松开 BOOT。如果板子上电直接跑固件,esptool 就收不到握手信号。
6.2 一个真实踩过的坑:COM 口选对了,但线的问题让一切归零
有一次现场反馈烧录失败,日志里反复出现Timed out waiting for packet header。我远程看日志以为又是下载模式问题,让同事反复按 BOOT 键都没用。后来发现,他用的是那根只能充电不能传数据的 micro USB 线——这种线能供电但通讯线根本没接。换了一根数据线后,一次通过。
这个教训让我在后续烧录包的 README 里专门加了一条:请在设备管理器里确认板子真的枚举出了 COM 口,再用这根线连接。如果设备管理器里能看到 COM 口,但烧录仍然超时,则进入下一步排查波特率。
6.3 为什么降低波特率是现场自救的第一招
esptool 默认波特率 460800,对大多数板子和原装数据线来说没问题,但在线比较长、接触电阻偏大或者板子电源纹波比较大的情况下,高速串口容易出时序问题。现场一手排查手法是:把波特率降到 230400 甚至 115200 再烧。低速烧录只是慢一点,成功率却高得多。
我会在脚本的配置区把FLASH_BAUD留成可调变量,README 里也写明:遇到超时先降波特率,别急着怀疑板子坏了。这个建议听着简单,但在现场能省掉大量无效沟通。
6.4 边界类报错的本质:你的地址和文件长度“破坏了约定”
最后说这类最容易被忽视的报错。esptool 在烧录前确实会对你指定的文件大小和 flash 容量做一次简单比对,比如你让它往 0x10000 写一个 10MB 的文件,而--flash_size是 8MB,它就会报exceeds flash size。
但前面说过,esptool 不会检查你的分区表。真正遭遇到 app 固件编译膨胀、镜像越过分区尾部的场景时,报错往往不是荒谬的“exceeds flash size”,而是烧录成功但板子启动异常或滚码反复重启。所以边界问题的排查要前置:用分区表对照实际 bin 大小,把检查写进一键脚本和版本发布流程里。这让“数据边界”从一次性的关键词变成了真正能在烧录包里落地的防御机制。
7. 面向批量生产的加固:合并镜像、校验与刷后验证
7.1 用 merge_bin 把多个镜像合并成一个连续文件
一键烧录做到这个程度,个人开发和小批量试点已经完全够用。但到了产线或大批量交付阶段,多文件烧录的隐患开始暴露:四个 bin 文件四套地址,任何一个文件缺失、被误改或拷贝错了,现场根本发现不了。这时候的解法,是用esptool --merge_bin把分散的镜像合并成一个从 0x0 开始的单一文件。
esptool --chip esp32s3 merge_bin --flash_mode dio --flash_freq 80m --flash_size 8MB --output merged.bin 0x0 bootloader.bin 0x8000 partition-table.bin 0xd000 ota_data_initial.bin 0x10000 app.bin合并后的merged.bin可以直接从偏移 0x0 烧录,烧录命令只剩一条:
esptool --chip esp32s3 -p COM5 write_flash 0x0 merged.bin这样做有两点好处:第一,烧录地址边界在“合并”这一步就已经固化好了,产线上的烧录命令变得极其简单;第二,整个仓库里只有一个固件文件,版本管理更干净。产线师傅也不用理解什么叫分区表。
但要提醒一句:合并镜像的烧录速度通常不会变快,因为写入的数据总量是一样的。它的价值在于减少操作变量和降低出错概率,而不是缩短时间。
7.2 烧录后的自我验证:不等重启,直接查证据
很多一键烧录脚本止步于“命令执行完成”。但 esptool 默认会在write_flash之后进行校验,只是这个信息被淹没在日志里,现场不一定注意得到。我会在脚本里额外做两件事:一是让脚本明确打出“校验通过”字样,二是提示操作人员观察板子的串口输出,确认 app 版本号符合预期。
如果你希望更严谨,也可以在烧录完成后,用esptool verify_flash做一次显式校验:
esptool --chip esp32s3 -p COM5 verify_flash 0x0 bootloader.bin 0x8000 partition-table.bin 0x10000 app.bin校验通过只代表写入的数据和本地文件一致,不代表固件逻辑正确。但至少可以排除“烧录过程中数据丢包”这一大类问题。对量产而言,这已经是一道必须有的闸门。
7.3 NVS 初始化数据和 flash 加密的特殊边界
如果你的产品依赖出厂预置数据——比如 WiFi 配网信息、设备证书、校准值——那完整镜像里还需要包含一个 NVS 初始化分区。这个分区一般放在nvs分区对应的偏移,用nvs_partition_gen生成出来,烧录地址由分区表决定。在量产场景下,NVS 数据经常是和 app 一起打的,否则每台设备出厂后都要人工进入配网模式,非常痛苦。
如果项目启用了 flash 加密和安全启动,烧录边界会变得更复杂。比如加密后的镜像不能再按普通明文方式从任意地址合并烧录,bootloader 的地址和格式也会有额外要求。对于要量产的项目,我建议在开发早期就把这套流程跑通,最晚不要拖到产线试产阶段才来验证--flash_encrypt相关命令。
7.4 给烧录包配一份“给非技术人员看”的说明文档
这也是整个烧录包里容易被忽略但极其重要的一部分:README.txt。不要把 README 写成给工程师看的参数手册,要写成给现场操作工看的一页纸说明。内容只需要三块:
- 什么情况下需要烧录;
- 双击哪个文件、按什么顺序操作;
- 成功了屏幕上显示什么、失败了要回传哪个文件。
我会在 README 里明确写:烧录过程中不要拔插 USB 线,不要重启电脑;如果报错,把 flash_log.txt 发回给技术负责人。这两个简单的防疫式约定,比整个脚本代码加起来都更能保障批量交付的顺畅。以后每次发新版固件,我只要把新的镜像放进固定的目录结构、改一下版本号,整个交付过程就能稳定复用。
回到开头说的那个场景——项目的推进本来就不该卡在烧录这种环节。把完整镜像、esptool 和数据边界这三件事想清楚,做成一个谁都能双击的 Windows 一键烧录包,看起来只是多写了一个 bat 文件,实际上是把整套烧录质量交付了出去。我个人的体会是,这个包不仅是给现场同事用的,也是给三个月后的自己用的。等到你手头同时在维护好几个固件版本、而客户现场正有人等着板子恢复时,你会庆幸当初在脚本里多写的那几行检查和那份 README。