☰
ESP32-S31 一键烧录包制作:镜像合并、esptool 独立打包与 bat 脚本实战
2026/10/21 1:33:25 网站建设 项目流程

1. 项目缘起与整体设计思路

做过 ESP32 系列开发的人都有一个共同体会:在实验室里烧录固件从来不是问题,真正让人头疼的是把固件交到别人手上。你不可能要求产线工人、测试同事或者客户先装一遍 Python、再配一遍环境变量、再记住那一长串 esptool 命令。所以“一键烧录包”这个东西,本质上解决的是交付最后一公里的问题——把完整的镜像、烧录工具、参数配置和批处理脚本打包成一个文件夹,对方双击一个 bat 文件就能把固件写进芯片。

这次要聊的是 ESP32-S31 平台上的完整方案。S31 这个型号在引脚布局和 Flash 配置上有自己的特点,不能直接照搬 S3 或者 C3 的烧录参数,尤其是 Flash 大小和分区表偏移这两项,搞错了就是烧进去不启动,串口一直打印乱码或者直接复位循环。我前后做过三四个量产项目的烧录包,踩过的坑基本都集中在“数据边界”这四个字上——镜像从哪个地址开始、写到哪个地址结束、参数区留多少、NVS 分区怎么算,这些边界一旦对不上,烧录工具不会报错,但设备就是跑不起来。

整套方案的核心思路可以拆成三层。第一层是镜像层,也就是 bootloader、分区表、应用程序、文件系统这几段二进制怎么合并成一个或者多个 bin 文件;第二层是工具层,esptool 怎么以独立可执行文件的形式打包进去,不依赖系统 Python;第三层是脚本层,用 bat 批处理把串口号检测、波特率设置、烧录地址映射、校验和重试这些逻辑串起来。三层各司其职,任何一层偷懒都会导致最终交付的包在别人电脑上翻车。

为什么不用图形化烧录工具而坚持用命令行加脚本?因为图形工具的参数是写死在界面里的,换一个 Flash 大小就要重新点一遍,而且没法做自动化。批处理脚本虽然看起来土,但它可版本管理、可参数化、可嵌入到更大的产线流程里。实测下来,一个写好的 bat 脚本配合独立 esptool,在 Windows 7 到 Windows 11 上都能跑,兼容性比想象中好得多。

注意:ESP32-S31 的 Flash 电压和 S3 不同,烧录时如果用了错误的 flash 模式参数,可能出现烧录成功但启动失败的情况,这个后面会专门讲。

适合读这篇内容的人有三类:一是第一次做量产交付的嵌入式开发者,二是需要给非技术同事提供烧录工具的测试负责人,三是想把自己项目打包成“绿色版”分发出去的个人开发者。不管你用的是 Arduino 框架还是 ESP-IDF,底层烧录逻辑是相通的,区别只在于镜像文件的生成方式。

2. 镜像构成与数据边界详解

2.1 ESP32-S31 的 Flash 布局到底长什么样

要搞清楚烧录包怎么做,先得把芯片内部的 Flash 地址空间理清楚。ESP32-S31 通常搭配 4MB 或 8MB 的 SPI Flash,默认的分区布局大致是这样的:0x0000 开始是 bootloader,紧接着 0x8000 是分区表,0x10000 开始是主应用程序,后面跟着 NVS、OTA 数据、SPIFFS 或者 FATFS 文件系统。这个顺序不是随便定的,是芯片上电后 ROM bootloader 的固定读取逻辑决定的。

bootloader 必须放在 0x1000 或者 0x0000,具体取决于芯片型号和 Flash 模式。S31 在 QIO 模式下 bootloader 起始地址是 0x1000,但如果你用的是 DIO 模式,有些配置会把它放到 0x0000。这个细节在官方文档里写得很隐蔽,我当初就是在这里卡了半天——烧录工具显示成功,串口却一直输出“invalid header”,后来用 esptool 的 read_flash 把芯片里的内容读出来对比,才发现 bootloader 写错了偏移。

