ESP32-USBTool:基于鸿蒙 USB 串口与 esptool 协议的 ESP32 固件烧录项目

在鸿蒙系统下的ESP32-USBTool工具。由于鸿蒙系统的沙箱权限机制,现在的idf框架不能实现完整的业务链条,主要卡在了flash和monitor的环节。本工具就是用来完成这两个环节。

分支2Tags0
文件最后提交记录最后更新时间
3 天前
1 天前
1 天前
3 天前
3 天前
3 天前
3 天前
1 天前
3 天前
3 天前
3 天前
3 天前
3 天前

ESP_USBTOOL — 鸿蒙上的 ESP32 串口烧录工具

在 HarmonyOS(HarmonyOS 6.1 / API 23,2in1 设备)上通过 USB 串口把 bin 固件烧进 ESP32 系列芯片。协议层是 esptool 的一个子集,帧格式与 esptool/loader.py 完全一致, 因此 PC 上 idf.py build 出来的 bin 可以直接由本应用烧录、结果可复现。


1. 使用的 API

串口通信走 @ohos.usbManager.serialserialManager,不直接碰 /dev/ttyACM0

import { serialManager } from '@kit.BasicServicesKit';

系统能力:SystemCapability.USB.USBManager.Serial,起始版本 API 19(2in1 设备)。 本工程 compatibleSdkVersion = 6.1.0(23),满足要求。

用到的接口:

接口 用途
getPortList(): Readonly<SerialPort>[] 枚举端口,SerialPort 只有 portIddeviceName
usbManager.getDevices(): USBDevice[] 取产品名/厂商名。SerialPort 不含这些字段,只能另查一次再按串口桥 VID 白名单配对
hasSerialRight(portId) / requestSerialRight(portId) 检查 / 申请串口权限(首次弹窗)
open(portId) / close(portId) 打开 / 关闭串口,必须配对
setAttribute(portId, attr) 设置波特率、数据位、校验、停止位
write(portId, Uint8Array, timeout) 写数据,单次上限 128 KB,promise 返回实际写入长度
read(portId, Uint8Array, timeout) 读数据,缓冲区上限 8192 字节,超时抛 31400006

关键约束(决定了本应用的形态)

  1. 没有 DTR/RTS 接口。 serialManager 不提供 setDtr/setRts,应用无法像 esptool 那样用串口控制线 自动拉低 GPIO0 复位芯片。必须手动进下载模式: 按住 BOOT → 点一下 RESET(或重新上电)→ 松开 BOOT
  2. 没有硬件流控。 SerialAttribute 里没有 rtscts 字段,只能 8-N-1。
  3. 写入必须分块。 一次 write 上限 128 KB,本应用按 4 KB 切片; ROM 协议本身要求每块 1024 字节。
  4. 授权会失效。 USB 拔出、应用退出重启后 hasSerialRight 变回 false,要重新申请。
  5. 错误码:31400002 未授权、31400003 端口不存在、31400004 被占用、 31400005 未打开、31400006 读超时、31400007 I/O 异常。

注意:本机 SDK 里没有 @ohos.busManager.serialSystemCapability.BusManager.Serial, API 26+)的头文件——只有 @ohos.usbManager.serial.d.ts。所以这里用的是 usbManager.serial。官方文档里两套接口的语义基本一致,后续 SDK 升级到 API 26 后可平滑迁移。


2. 代码结构

entry/src/main/ets/
├── model/
│   ├── Types.ets            # ROM 命令码、SLIP 常量、芯片识别表、MAC 寄存器表、字节工具
│   ├── SlipCodec.ets        # SLIP 编码 + 流式解码(可跨多次 read)
│   ├── SerialTransport.ets  # 端口枚举(含 USB 描述配对)/授权/开闭/参数/分块写/超时读
│   ├── EspLoader.ets        # ROM 协议:sync、识别芯片、读 MAC、FLASH_BEGIN/DATA/END、MD5 校验
│   └── FirmwareSource.ets   # 文件选择器选取 bin、rawfile 内置固件、偏移解析
└── pages/
    └── Index.ets            # UI:端口列表 / 连接 / 选固件 / 烧录进度 / 日志

