USB MCU的固件烧写工具:从 vendor 命令到逐字节校验

手写一个 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

写操作还会区分 flagerase 需额外等待擦除完成(超时给到 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 固件烧写工具,本质是三层职责的清晰切分:主流程管编排、指令层管映射、收发层管协议。三个容易被忽略的关键点:

  1. 认证与数据走不同传输------认证用控制传输,固件数据走 bulk 端点。
  2. 分区由 mode 决定------同一颗 Flash 的多个逻辑区,烧错 mode 会擦到错误区间。
  3. 校验要高效------用 numpy 全量比对,避免逐字节 Python 循环拖慢大固件。

把这三层和 vendor 命令头结构摸清楚,同类「USB + 板载 Flash」的设备都能照这套思路接入自动化烧写。

相关推荐
意法半导体STM322 小时前
【官方原创】STM32CubeMX 6.18.0新版本发布
stm32·单片机·嵌入式硬件·stm32cubemx·cube开发工具
恒锐丰小瑞2 小时前
率能SS8833T 15V/1A/双通道H桥驱动芯片,低静态电流与过温报警输出(FAULT),用于POS打印机/机器人/电池玩具
stm32·单片机·机器人
振南的单片机世界2 小时前
IDLE中断:串口收完一帧数据,硬件自动“吹哨”
arm开发·stm32·单片机·嵌入式硬件
天空'之城2 小时前
单片机基础核心知识点汇总(四)
单片机·嵌入式硬件
zcmodeltech3 小时前
新能源教学实训沙盘模型多系统协同控制系统设计:基于STM32与Modbus RTU的光伏-储能-智慧能源全场景联动方案
stm32·嵌入式硬件·能源·嵌入式实时数据库·多分类
优信电子3 小时前
STM32F103C8T6连接ESP-01S实现WiFi通信:TCP客户端开发实战(二)
stm32·嵌入式硬件·物联网·tcp/ip·esp8266·stm32f103c8t6·esp01/01s
小僧景贤4 小时前
嵌入式之队列解析(裸机/RTOS通用原理+实战场景+源码解析)
数据结构·嵌入式·队列·裸机开发·rtos开发·固件架构
智闲电子设计4 小时前
STM32 外扩 QSPI Flash 实战:字库和开机图存不下,就把 W25Q64 映射成内存直接读
网络·stm32·嵌入式硬件
嵌入式阿蔡4 小时前
逻辑分析仪实战:从波形一眼看出 UART/SPI/I2C 通信错在哪
网络·stm32·单片机·嵌入式硬件·嵌入式实时数据库