分区表固定在 0x8000,长度一般是 0x1000,也就是 4KB。这 4KB 里定义了每个分区的名称、类型、子类型、偏移地址和大小。应用程序的偏移通常是 0x10000,但如果你用了自定义分区表,这个值可能变成 0x20000 甚至更大。烧录包里的地址参数必须和分区表里写的完全一致,否则芯片启动时按分区表去找应用,找到的却是一片空白,结果就是不断复位。

NVS 分区一般从应用后面开始,大小 0x6000 到 0x9000 不等。这里存的是 WiFi 配置、用户参数这些掉电不丢的数据。做烧录包的时候要考虑一个边界问题:如果你烧的是“出厂固件”,NVS 应该是空的;如果你烧的是“升级固件”,NVS 必须保留。这两种场景的烧录地址范围完全不同,不能混用一个脚本。

2.2 镜像合并:什么时候该合,什么时候不该合

esptool 提供了一个 merge_bin 功能,可以把 bootloader、分区表、应用、文件系统合并成一个大的 bin 文件。合并的好处是烧录时只需要指定一个起始地址 0x0000,一条命令写完,脚本简单不容易出错。坏处是文件体积大,而且如果只想更新应用部分,还得重新烧整个镜像,浪费时间。

我的经验是:产线首次烧录用合并镜像,现场升级用分段烧录。首次烧录时芯片是空的,从 0x0000 开始写整个镜像最稳妥,不用担心残留数据。现场升级时只烧应用分区,地址从 0x10000 开始,NVS 和文件系统不动,这样用户配置不会丢。

合并镜像的命令大概是这样:

esptool.py --chip esp32s31 merge_bin -o factory_full.bin \ --flash_mode qio --flash_size 4MB \ 0x1000 bootloader.bin \ 0x8000 partition-table.bin \ 0x10000 app.bin \ 0x210000 spiffs.bin

这里每个地址都不是随便写的。0x1000 是 bootloader 的边界,0x8000 是分区表的边界,0x10000 是应用的边界,0x210000 是文件系统的边界。这些值必须和分区表 CSV 文件里定义的完全对应。我见过有人把文件系统地址写成 0x200000,结果和 OTA 数据区重叠,烧进去之后文件系统挂载失败,查了两天才发现是地址冲突。

提示:合并镜像时 esptool 不会帮你检查地址重叠,它只是按你给的地址依次写入。所以合并之前一定要拿分区表 CSV 和地址列表逐行核对。

2.3 数据边界的三个致命陷阱

第一个陷阱是Flash 大小参数和实际芯片不匹配。S31 支持 2MB、4MB、8MB、16MB 几种规格,如果你在烧录命令里写了--flash_size 4MB,但实际芯片是 8MB,烧录本身不会报错,但芯片启动时 ROM 会按 4MB 去映射地址空间,超过 4MB 的部分访问不到。反过来,如果芯片是 4MB 但你写了 8MB,启动时访问到不存在的地址,直接硬件异常。

第二个陷阱是分区表偏移和 bootloader 偏移混淆。有些教程把 bootloader 写在 0x0000,有些写在 0x1000,这两个值对应不同的 Flash 模式。S31 在大多数开发板上用的是 QIO 模式,bootloader 在 0x1000。但如果你自己画板子用了 DIO 模式,就要改成 0x0000。这个参数在 esptool 命令里是通过--flash_mode控制的,写错了不会报错,但启动不了。

第三个陷阱是NVS 分区的擦除边界。NVS 默认大小是 0x6000,但实际使用中如果存了大量键值对,可能会扩展到 0x9000。做烧录包时如果只擦除 0x6000 的范围,残留的数据可能导致新固件读取到旧的配置项,出现莫名其妙的 behavior。稳妥的做法是在烧录脚本里对 NVS 区域做一次全擦除,或者干脆在固件里加一个版本号校验,版本不匹配就重置 NVS。