协议实现要点

  • SLIP:帧定界 0xC0,转义 0xDB0xDB→0xDB 0xDD0xC0→0xDB 0xDC。 解码是流式的——一次 read 可能带回"上一帧尾巴 + 整帧 + 下一帧帧头"。
  • 命令帧struct.pack("<BBHI", 0x00, op, len(data), chk) + data
  • 应答帧:同样是 "<BBHI",但字段位置不同—— raw[0]方向位(必须是 0x01)、raw[1] 才是命令码raw[2..3] 长度、raw[4..7] value。方向位在 raw[0],不要拿 raw[1] 去比 0x01, 否则 SYNC 的应答(命令码 0x08)会被当成"非应答帧"全部丢掉,表现为同步一直失败。 另外 ROM 对一条命令可能连回多帧,要按命令码过滤到匹配的那一帧。
  • 校验和:逐字节异或,初值 0xEF
  • SYNC:载荷 07 07 12 20 + 32 个 0x55,成功后连发 7 帧空命令清掉重复应答。
  • 识别芯片(顺序很重要)GET_SECURITY_INFO 用返回的 chip_id 查表 (ESP32-S3 = 9、C3 = 5、C6 = 13…);只有当该命令不被支持时(ESP8266 / ESP32 / ESP32-S2), 才回退读 0x40001000 的魔数(ESP32 = 0x00F01D83、S2 = 0x000007C6)。 S3 及之后的型号在魔数寄存器上没有固定值(esptool 里 USES_MAGIC_VALUE = False), 必须靠 chip_id 识别,顺序反了会认错芯片。
  • SPI_ATTACH 不是 ESP32 专属:ROM 阶段所有型号都要显式挂载 flash—— ESP32 传 eFuse 里读出的引脚编码,其它型号(S3/C3/C6/P4)传 0 表示默认引脚; ESP8266 的 ROM 没有这条命令(由 FLASH_BEGIN 自行处理)才跳过。
  • FLASH_BEGIN 参数长度因型号而异:ESP32 / ESP8266 只认 16 字节; S3/C3/C6/P4 等要带一个 encrypted 字段,共 20 字节,少发会被 ROM 判为参数错误。
  • ESP32-S3 关看门狗:走 USB-Serial/JTAG 时 RTC WDT 在烧录期间不会被自动复位, 可能中途把板子重启,所以识别为 S3 后先关掉 RTC WDT 并使能 SWD 自动喂狗 (对应 esptool 的 disable_watchdogs,寄存器基址 0x60008000)。
  • 每块 1024 字节FLASH_WRITE_SIZE = 0x400),最后一块用 0xFF 补齐; FLASH_DATA 失败重试 3 次。
  • 烧录后校验SPI_FLASH_MD5 拿芯片侧 MD5,与本地 cryptoFramework 计算的 MD5 比对, 不一致直接报错。
  • 读 MAC:用 READ_REG 读 eFuse,各芯片族地址不同(见 MAC_EFUSE_REG_MAP): ESP32 在 EFUSE_RD_REG_BASE+4/+8 两个字里,ESP8266 在 OTP 里且前 3 字节要按 OUI 规则补, 其余型号在各自 MAC_EFUSE_REG 起的两个字里。取 pack(">II", mac1, mac0)[2:] 得 6 字节。
  • USB 描述靠启发式配对serialManager.SerialPort 只有 portId/deviceName, 想显示 VID/PID 和产品名只能另查 usbManager.getDevices(),再用串口桥 VID 白名单 滤出候选、按数量对齐配到端口上。端口行因此显示成 PID:xxxx VID:xxxx + 产品名(产品名为空时退用厂商名;VID/PID 也配不上时 首段退用 deviceName)。配不上只影响列表上显示什么,不影响任何串口功能。

