17、USB DFU与固件升级:DFU协议概述、Android端实现DFU Host、固件包校验与安全升级、实战:通过USB升级STM32/ESP32固件
说到USB DFU,我脑子里第一个蹦出来的画面,是几年前在产线调试时,一台设备刷成砖,现场所有人都盯着我。嗯,那时候我就意识到——固件升级这件事,看着简单,坑是真不少。
DFU,全称Device Firmware Upgrade,说白了就是通过USB给设备更新固件。你想想看,嵌入式设备越来越多,总不能每次升级都拆机接烧录器吧?DFU就是干这个的——让用户插上USB线,就能把新固件刷进去。
17.1 DFU协议概述
DFU协议是USB设备类规范的一部分,标准定义在USB Device Class Specification for DFU中。它定义了一套标准的命令集,让USB Host(比如你的Android手机)能够控制设备进入升级模式、传输固件数据、校验并重启。
我个人习惯把DFU协议拆成三层理解:
- 传输层:基于USB控制传输(Endpoint 0),使用标准的USB请求格式。
- 命令层:定义了一套DFU专用命令,比如DNLOAD(下载)、UPLOAD(上传)、GETSTATUS(获取状态)等。
- 状态机层:设备端维护一个状态机,管理从空闲到编程再到完成的整个流程。
DFU协议的核心命令,我整理了一个表格,方便你对照:
| 命令 | 代码 | 方向 | 说明 |
|---|---|---|---|
| DNLOAD | 0x01 | Host→Device | 下载固件数据块 |
| UPLOAD | 0x02 | Device→Host | 上传固件数据块(用于备份或校验) |
| GETSTATUS | 0x03 | Host→Device | 获取设备当前状态和进度 |
| CLRSTATUS | 0x04 | Host→Device | 清除错误状态 |
| GETSTATE | 0x05 | Host→Device | 获取状态机当前状态 |
| ABORT | 0x06 | Host→Device | 中止当前操作 |
17.2 Android端实现DFU Host
Android作为USB Host,实现DFU升级,核心就是通过USB Host API与设备通信。流程大致如下:
- 检测USB设备插入,获取设备权限。
- 读取设备描述符,确认设备支持DFU接口。
- 通过控制传输发送DFU命令,进入编程模式。
- 分块发送固件数据,每发一块查询一次状态。
- 发送完成指令,设备重启运行新固件。
下面是一个简化的代码片段,展示如何发送DNLOAD命令:
// 通过USB控制传输发送DFU DNLOAD命令
UsbDeviceConnection connection = usbManager.openDevice(device);
connection.claimInterface(intf, true);
// 构造DNLOAD请求
// bmRequestType: 0x21 (Host to Device, Class, Interface)
// bRequest: 0x01 (DNLOAD)
// wValue: 块序号(从0开始)
// wIndex: 接口号
// wLength: 数据长度
byte[] firmwareBlock = getFirmwareBlock(blockNum);
int result = connection.controlTransfer(
0x21, // 请求类型
0x01, // 请求码:DNLOAD
blockNum, // 块序号
intf.getId(), // 接口号
firmwareBlock,
firmwareBlock.length,
1000 // 超时时间(毫秒)
);
// 发送后必须查询状态
byte[] statusBuf = new byte[6];
connection.controlTransfer(0xA1, 0x03, 0, intf.getId(), statusBuf, 6, 1000);
// 解析statusBuf,检查bStatus字段是否为0(OK)
这里有个细节——DFU协议要求每个DNLOAD命令后,必须跟一个GETSTATUS命令。为什么?因为设备需要时间擦除Flash、写入数据。你不查状态就直接发下一块,数据大概率会丢。我在项目中遇到过这个问题,排查了整整一个下午才发现是状态查询没做对。
17.3 固件包校验与安全升级
固件升级最怕什么?刷到一半断电、固件包被篡改、刷了错误的版本。这些问题轻则设备变砖,重则造成安全隐患。
我个人习惯在固件包中嵌入以下信息:
- 固件头:包含魔数、版本号、硬件平台ID、固件长度、校验和。
- 固件体:实际的二进制数据。
- 签名:使用私钥对固件头+固件体进行签名。
校验流程我画了一张图,方便你理解:
在Android端,校验固件包的代码大致如下:
// 校验固件包签名
public boolean verifyFirmware(byte[] firmwareData, PublicKey publicKey) {
try {
// 解析固件头(前64字节)
ByteBuffer buffer = ByteBuffer.wrap(firmwareData);
int magic = buffer.getInt();
int version = buffer.getInt();
int hwPlatform = buffer.getInt();
int firmwareLen = buffer.getInt();
byte[] storedHash = new byte[32];
buffer.get(storedHash);
byte[] signature = new byte[256];
buffer.get(signature);
// 检查魔数
if (magic != 0xDFU1) return false;
// 计算固件体哈希
byte[] firmwareBody = Arrays.copyOfRange(firmwareData, 320, 320 + firmwareLen);
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] computedHash = digest.digest(firmwareBody);
// 验证哈希匹配
if (!Arrays.equals(storedHash, computedHash)) return false;
// 验证签名
Signature sig = Signature.getInstance("SHA256withRSA");
sig.initVerify(publicKey);
sig.update(firmwareBody);
return sig.verify(signature);
} catch (Exception e) {
return false;
}
}
17.4 实战:通过USB升级STM32/ESP32固件
理论说完了,咱们来点实际的。我以STM32和ESP32为例,讲讲Android手机怎么给它们升级固件。
17.4.1 升级STM32
STM32内置了系统存储器(System Memory),里面有一段出厂固化的Bootloader。当BOOT0引脚拉高、BOOT1拉低时,芯片上电就会进入DFU模式。这时候,Android手机作为Host,就能通过DFU协议给它刷固件。
具体步骤:
- 将STM32的BOOT0接3.3V,BOOT1接GND,上电或复位。
- Android端通过USB检测到设备,读取接口描述符,确认DFU接口。
- 发送DFU命令,进入编程模式。
- 分块发送固件(STM32的DFU固件通常是.srec或.hex格式,需要解析成二进制)。
- 发送完成后,设备自动跳转到用户Flash执行。
17.4.2 升级ESP32
ESP32的情况稍微不同。它没有内置DFU Bootloader,但Espressif提供了esp-dfu库,可以在ESP32上实现DFU协议。或者,你也可以用ESP32自带的OTA分区,通过USB转串口实现升级。
我个人更推荐用esp-dfu方案,因为它直接走USB,不需要额外的串口芯片。流程如下:
- 在ESP32固件中集成
esp-dfu组件,编译时启用DFU支持。 - 设备上电后,检测USB是否连接。如果检测到Host请求进入DFU模式,则切换到DFU状态机。
- Android端发送DFU命令,分块传输固件。
- 固件写入OTA分区(通常是分区表里的ota_0或ota_1)。
- 升级完成后,设备重启,从新分区启动。
这里有个坑——ESP32的Flash写入速度有限,我建议每发送一个数据块(比如1024字节),就查询一次状态,不要连续猛发。曾经有个项目,我为了追求速度,把块大小设成4096,结果ESP32频繁超时,升级成功率不到60%。后来改成1024字节,成功率直接拉到99%以上。
最后说一句——DFU升级这件事,看着是技术活,其实更是细心活。协议本身不复杂,但边界情况特别多。你想想看,不同芯片的Flash特性不同,不同USB控制器的兼容性不同,甚至不同USB线的质量都会影响升级成功率。我的建议是:先在小范围测试,确认稳定后再推给用户。
好了,这一章的内容就到这里。如果你在实际项目中遇到DFU相关的问题,欢迎随时交流。