这三个陷阱我在不同项目里都遇到过,共同点是烧录工具不报错,但设备行为异常。所以做烧录包的时候,校验环节比烧录环节更重要。

3. esptool 独立打包与脚本实现

3.1 为什么必须用独立版 esptool

esptool 本质上是一个 Python 脚本,官方推荐用 pip 安装。但在交付场景里,你不可能要求对方装 Python。所以必须用 PyInstaller 或者官方提供的独立可执行文件,把 esptool 打包成一个 exe,放在烧录包目录里,脚本直接调用这个 exe。

独立版 esptool 的获取方式有两种:一是从官方发布页下载预编译的 exe,二是自己用 PyInstaller 打包。我推荐第一种,因为官方预编译的版本已经处理好了依赖库和驱动,体积大概 10MB 左右,放在烧录包里完全可以接受。自己打包的话容易漏掉 pyserial 或者 cryptography 这些依赖,在别人电脑上跑不起来。

打包目录结构大概是这样:

flash_package/ ├── esptool.exe ├── factory_full.bin ├── flash.bat ├── config.ini └── drivers/ └── cp210x_windows.zip

esptool.exe 放在根目录,factory_full.bin 是合并好的镜像,flash.bat 是主脚本,config.ini 存串口号和波特率这些可配置项,drivers 目录放 USB 转串口芯片的驱动。这个结构的好处是整个文件夹可以压缩成一个 zip 发给别人,解压就能用,不需要安装任何东西。

注意:esptool.exe 在不同 Windows 版本上可能需要不同的 VC 运行库。如果对方电脑是刚装好的干净系统,可能会提示缺少 msvcp140.dll。稳妥的做法是在 drivers 目录里放一个 vc_redist.x64.exe,脚本检测到运行库缺失时自动安装。

3.2 批处理脚本的核心逻辑拆解

flash.bat 这个脚本看起来简单,但要写得健壮并不容易。核心逻辑分五步:检测串口、选择波特率、擦除 Flash、写入镜像、校验结果。每一步都要有错误处理和用户提示。

检测串口这一步,可以用 esptool 的--list-ports参数,也可以直接调 Windows 的 mode 命令。我习惯用 esptool 的方式,因为输出格式统一,方便解析。脚本里大概这样写:

@echo off setlocal enabledelayedexpansion set ESPTOOL=esptool.exe set IMAGE=factory_full.bin set BAUD=921600 echo 正在检测串口... for /f "tokens=1" %%p in ('%ESPTOOL% --list-ports') do ( set PORT=%%p echo 发现串口: !PORT! )

这段代码会把第一个检测到的串口赋值给 PORT 变量。如果电脑上接了多个串口设备,就需要让用户手动选择,或者根据 USB VID/PID 过滤。S31 开发板常用的串口芯片是 CP2102 和 CH340,VID 分别是 10C4 和 1A86,可以在脚本里加判断。

波特率的选择也有讲究。921600 是常用值,速度快且稳定。但有些便宜的 USB 转串口线在 921600 下会丢数据,这时候要降到 460800 或者 115200。我的做法是在 config.ini 里设一个默认值,脚本启动时读这个值,如果烧录失败自动降速重试。

擦除 Flash 这一步,全片擦除用erase_flash命令,大概需要几秒钟。如果只擦应用区,可以用erase_region指定地址范围。全片擦除的好处是干净,坏处是 NVS 和文件系统也没了。产线首次烧录用全片擦除,现场升级用区域擦除。

写入镜像的命令是核心:

%ESPTOOL% --chip esp32s31 --port %PORT% --baud %BAUD% ^ --before default_reset --after hard_reset ^ write_flash --flash_mode qio --flash_size 4MB ^ --flash_freq 80m 0x0 %IMAGE%

