从零手撕usb_burning_tool:一个嵌入式工程师眼中的固件烧录真相
你有没有在产线调试时,盯着电脑屏幕上的“USB device not found”发呆过?
有没有在凌晨三点,因为一块板子反复烧录失败、日志只显示LIBUSB_ERROR_TIMEOUT而怀疑人生?
有没有翻遍芯片手册第17章第4节,却仍搞不清为什么CMD_ENTER_BURNING发出去后,设备就是不回 ACK?
这不是玄学——这是 USB 烧录工具usb_burning_tool(UBT)在向你发出真实世界的叩问。它不是黑盒 GUI,也不是 SDK 里那个“点一下就完事”的按钮;它是 BootROM 和 PC 之间用字节写就的暗语,是 CRC 校验与序列号校验织成的信任之网,是一次次擦除-写入-验证背后对 Flash 物理特性的敬畏。
今天,我们不讲概念,不堆术语,就用工程师的视角,一层层剥开 UBT 的外壳,看看它的血肉是怎么长的。
HID 不是键盘,但它借了键盘的“免驱身份证”
很多人第一次看到全志方案用 HID 类做烧录,第一反应是:“这也能行?”
其实不是“能行”,而是精妙地钻了操作系统的空子。
Windows 自带hidusb.sys,Linux 有hid-generic,macOS 从 10.6 就原生支持——它们认的是Class ID(0x03)+ 描述符结构,而不是“你是不是鼠标”。只要你的设备报告描述符(Report Descriptor)声明自己是 HID,并且定义好 Report ID 和最大包长,系统就自动给你分配端点、建立通信通道。你根本不用写.inf、不用签名驱动、不用求管理员点“始终允许”。
但代价也很真实:HID 中断传输最大包长被硬性限制在 64 字节(低速)或 512 字节(全速)。这意味着你不能像 CDC 那样一股脑扔几 MB 数据过去。你得切块、编号、加校验、等 ACK、重传 NACK——这恰恰逼出了 UBT 最核心的健壮性设计。
看这段实际跑在产线上的代码:
int ubt_send_hunk(libusb_device_handle *dev, uint8_t *data, int len) { uint8_t report[513] = {0}; // 注意:513 = 1字节 Report ID + 512字节 payload report[0] = 0x01; // 必须和设备端 HID descriptor 里定义的 Report ID 严格一致 memcpy(&report[1], data, len); int ret = libusb_control_transfer( dev, LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_CLASS | LIBUSB_RECIPIENT_INTERFACE, 0x09, // SET_REPORT —— 这是 HID 类协议里唯一能发数据的控制请求 0x0300 | 0x01, // Feature Report (0x03) + Report ID (0x01) 0, // interface 0 —— 全志 A64/H616 的烧录接口永远是 interface 0 report, sizeof(report), 5000 // 关键!Bootloader 擦除扇区要时间,5秒超时是底线 ); if (ret < 0) { // 坑点来了:libusb_error_name(ret) 返回的字符串可能被截断 // 真实调试中,你应该记录 raw ret 值(如 -7 = LIBUSB_ERROR_TIMEOUT),再查表 return -1; } return 0; }这段代码里藏着三个产线老手才懂的细节:
report[0] = 0x01不是随便写的。如果你的设备描述符里写的是Report ID = 0x02,而这里填了0x01,设备端 Bootloader 解析时直接丢包——没有报错,只有静默失败。0x0300 | 0x01是位运算组合,不是魔法数字。0x0300表示 Feature Report 类型(区别于 Input/Output Report),| 0x01是 Report ID。漏掉这个|,设备端根本收不到你的数据。5000ms超时不是拍脑袋定的。SPI NOR 擦除一个 sector(4KB)在典型主频下需要 100–300ms;NAND 更慢。如果设成 1000ms,遇到擦除慢的 Flash,你永远卡在第三块。
所以,HID 的“免驱”优势,本质是用协议简洁性换来了逻辑复杂性——你省掉了驱动开发,但必须亲手实现一套类 TCP 的可靠传输机制:序列号、ACK/NACK、滑动窗口(虽然 UBT 多数用停等)、重传退避。
固件镜像不是.bin,它是带防伪印章的“电子公文”
你拿到的firmware.img,从来不是一坨裸二进制。它是一个结构化容器,像一份盖了三重章的政府公文:
第一重章:Header 魔数(Magic Number)
全志是0x4D49494E(ASCII “NIIM”),瑞芯微是0x524B534E(“RKS”+“N”)。这不是为了炫技,而是最廉价的预过滤器。UBT 启动时先读前 4 字节,不对就立刻退出——避免把build.log或coredump当固件烧进去,毁掉整片 Flash。第二重章:CRC16 校验(CCITT-FALSE)
每 512 字节数据块附带一个 CRC16 值(多项式0x1021,初值0xFFFF)。设备端收到后立刻重算,比对不一致就返回NACK。
为什么不用 MD5?因为 MD5 是全局哈希,坏了一块你得重传全部;CRC16 是逐块校验,定位精准,重传成本极低。产线每秒都在烧,时间就是良率。第三重章:SHA256 + RSA 签名(可选但关键)
Header 末尾存着整个 Payload 的 SHA256 摘要,Signature 区则放着厂商私钥签名。设备 BootROM 在启动时会用内置公钥验签——这构成了可信启动(Secure Boot)的第一道门。没有这道门,任何篡改过的固件都能运行;有了它,哪怕你物理拿到 Flash 芯片,也解不开加密密钥。
真正让工程师夜不能寐的,往往不是算法本身,而是边界条件:
如果镜像总长度不是 512 字节对齐怎么办?
→ UBT 会在末尾自动填充0xFF(Flash 擦除后的默认值),并把真实长度写入 Header 的image_length字段。设备端读取时,只处理有效字节,忽略填充。如果 SHA256 计算分块大小设为 4KB,但最后一块不足 4KB 怎么办?
→ NIST FIPS 180-4 明确规定:最后一块不足时,按实际长度计算,无需补零。UBT 的哈希引擎必须严格遵循此规则,否则和 BootROM 算出的摘要永远对不上。
这些细节不会写在用户手册首页,但它们决定了你的固件是“一次成功”,还是“十次九败”。
Bootloader 状态机:不是流程图,而是心跳监护仪
UBT 和 Bootloader 的交互,表面是命令-响应,实质是一场带心跳监测的生命体征监护。
你不能假设“发完 CMD_WRITE_DATA 就万事大吉”。Flash 擦写有物理延迟,USB 总线可能瞬时中断,供电电压可能跌落——任何一个环节出问题,设备都可能卡死在中间状态,变成一块“砖”。
所以 UBT 内置了一个硬实时状态机,每个状态都有自己的超时计时器和迁移守则:
| 状态 | 触发条件 | 超时阈值 | 迁移守则 |
|---|---|---|---|
IDLE | 上电/复位后 | 10s | 收到CMD_ENTER_BURNINGACK →BURNING_READY;超时 → 报错 |
BURNING_READY | 设备返回 Chip ID/Flash ID | 3s | 成功发送首块数据 →WRITE_IN_PROGRESS;超时 → 重发CMD_ENTER_BURNING |
WRITE_IN_PROGRESS | 正在传数据块 | 5s/块 | 每块需带递增seq_num;连续 3 次 NACK → 切换至RECOVERY模式 |
VERIFYING | 所有块发完,发CMD_VERIFY | 15s | 设备回传 SHA256 匹配 →DONE;不匹配 →FAIL并输出坏块位置 |
最关键的设计是seq_num:它不只是序号,更是防重放攻击的密钥。设备端 Bootloader 维护一个期望序列号,收到非递增值(如重复、跳变)直接丢弃。这意味着即使有人截获 USB 流量重放某块数据,也无法破坏烧录完整性——这已满足 ISO/IEC 15408 EAL4+ 对固件更新通道的安全要求。
更狠的是心跳保活机制:UBT 每 5 秒强制发送CMD_KEEPALIVE。很多低成本 USB PHY 在无数据时会进入 suspend 模式,导致 Bootloader 休眠。KEEPALIVE就像给设备打强心针,确保它永远在线待命。
产线不是实验室,它用故障教会你什么是“鲁棒性”
所有理论,在产线灯光下都会被现实击穿。UBT 的真正价值,体现在它如何应对那些教科书不写的故障:
▶ 线缆接触不良?那就学会“断点续传”
USB 插拔瞬间的抖动,常导致LIBUSB_ERROR_NO_DEVICE。旧版工具直接报错退出,工人只能拔插重来。UBT 的做法是:
- 记录最后成功写入的block_index;
- 检测到设备消失后,启动 3 秒重枚举循环;
- 重新连接后,跳过已写区域,从block_index + 1继续——整包重刷?不存在的。
▶ eMMC 出现坏块?那就学会“绕道而行”
eMMC 的 RPMB 分区虽安全,但物理坏块无法避免。UBT 不是硬刚,而是:
- 当CMD_VERIFY返回ERR_BAD_BLOCK时,记录该 LBA 地址;
- 主动跳过该 block,继续写后续数据;
- 最终生成burn_log.csv,标注所有跳过的 LBA 和原因——让 QA 工程师一眼看出是硬件缺陷,而非烧录工具问题。
▶ 多型号混线?那就学会“验明正身”
同一产线可能同时生产 H616 和 H313 板卡。UBT 在IDLE状态后,强制解析镜像 Header 中的chip_id字段(如0x1681= H616),并与设备实际返回的 Chip ID 比对。不匹配?立即终止并弹窗:“固件与硬件不匹配,请更换 firmware.img!”——用代码守住最后一道防错闸门。
这些能力,不是靠增加功能按钮实现的,而是深埋在状态机迁移逻辑、错误码映射表、日志结构体定义里的工程直觉。
写在最后:UBT 是镜子,照见你对底层的理解深度
当你能徒手写出ubt_send_hunk()并解释清楚0x0300 | 0x01的含义;
当你能在firmware.img的 hex dump 里一眼定位 SHA256 摘要位置,并手动验证 CRC16;
当你读懂 Bootloader 的汇编启动代码,知道它在哪一行关闭了 DDR 控制器、哪一行初始化了 USB PHY;
你就不再是一个“调用工具的人”,而是一个能和硅片对话的工程师。
UBT 从不承诺“一键烧录”,它只提供一个契约:
“只要你给我标准的 USB 描述符、正确的 Report ID、合规的镜像格式、稳定的供电与信号,我就保证,每一个字节,都准确无误地躺在它该在的 Flash 地址上。”
剩下的,是你的事——去理解那根 USB 线缆的阻抗匹配,去读懂 BootROM 的 errata note,去调试那行让CMD_RESET失效的寄存器配置。
如果你在实现过程中遇到了其他挑战,欢迎在评论区分享讨论。