手写一个 USB MCU的固件烧写工具:从 vendor 命令到逐字节校验
概述
很多MCU出厂时带一个 USB 固件更新模式:上电后枚举出一个 vendor 设备,主机通过下发厂商自定义命令完成「擦除 → 写入 → 回读校验 → 重启」。这类设备往往没有公开文档,想要把它接入自动化烧写流水线,就得自己「说」厂商的协议。
本文拆解一个完整的固件烧写工具的三层架构,重点讲清 vendor 命令协议 与 逐字节校验 这两块最核心、也最容易踩坑的部分。
整体架构:三层调用链
烧写工具按职责分三层,越往下越接近硬件:
#mermaid-svg-96rkCFi9de9aEaDX{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-96rkCFi9de9aEaDX .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-96rkCFi9de9aEaDX .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-96rkCFi9de9aEaDX .error-icon{fill:#552222;}#mermaid-svg-96rkCFi9de9aEaDX .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-96rkCFi9de9aEaDX .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-96rkCFi9de9aEaDX .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-96rkCFi9de9aEaDX .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-96rkCFi9de9aEaDX .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-96rkCFi9de9aEaDX .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-96rkCFi9de9aEaDX .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-96rkCFi9de9aEaDX .marker{fill:#333333;stroke:#333333;}#mermaid-svg-96rkCFi9de9aEaDX .marker.cross{stroke:#333333;}#mermaid-svg-96rkCFi9de9aEaDX svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-96rkCFi9de9aEaDX p{margin:0;}#mermaid-svg-96rkCFi9de9aEaDX .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-96rkCFi9de9aEaDX .cluster-label text{fill:#333;}#mermaid-svg-96rkCFi9de9aEaDX .cluster-label span{color:#333;}#mermaid-svg-96rkCFi9de9aEaDX .cluster-label span p{background-color:transparent;}#mermaid-svg-96rkCFi9de9aEaDX .label text,#mermaid-svg-96rkCFi9de9aEaDX span{fill:#333;color:#333;}#mermaid-svg-96rkCFi9de9aEaDX .node rect,#mermaid-svg-96rkCFi9de9aEaDX .node circle,#mermaid-svg-96rkCFi9de9aEaDX .node ellipse,#mermaid-svg-96rkCFi9de9aEaDX .node polygon,#mermaid-svg-96rkCFi9de9aEaDX .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-96rkCFi9de9aEaDX .rough-node .label text,#mermaid-svg-96rkCFi9de9aEaDX .node .label text,#mermaid-svg-96rkCFi9de9aEaDX .image-shape .label,#mermaid-svg-96rkCFi9de9aEaDX .icon-shape .label{text-anchor:middle;}#mermaid-svg-96rkCFi9de9aEaDX .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-96rkCFi9de9aEaDX .rough-node .label,#mermaid-svg-96rkCFi9de9aEaDX .node .label,#mermaid-svg-96rkCFi9de9aEaDX .image-shape .label,#mermaid-svg-96rkCFi9de9aEaDX .icon-shape .label{text-align:center;}#mermaid-svg-96rkCFi9de9aEaDX .node.clickable{cursor:pointer;}#mermaid-svg-96rkCFi9de9aEaDX .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-96rkCFi9de9aEaDX .arrowheadPath{fill:#333333;}#mermaid-svg-96rkCFi9de9aEaDX .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-96rkCFi9de9aEaDX .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-96rkCFi9de9aEaDX .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-96rkCFi9de9aEaDX .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-96rkCFi9de9aEaDX .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-96rkCFi9de9aEaDX .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-96rkCFi9de9aEaDX .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-96rkCFi9de9aEaDX .cluster text{fill:#333;}#mermaid-svg-96rkCFi9de9aEaDX .cluster span{color:#333;}#mermaid-svg-96rkCFi9de9aEaDX div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-96rkCFi9de9aEaDX .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-96rkCFi9de9aEaDX rect.text{fill:none;stroke-width:0;}#mermaid-svg-96rkCFi9de9aEaDX .icon-shape,#mermaid-svg-96rkCFi9de9aEaDX .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-96rkCFi9de9aEaDX .icon-shape p,#mermaid-svg-96rkCFi9de9aEaDX .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-96rkCFi9de9aEaDX .icon-shape .label rect,#mermaid-svg-96rkCFi9de9aEaDX .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-96rkCFi9de9aEaDX .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-96rkCFi9de9aEaDX .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-96rkCFi9de9aEaDX :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-96rkCFi9de9aEaDX .app>*{fill:#0078D7!important;stroke:#1D2B3A!important;color:#fff!important;}#mermaid-svg-96rkCFi9de9aEaDX .app span{fill:#0078D7!important;stroke:#1D2B3A!important;color:#fff!important;}#mermaid-svg-96rkCFi9de9aEaDX .app tspan{fill:#fff!important;}#mermaid-svg-96rkCFi9de9aEaDX .mid>*{fill:#26384A!important;stroke:#0078D7!important;color:#fff!important;}#mermaid-svg-96rkCFi9de9aEaDX .mid span{fill:#26384A!important;stroke:#0078D7!important;color:#fff!important;}#mermaid-svg-96rkCFi9de9aEaDX .mid tspan{fill:#fff!important;}#mermaid-svg-96rkCFi9de9aEaDX .dev>*{fill:#C62828!important;stroke:#8E0000!important;color:#fff!important;}#mermaid-svg-96rkCFi9de9aEaDX .dev span{fill:#C62828!important;stroke:#8E0000!important;color:#fff!important;}#mermaid-svg-96rkCFi9de9aEaDX .dev tspan{fill:#fff!important;} subprocess
flash=true
from flash_cmd import *
from base_api_rom import *
pyusb / libusb
下载工具
SFTP 下载固件
烧写主流程
读 bin → 擦写 → 校验
指令封装层
flash 指令
底层收发层
vendor 命令 + ctypes 结构体
MCU
VID 0xVVVV / PID 0xPPPP
- 烧写主流程:编排「读文件 → 擦除 → 分块写 → 分块读回 → 校验 → 重启」。
- 指令封装层:把每类 Flash 操作(擦/写/读/取 ID...)映射成一条 vendor 命令。
- 底层收发层 :用 ctypes 定义命令头结构体,通过 bulk 端点
0x02(OUT)/0x81(IN)收发。
三层解耦的好处:主流程不关心 USB 细节,换硬件只需改底层收发层。
烧写主流程
核心入口 flash_firmware(mode, bin_path) 的完整流程:
#mermaid-svg-5I0tPU8HQCZPUXun{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-5I0tPU8HQCZPUXun .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-5I0tPU8HQCZPUXun .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-5I0tPU8HQCZPUXun .error-icon{fill:#552222;}#mermaid-svg-5I0tPU8HQCZPUXun .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-5I0tPU8HQCZPUXun .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-5I0tPU8HQCZPUXun .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-5I0tPU8HQCZPUXun .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-5I0tPU8HQCZPUXun .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-5I0tPU8HQCZPUXun .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-5I0tPU8HQCZPUXun .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-5I0tPU8HQCZPUXun .marker{fill:#333333;stroke:#333333;}#mermaid-svg-5I0tPU8HQCZPUXun .marker.cross{stroke:#333333;}#mermaid-svg-5I0tPU8HQCZPUXun svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-5I0tPU8HQCZPUXun p{margin:0;}#mermaid-svg-5I0tPU8HQCZPUXun .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-5I0tPU8HQCZPUXun .cluster-label text{fill:#333;}#mermaid-svg-5I0tPU8HQCZPUXun .cluster-label span{color:#333;}#mermaid-svg-5I0tPU8HQCZPUXun .cluster-label span p{background-color:transparent;}#mermaid-svg-5I0tPU8HQCZPUXun .label text,#mermaid-svg-5I0tPU8HQCZPUXun span{fill:#333;color:#333;}#mermaid-svg-5I0tPU8HQCZPUXun .node rect,#mermaid-svg-5I0tPU8HQCZPUXun .node circle,#mermaid-svg-5I0tPU8HQCZPUXun .node ellipse,#mermaid-svg-5I0tPU8HQCZPUXun .node polygon,#mermaid-svg-5I0tPU8HQCZPUXun .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-5I0tPU8HQCZPUXun .rough-node .label text,#mermaid-svg-5I0tPU8HQCZPUXun .node .label text,#mermaid-svg-5I0tPU8HQCZPUXun .image-shape .label,#mermaid-svg-5I0tPU8HQCZPUXun .icon-shape .label{text-anchor:middle;}#mermaid-svg-5I0tPU8HQCZPUXun .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-5I0tPU8HQCZPUXun .rough-node .label,#mermaid-svg-5I0tPU8HQCZPUXun .node .label,#mermaid-svg-5I0tPU8HQCZPUXun .image-shape .label,#mermaid-svg-5I0tPU8HQCZPUXun .icon-shape .label{text-align:center;}#mermaid-svg-5I0tPU8HQCZPUXun .node.clickable{cursor:pointer;}#mermaid-svg-5I0tPU8HQCZPUXun .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-5I0tPU8HQCZPUXun .arrowheadPath{fill:#333333;}#mermaid-svg-5I0tPU8HQCZPUXun .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-5I0tPU8HQCZPUXun .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-5I0tPU8HQCZPUXun .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5I0tPU8HQCZPUXun .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-5I0tPU8HQCZPUXun .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5I0tPU8HQCZPUXun .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-5I0tPU8HQCZPUXun .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-5I0tPU8HQCZPUXun .cluster text{fill:#333;}#mermaid-svg-5I0tPU8HQCZPUXun .cluster span{color:#333;}#mermaid-svg-5I0tPU8HQCZPUXun div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-5I0tPU8HQCZPUXun .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-5I0tPU8HQCZPUXun rect.text{fill:none;stroke-width:0;}#mermaid-svg-5I0tPU8HQCZPUXun .icon-shape,#mermaid-svg-5I0tPU8HQCZPUXun .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-5I0tPU8HQCZPUXun .icon-shape p,#mermaid-svg-5I0tPU8HQCZPUXun .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-5I0tPU8HQCZPUXun .icon-shape .label rect,#mermaid-svg-5I0tPU8HQCZPUXun .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-5I0tPU8HQCZPUXun .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-5I0tPU8HQCZPUXun .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-5I0tPU8HQCZPUXun :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-5I0tPU8HQCZPUXun .step>*{fill:#26384A!important;stroke:#0078D7!important;color:#fff!important;}#mermaid-svg-5I0tPU8HQCZPUXun .step span{fill:#26384A!important;stroke:#0078D7!important;color:#fff!important;}#mermaid-svg-5I0tPU8HQCZPUXun .step tspan{fill:#fff!important;}#mermaid-svg-5I0tPU8HQCZPUXun .io>*{fill:#0078D7!important;stroke:#1D2B3A!important;color:#fff!important;}#mermaid-svg-5I0tPU8HQCZPUXun .io span{fill:#0078D7!important;stroke:#1D2B3A!important;color:#fff!important;}#mermaid-svg-5I0tPU8HQCZPUXun .io tspan{fill:#fff!important;}#mermaid-svg-5I0tPU8HQCZPUXun .erase>*{fill:#F57C00!important;stroke:#E65100!important;color:#fff!important;}#mermaid-svg-5I0tPU8HQCZPUXun .erase span{fill:#F57C00!important;stroke:#E65100!important;color:#fff!important;}#mermaid-svg-5I0tPU8HQCZPUXun .erase tspan{fill:#fff!important;}#mermaid-svg-5I0tPU8HQCZPUXun .ok>*{fill:#2E7D32!important;stroke:#1B5E20!important;color:#fff!important;}#mermaid-svg-5I0tPU8HQCZPUXun .ok span{fill:#2E7D32!important;stroke:#1B5E20!important;color:#fff!important;}#mermaid-svg-5I0tPU8HQCZPUXun .ok tspan{fill:#fff!important;}#mermaid-svg-5I0tPU8HQCZPUXun .err>*{fill:#C62828!important;stroke:#8E0000!important;color:#fff!important;}#mermaid-svg-5I0tPU8HQCZPUXun .err span{fill:#C62828!important;stroke:#8E0000!important;color:#fff!important;}#mermaid-svg-5I0tPU8HQCZPUXun .err tspan{fill:#fff!important;} 一致
不一致
开始
open_device()
打开并初始化 USB 设备
flash_get_id()
读 Flash ID (4B)
get_rom_size(idx)
ID → ROM 容量
mode → erase_offset / erase_max
flash_update_mode_set(mode)
设置更新模式
flash_get_update_base()
读擦写基地址
读 bin 文件 → numpy 数组
flash_erase(base, size+256)
擦除 Flash
按 4K 循环
flash_write_4k 写入
按 4K 循环
flash_read_4k 回读
numpy.array_equal
读回 == 预期 ?
reboot_device()
重启设备
定位首个差异字节
assert 失败
完成
分区与容量:两处映射
mode 决定擦写区间------同一颗 Flash 分多个逻辑区,烧不同内容用不同 mode:
| mode | 含义 | erase_offset | erase_max |
|---|---|---|---|
| 0 | 更新 app code | 0xD000 |
ROM_MAX - 0xD000 |
| 1 | 更新 patch code | 0x2000 |
0x6FFF - 0x2000 |
| 2 | 更新全片 Flash | 0 |
ROM_MAX |
| 3 | 更新 mfi code | 0 |
0x1FFF |
| 4 | 更新 app manage info | 0xB000 |
0xCFFF - 0xB000 |
| 5 | 更新 cfg code | 0x7000 |
0xAFFF - 0x7000 |
ROM 总容量则按 Flash ID 的低字节映射(idx & 0xFF):
| idx & 0xFF | ROM 容量 |
|---|---|
0x14 |
1 MB |
0x15 |
2 MB |
0x16 |
4 MB |
0x17 |
8 MB |
0x18 |
16 MB |
0x19 / 0x39 |
32 MB |
0x1A / 0x20 |
64 MB |
0x1B / 0x21 |
128 MB |
| 其它 | 无法确定 |
USB 设备初始化:跳过认证
设备上电默认进入厂商认证流程(MFI / IAP2)。烧写前要先「跳过认证」、激活命令接口:
#mermaid-svg-czTYj8uD2aIvGRbi{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-czTYj8uD2aIvGRbi .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-czTYj8uD2aIvGRbi .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-czTYj8uD2aIvGRbi .error-icon{fill:#552222;}#mermaid-svg-czTYj8uD2aIvGRbi .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-czTYj8uD2aIvGRbi .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-czTYj8uD2aIvGRbi .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-czTYj8uD2aIvGRbi .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-czTYj8uD2aIvGRbi .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-czTYj8uD2aIvGRbi .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-czTYj8uD2aIvGRbi .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-czTYj8uD2aIvGRbi .marker{fill:#333333;stroke:#333333;}#mermaid-svg-czTYj8uD2aIvGRbi .marker.cross{stroke:#333333;}#mermaid-svg-czTYj8uD2aIvGRbi svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-czTYj8uD2aIvGRbi p{margin:0;}#mermaid-svg-czTYj8uD2aIvGRbi .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-czTYj8uD2aIvGRbi .cluster-label text{fill:#333;}#mermaid-svg-czTYj8uD2aIvGRbi .cluster-label span{color:#333;}#mermaid-svg-czTYj8uD2aIvGRbi .cluster-label span p{background-color:transparent;}#mermaid-svg-czTYj8uD2aIvGRbi .label text,#mermaid-svg-czTYj8uD2aIvGRbi span{fill:#333;color:#333;}#mermaid-svg-czTYj8uD2aIvGRbi .node rect,#mermaid-svg-czTYj8uD2aIvGRbi .node circle,#mermaid-svg-czTYj8uD2aIvGRbi .node ellipse,#mermaid-svg-czTYj8uD2aIvGRbi .node polygon,#mermaid-svg-czTYj8uD2aIvGRbi .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-czTYj8uD2aIvGRbi .rough-node .label text,#mermaid-svg-czTYj8uD2aIvGRbi .node .label text,#mermaid-svg-czTYj8uD2aIvGRbi .image-shape .label,#mermaid-svg-czTYj8uD2aIvGRbi .icon-shape .label{text-anchor:middle;}#mermaid-svg-czTYj8uD2aIvGRbi .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-czTYj8uD2aIvGRbi .rough-node .label,#mermaid-svg-czTYj8uD2aIvGRbi .node .label,#mermaid-svg-czTYj8uD2aIvGRbi .image-shape .label,#mermaid-svg-czTYj8uD2aIvGRbi .icon-shape .label{text-align:center;}#mermaid-svg-czTYj8uD2aIvGRbi .node.clickable{cursor:pointer;}#mermaid-svg-czTYj8uD2aIvGRbi .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-czTYj8uD2aIvGRbi .arrowheadPath{fill:#333333;}#mermaid-svg-czTYj8uD2aIvGRbi .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-czTYj8uD2aIvGRbi .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-czTYj8uD2aIvGRbi .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-czTYj8uD2aIvGRbi .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-czTYj8uD2aIvGRbi .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-czTYj8uD2aIvGRbi .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-czTYj8uD2aIvGRbi .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-czTYj8uD2aIvGRbi .cluster text{fill:#333;}#mermaid-svg-czTYj8uD2aIvGRbi .cluster span{color:#333;}#mermaid-svg-czTYj8uD2aIvGRbi div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-czTYj8uD2aIvGRbi .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-czTYj8uD2aIvGRbi rect.text{fill:none;stroke-width:0;}#mermaid-svg-czTYj8uD2aIvGRbi .icon-shape,#mermaid-svg-czTYj8uD2aIvGRbi .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-czTYj8uD2aIvGRbi .icon-shape p,#mermaid-svg-czTYj8uD2aIvGRbi .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-czTYj8uD2aIvGRbi .icon-shape .label rect,#mermaid-svg-czTYj8uD2aIvGRbi .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-czTYj8uD2aIvGRbi .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-czTYj8uD2aIvGRbi .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-czTYj8uD2aIvGRbi :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-czTYj8uD2aIvGRbi .usb>*{fill:#0078D7!important;stroke:#1D2B3A!important;color:#fff!important;}#mermaid-svg-czTYj8uD2aIvGRbi .usb span{fill:#0078D7!important;stroke:#1D2B3A!important;color:#fff!important;}#mermaid-svg-czTYj8uD2aIvGRbi .usb tspan{fill:#fff!important;}#mermaid-svg-czTYj8uD2aIvGRbi .skip>*{fill:#0078D7!important;stroke:#1D2B3A!important;color:#fff!important;}#mermaid-svg-czTYj8uD2aIvGRbi .skip span{fill:#0078D7!important;stroke:#1D2B3A!important;color:#fff!important;}#mermaid-svg-czTYj8uD2aIvGRbi .skip tspan{fill:#fff!important;} 是
否
find(VID=0xVVVV,
PID=0xPPPP)
set_configuration()
枚举 OUT / IN endpoint
ctrl_transfer(0xC1, 0x05)
厂商检测
msg0 == 1 ?
read(0x83) 读检测数据
write(0x04) 写应答
跳过 IAP2 检测
ctrl_transfer(0x41, 0x04)
跳过认证
set_interface_altsetting(1, 1)
激活命令接口
注意:真正走控制传输(ctrl_transfer)的只有这段认证握手,后续的固件数据都是走 bulk 端点。
vendor 命令协议:26 字节命令头
跨语言用 ctypes 定义命令头结构体(_pack_ = 1 紧凑对齐),标准头 26 字节:
VDCMD_STANDARD_HDR_INFO (26 字节, _pack_=1)
┌──────────┬───────────┬─────────┬────────────┬──────┬───────────┬───────────┬────────┬───────┬───────────┐
│ CmdIdx │ CmdStatus │ CmdType │ SubCmdType │ RSVD │ Paramters0│ Paramters1│ Length │ CRC16 │ CRC16_HDR │
│ u32 │ u32 │ u16 │ u8 │ u8 │ u32 │ u32 │ u16 │ u16 │ u16 │
│ 4B │ 4B │ 2B │ 1B │ 1B │ 4B │ 4B │ 2B │ 2B │ 2B │
└──────────┴───────────┴─────────┴────────────┴──────┴───────────┴───────────┴────────┴───────┴───────────┘
收发模式固定:写 0x02(OUT 端点)下发命令,读 0x81(IN 端点)收响应:
MCU 主机 Python MCU 主机 Python #mermaid-svg-epv10Ga30soxBlg6{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-epv10Ga30soxBlg6 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-epv10Ga30soxBlg6 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-epv10Ga30soxBlg6 .error-icon{fill:#552222;}#mermaid-svg-epv10Ga30soxBlg6 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-epv10Ga30soxBlg6 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-epv10Ga30soxBlg6 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-epv10Ga30soxBlg6 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-epv10Ga30soxBlg6 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-epv10Ga30soxBlg6 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-epv10Ga30soxBlg6 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-epv10Ga30soxBlg6 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-epv10Ga30soxBlg6 .marker.cross{stroke:#333333;}#mermaid-svg-epv10Ga30soxBlg6 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-epv10Ga30soxBlg6 p{margin:0;}#mermaid-svg-epv10Ga30soxBlg6 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-epv10Ga30soxBlg6 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-epv10Ga30soxBlg6 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-epv10Ga30soxBlg6 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-epv10Ga30soxBlg6 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-epv10Ga30soxBlg6 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-epv10Ga30soxBlg6 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-epv10Ga30soxBlg6 .sequenceNumber{fill:white;}#mermaid-svg-epv10Ga30soxBlg6 #sequencenumber{fill:#333;}#mermaid-svg-epv10Ga30soxBlg6 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-epv10Ga30soxBlg6 .messageText{fill:#333;stroke:none;}#mermaid-svg-epv10Ga30soxBlg6 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-epv10Ga30soxBlg6 .labelText,#mermaid-svg-epv10Ga30soxBlg6 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-epv10Ga30soxBlg6 .loopText,#mermaid-svg-epv10Ga30soxBlg6 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-epv10Ga30soxBlg6 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-epv10Ga30soxBlg6 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-epv10Ga30soxBlg6 .noteText,#mermaid-svg-epv10Ga30soxBlg6 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-epv10Ga30soxBlg6 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-epv10Ga30soxBlg6 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-epv10Ga30soxBlg6 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-epv10Ga30soxBlg6 .actorPopupMenu{position:absolute;}#mermaid-svg-epv10Ga30soxBlg6 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-epv10Ga30soxBlg6 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-epv10Ga30soxBlg6 .actor-man circle,#mermaid-svg-epv10Ga30soxBlg6 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-epv10Ga30soxBlg6 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} struct.pack_into 组装命令头 返回 = 26B 头 + 数据, 取 26: device.write(0x02, 标准头 26B + payload)1device.read(0x81, 26 + data_len)2
写操作还会区分 flag:erase 需额外等待擦除完成(超时给到 240000ms),reboot 只写不读(设备直接重启)。
Flash 指令封装
所有 Flash 指令走同一个 cmd = 0x0201 的 vendor 命令,用 subcmd 区分:
| 函数 | subcmd | 读写 | flag | 说明 |
|---|---|---|---|---|
flash_update_mode_set |
0x46 |
write | - | 设置更新模式 |
reboot_device |
0x47 |
write | reboot |
回卷代码并重启 |
flash_get_update_base |
0x85 |
read | - | 读擦写基地址 |
flash_erase |
0x44 |
write | erase |
擦除 Flash |
flash_write_4k |
0xC1 |
write | - | 写 4K 数据 |
flash_read_4k |
0x82 |
read | - | 读 4K 数据 |
flash_get_id |
0x81 |
read | - | 读 Flash ID (4B) |
reset_device |
0x0101/0x41 |
write | - | 复位到 ROM |
校验机制:用 numpy 而非逐字节循环
- 写入与回读都以 4K 为粒度 循环(
ROM_SIZE_4K = 4096)。 - 回读数据用 numpy
array_equal全量比对,零 Python 逐字节循环------这在大固件下是明显的性能点。 - 不一致时用
np.where定位首个差异偏移,打印「预期/实际」字节后assert失败。
小结
一个可靠的 USB 固件烧写工具,本质是三层职责的清晰切分:主流程管编排、指令层管映射、收发层管协议。三个容易被忽略的关键点:
- 认证与数据走不同传输------认证用控制传输,固件数据走 bulk 端点。
- 分区由 mode 决定------同一颗 Flash 的多个逻辑区,烧错 mode 会擦到错误区间。
- 校验要高效------用 numpy 全量比对,避免逐字节 Python 循环拖慢大固件。
把这三层和 vendor 命令头结构摸清楚,同类「USB + 板载 Flash」的设备都能照这套思路接入自动化烧写。