这里的--before default_reset和--after hard_reset控制烧录前后的复位行为。default_reset 会让芯片进入下载模式,hard_reset 让芯片烧完后自动重启。这两个参数如果写错,会出现“烧录成功但设备不启动”的假象——其实是芯片还在下载模式,没有正常复位。

校验环节用verify_flash命令,把刚写入的内容读回来和源文件对比。这一步会多花几秒钟,但能确保数据完整性。我遇到过 USB 线接触不良导致写入数据错位的情况,如果没有校验,设备启动后行为异常,排查起来非常麻烦。

3.3 参数计算:波特率、超时与重试次数

波特率不是越高越好。921600 在大多数情况下没问题,但如果你用的 USB 线质量差,或者电脑 USB 口供电不足,高波特率会导致数据丢包。esptool 在写入时会分块传输,每块大概 1KB 到 4KB,如果某一块丢了,整个镜像就废了。

超时时间也要根据波特率调整。esptool 默认的超时是 3 秒,在 115200 波特率下烧 4MB 镜像大概需要 5 分钟,每块传输都在超时范围内。但如果波特率降到 9600,单块传输就可能超过 3 秒,需要把超时调到 10 秒以上。这个参数在 esptool 里是--timeout,单位是秒。

重试次数我一般设 3 次。第一次失败可能是串口占用或者芯片没进下载模式,第二次失败可能是波特率问题,第三次还失败就提示用户检查硬件连接。重试逻辑在 bat 里用 errorlevel 判断:

set RETRY=0 :flash_loop %ESPTOOL% ... write_flash ... if %errorlevel% neq 0 ( set /a RETRY+=1 if !RETRY! lss 3 ( echo 烧录失败,正在重试第 !RETRY! 次... timeout /t 2 >nul goto flash_loop ) else ( echo 烧录失败,请检查设备连接。 pause exit /b 1 ) )

这段逻辑看起来简单,但实际用起来很有效。产线上工人插拔 USB 线的动作不规范,经常出现接触不良,自动重试能省掉很多人工干预。

4. 常见问题排查与避坑经验

4.1 烧录失败的五种典型表现与对策

烧录失败的表现有很多种,但归纳起来无非五类。第一类是找不到串口,通常是驱动没装或者 USB 线只供电不传数据。第二类是连接超时,芯片没有进入下载模式,需要手动按住 BOOT 键再点烧录。第三类是写入校验失败,数据传输出错,降波特率或者换线。第四类是烧录成功但设备不启动,地址参数或者 Flash 模式不对。第五类是烧录到一半卡死,通常是镜像文件损坏或者 Flash 有坏块。

这五类问题里,第四类最难排查,因为工具层面没有任何报错。我的排查方法是:先用read_flash把芯片里的内容读出来,和源文件做二进制对比。如果内容一致但设备不启动,那就是参数问题;如果内容不一致,那就是传输问题。这个对比用fc命令或者 Python 的 filecmp 模块都能做。

故障表现可能原因排查方法解决措施
找不到串口驱动缺失/线缆问题设备管理器查看安装驱动/换线
连接超时未进下载模式手动按 BOOT调整复位电路
校验失败传输丢包降波特率重试换高质量线缆
烧录成功不启动地址/模式错误read_flash 对比核对分区表
烧录卡死镜像损坏/坏块检查文件哈希重新生成镜像

提示:read_flash 读出来的数据可以用 certutil 算 MD5,和源文件对比,几秒钟就能定位问题。

4.2 那些文档里不会写的实操心得

第一个心得是烧录包里的镜像文件要带版本号和日期。我习惯把镜像命名为factory_full_v1.2.3_20240615.bin,这样产线上出了问题能快速定位是哪个版本。脚本里也可以加一个显示版本号的逻辑,烧录前让用户确认。

第二个心得是config.ini 要放在脚本同级目录,不要放在系统目录。有些教程把配置写到注册表或者 AppData 里,结果换一台电脑就找不到配置了。绿色版的原则是所有文件都在一个文件夹里,拷贝走就能用。