3. 编译

3.1 本机上的一个坑:SDK 自带的工具没有签名

本机是 HarmonyOS(HongMeng 内核,aarch64 + musl),内核拒绝执行未签名的 ELF。 DevEco 内置的 SDK(/data/app/sdk.org/sdk_1.0.0)里 restooles2abcsyscap_tool 等命令行工具都没签名,于是 hvigor 一调用就报:

spawn .../toolchains/restool EACCES
/bin/sh: .../ets-loader/bin/ark/build/bin/es2abc: Permission denied

解决办法是生成一份"影子 SDK":普通文件复制过去,14 个可执行 ELF 逐个自签名, GB 级的 native/previewer/ 整棵用符号链接挂过去(不占空间):

python3 tools/make_signed_sdk.py \
    --src /data/app/sdk.org/sdk_1.0.0 \
    --dst ~/.esp-usbtool-sdk

脚本用实体复制而不是符号链接来放 ets-loader 等目录,因为 hvigor 内部会对 这些路径做 realpath,符号链接会被还原回原 SDK,结果又拿到未签名的 es2abc

3.2 构建命令

本机 NODE_HOME 默认指向 /data/app/node.org/node_22.7.0/bin,那个 node 同样未签名、 无法执行,构建时改用可运行的 node:

cd /storage/Users/currentUser/Documents/DevEcoStudioProjects/ESP_USBTOOL

NODE_HOME=/storage/Users/currentUser/.harmonybrew/bin \
DEVECO_SDK_HOME=/storage/Users/currentUser/.esp-usbtool-sdk \
hvigorw assembleHap --mode module -p module=entry@default \
    -p product=default -p buildMode=debug --no-daemon

产物:

文件 说明
entry-default-signed.hap 已签名,可 hdc install 到真机
entry-default-unsigned.hap 未签名,仅用于检查打包结果

工程已在 build-profile.json5 里配置了调试签名(signingConfigs,证书在 ~/Documents/ohos/config/ 下)。若换机器或证书过期,签名的产物会构建失败或无法安装, 需要在 DevEco Studio 的 Project Structure → Signing Configs 里重新生成。

构建日志里的 WARN 是 serialManager 的 syscap 提示(该能力只有 USB/2in1 设备支持) 和"函数可能抛异常"的静态检查提示,不是错误。

3.3 安装到设备

hdc list targets
hdc -t <UDID> install entry/build/default/outputs/default/entry-default-signed.hap
hdc -t <UDID> shell "aa start -a EntryAbility -b com.example.esp_usbtool"

4. 使用步骤

  1. 接线:USB 线接 ESP32 开发板的 UART 口(CH340/CP2102/FT232 等 USB 转串口芯片)。

  2. 进下载模式(因为 API 没有 DTR/RTS,这步必须手动): 按住 BOOT → 点一下 RESET → 松开 BOOT

  3. 刷新端口:点"刷新端口",列表里选中目标串口。 若列表为空,检查 USB 线是否为数据线、板子是否上电。

  4. 连接:点"连接",系统会弹窗申请串口权限,选允许。 应用会反复 SYNC 最多 20 秒,成功后显示识别到的芯片型号。 若一直失败,重新执行第 2 步(芯片可能已经跑完程序不在 ROM 里了)。

  5. 选项目(可选,推荐):点「① 进入下载模式」下方的「选择项目」, 选中 ESP-IDF 工程根目录。之后每组「选择文件」会直接跳到该组 bin 所在的位置,不用自己一层层点进去:

    槽位 「选择文件」打开的目录
    引导 <项目目录>/build/bootloader
    分区表 <项目目录>/build/partition_table
    应用 <项目目录>/build
    • 选完会顺带检查 build 目录是否存在,不存在会提示先编译。
    • 点「选择文件」时如果 build 不存在,会提示 项目下没有 build 目录,请先编译(idf.py build) 并拦住,不打开空目录。
    • 如果 build 在、但某个子目录不在(比如只烧应用没生成 bootloader), 会记一条提示并退回打开 build 根目录,不会因此卡住。
    • 不选项目也能用,只是选择器会停在系统上次打开的位置。
    • 路径框也可以直接手输。
  6. 选固件:界面固定给了三个槽位,每行是 槽位名 | 偏移框 | 文件名框 | 选择文件 | 烧录

    槽位 默认偏移 对应 build/ 下的产物
    引导 0x1000 bootloader/bootloader.bin
    分区表 0x8000 partition_table/partition-table.bin
    应用 0x10000 <项目名>.bin

    只烧某一组就点它那一行的「选择文件」和「烧录」,三组互不影响。 偏移可以按需改,但必须 4KB 对齐。文件名框底色与运行日志一致, 选好后文件名和大小会显示在框里,右侧「清除」可单独重选该组。

  7. 烧录:进度条和运行日志实时更新,日志出现 MD5 校验通过 才算写对。 要一次装齐整套固件,按 引导 → 分区表 → 应用 的顺序依次点三行的「烧录」, 并且每次都勾上「烧完后保持在下载模式」——否则第一组烧完芯片就复位 并退出 ROM 下载模式,点第二组会同步失败,得重新按 BOOT+RESET。 全部烧完后取消勾选再烧一次,或者手动 RESET 一下让芯片跑新固件。

  8. 看输出:烧录完成后输出区会自动切到「串口数据」并开始监视, 直接显示芯片复位后的启动日志。也可以手动点「开始监视 / 停止监视」。 输出区固定 480px 高,内容超出时窗口内出现滑动条。

    「运行日志」视图右上角有「显示详细」开关,打开后会多出协议收发明细:

    [21:50:12][T] -> SYNC len=36 0x07 0x07 0x12 0x20 ...(共 36 字节)
    [21:50:12][T] <- SYNC value=0x00000000 (无数据)
    

    -> 是发出的命令、<- 是收到的应答,用来排查"卡在哪一步"。默认关闭: 烧一个 900 KB 固件要发近千块 FLASH_DATA,每块一来一回将近两千行, 会把 300 行的日志上限冲掉、把真正有用的信息挤没。关闭该开关时, 已有的明细行会一并清除。


5. 已知限制

  • 无法自动复位:没有 DTR/RTS,进下载模式和烧完复位都得手动。 部分开发板(带自动下载电路、由 DTR/RTS 经三极管驱动 EN/GPIO0)也帮不上忙, 因为 API 层面就拿不到这两根线。
  • ROM 阶段速度:本实现直接用 ROM 协议,没有上传 flasher stub, 所以不做 CHANGE_BAUDRATE 升速,稳定在 115200。 好处是不必在 ArkTS 里维护一份 stub 二进制;代价是大固件烧录较慢 (约 10 KB/s 量级,1 MB 固件要一分多钟)。
  • 每组独立触发:三个槽位各自独立烧录,不会自动连着烧完三组。 连续烧多组时必须每组都勾「烧完后保持在下载模式」,否则芯片在第一次烧完后 就复位退出 ROM 下载模式,后续组需要重新手动进下载模式。
  • 固定槽位:只有引导 / 分区表 / 应用三个位置,偏移可改但不支持任意增删。 烧别的分区(如 SPIFFS、NVS、OTA 数据)需要把某个槽位的偏移改掉来复用。
  • 未在真机验证:本机没有连接的 ESP32 硬件和可安装的设备, 代码只做到"编译通过",协议逻辑对照 esptool 源码逐条核对但未做端到端实测。

项目介绍

在鸿蒙系统下的ESP32-USBTool工具。由于鸿蒙系统的沙箱权限机制,现在的idf框架不能实现完整的业务链条,主要卡在了flash和monitor的环节。本工具就是用来完成这两个环节。

定制我的领域