第三个心得是给 bat 脚本加一个日志功能。烧录过程中的所有输出都重定向到一个 log 文件里,出问题时把 log 发回来就能分析。日志文件名带时间戳,避免覆盖。这个功能在产线上特别有用,工人只需要把 log 文件发过来,我就能判断是硬件问题还是脚本问题。

set LOGFILE=flash_log_%date:~0,4%%date:~5,2%%date:~8,2%_%time:~0,2%%time:~3,2%.txt echo 烧录开始 %date% %time% > %LOGFILE% %ESPTOOL% ... >> %LOGFILE% 2>&1

这段代码会把 esptool 的所有输出追加到日志文件里。注意2>&1不能少,否则错误信息不会进日志。

第四个心得是驱动安装要自动化。CP2102 和 CH340 的驱动在 Windows 10 以上版本通常能自动识别,但 Windows 7 需要手动装。我一般在烧录包里放一个install_driver.bat,用 pnputil 命令静默安装驱动。这个命令需要管理员权限,所以脚本开头要加权限检测。

4.3 数据边界校验的自动化方案

前面反复强调数据边界的重要性,但靠人工核对地址列表容易出错。我的做法是写一个 Python 脚本,读分区表 CSV 文件,自动生成 esptool 的烧录参数。这样分区表改了,烧录参数自动跟着变,不会出现不一致。

脚本逻辑大概是:解析 CSV 里的 Name、Offset、Size 三列,过滤掉不需要烧录的分区(比如 otadata),然后按 Offset 排序,生成 merge_bin 命令的参数列表。这个脚本大概 50 行代码,但能省掉大量人工核对的时间。

import csv partitions = [] with open('partitions.csv', 'r') as f: reader = csv.DictReader(f) for row in reader: if row['Name'] in ['nvs', 'otadata']: continue partitions.append((int(row['Offset'], 16), row['Name'])) partitions.sort() for offset, name in partitions: print(f"0x{offset:x} {name}.bin")

这个脚本的输出可以直接拼到 esptool 命令里。实际项目中我会把它集成到 CI 流程里,每次编译完自动生成烧录包,确保镜像和参数永远同步。

注意:分区表 CSV 里的 Offset 是十六进制字符串,解析时要指定 base 16,否则 0x10000 会被当成十进制 10000,差了两个数量级。

4.4 产线批量烧录的效率优化

如果只是给几个同事烧录,上面的方案足够了。但如果是产线批量烧录,一天几百片,就需要考虑效率。第一个优化是并行烧录,一台电脑接多个 USB 口,同时烧多片。esptool 支持指定不同的串口,用多线程或者多进程调用就行。第二个优化是跳过擦除,如果芯片是全新的,Flash 本来就是空的,可以跳过 erase_flash 直接写入,省几秒钟。第三个优化是用更快的波特率,921600 不够就上 1500000,但前提是 USB 转串口芯片支持。

并行烧录的脚本稍微复杂一点,要用 start 命令开多个窗口,每个窗口烧一个串口。日志分别记录,最后汇总结果。这个方案我在一个小批量产线上用过,四路并行,效率比单路提升了将近三倍。

不过并行烧录有个坑:多个 esptool 实例同时运行时,如果它们用的临时文件路径冲突,会互相干扰。解决办法是在脚本里给每个实例指定不同的工作目录,或者用--no-stub参数跳过 stub 加载。这个参数会稍微降低烧录速度,但能避免冲突。

最后再分享一个实用技巧:烧录完成后自动校验设备是否正常启动。方法是在脚本最后加一个串口读取逻辑,等几秒钟,看串口有没有输出预期的启动日志。如果有,说明烧录成功;如果没有,提示可能存在问题。这个逻辑用 Python 的 pyserial 实现最简单,但为了不依赖 Python,也可以用 PowerShell 的 SerialPort 类。虽然多花一点时间,但能把问题拦截在产线环节,避免不良品流出。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询