本文档深入剖析 OTX 刷写框架中两种文件格式下载器的架构设计与实现差异——BinFileDownloader 处理原始二进制(.bin)文件,VbfFileDownloader 处理带结构化头部的 VBF(Vector Binary Format)文件。两者均位于 otx::flash 命名空间下,共同继承自 asam::mcd::MCDEventHandler,共享与 MCD3D 诊断栈的事件回调接口,但在刷写协议流程、数据分帧策略、签名验证机制和进度上报方式上存在本质差异。此外,本文档也将 MCDFileDownloader 作为对比基准——它是完全依赖 ASAM MCD3D 框架内建 MCDFlashJob 的替代实现路径,三者共同构成刷写文件下载的策略矩阵。
Sources: ECUFlasherImpl.cpp
架构定位与类关系
三个下载器均通过 asam::mcd::MCDEventHandler 接口与 MCD3D 诊断运行时对接,但各自的下载逻辑独立于继承关系之外。MCDFileDownloader 将全部刷写控制权委托给 MCD3D 框架(MCDFlashJob),自身仅负责参数设置与结果解析;BinFileDownloader 和 VbfFileDownloader 则绕过框架,通过 MCDCmdTrans 对象手动组装并发送 ISO 14229(UDS)诊断命令,实现对刷写流程的精细化控制。ECUFlasherImpl 将三者均声明为 friend class,允许它们直接访问内部字段(如 mDownloadedSize、mFieldLock)以发布下载进度事件。
Sources: BinFileDownloader.hpp, VbfFileDownloader.hpp, MCDFileDownloader.hpp, ECUFlasherImpl.hpp
classDiagram
class MCDEventHandler {
<<interface>>
+onFirstEvent()
+onLastEvent()
+onPrimitiveProgressInfo()
+onPrimitiveJobInfo()
+onPrimitiveHasResult()
...
}
class MCDFileDownloader {
-ECUFlasher *mFlasher
-File *mEcuFile
-ECU *mECU
-int mErrorCode
-string mMessage
+downloadFile(ECU*, File*) bool
+getFlasher() ECUFlasher*
+setFlasher(ECUFlasher*)
}
class BinFileDownloader {
-ECUFlasher *mFlasher
-File *mEcuFile
-ECU *mECU
-int mErrorCode
-string mMessage
-MCDCmdTrans *cmdTrans
+downloadFile(ECU*, File*) bool
-transferFile(string, uint64_t, int32_t) bool
}
class VbfFileDownloader {
-ECUFlasher *mFlasher
-File *mEcuFile
-ECU *mECU
-int mErrorCode
-string mMessage
-MCDCmdTrans *cmdTrans
+downloadFile(ECU*, File*) bool
-transferFile() bool
}
class MCDCmdTrans {
+EraseMemory(uint32_t, uint32_t) bool
+RequestDownload(uint32_t, uint32_t, uint64_t) bool
+RequestTransferExit() bool
+SendFrame_Cmd36(uint8_t*, int, int, int) CBinary
+VerifyAuthenticity(string, bool) bool
+FinishProgram() bool
+ActivateSbl(string) bool
+SecurityAccess(string, int) bool
+EnterProgramMode()
+EnterSystem() bool
}
class ECUFlasherImpl {
+downloadFile(ECU*, File*) bool
-mDownloadedSize
-mFieldLock
+postFileDownloadProgress(...)
}
MCDEventHandler <|.. MCDFileDownloader
MCDEventHandler <|.. BinFileDownloader
MCDEventHandler <|.. VbfFileDownloader
BinFileDownloader --> MCDCmdTrans : uses
VbfFileDownloader --> MCDCmdTrans : uses
MCDFileDownloader ..> ECUFlasherImpl : friend
BinFileDownloader ..> ECUFlasherImpl : friend
VbfFileDownloader ..> ECUFlasherImpl : friend这种"平级兄弟类 + friend 访问"的设计使得每个下载器可以独立演进各自的刷写协议逻辑,同时保持对 ECUFlasherImpl 内部状态(如下载进度计数器)的直接同步能力。核心差异在于 MCDCmdTrans *cmdTrans 成员——MCDFileDownloader 没有此成员(因为它使用框架接管),而 Bin 和 Vbf 均持有一个 cmdTrans 指针来实现手动诊断通信。
Sources: MCDFileDownloader.hpp, BinFileDownloader.hpp, VbfFileDownloader.hpp
下载器选择工厂:ECUFlasherImpl 中的路由逻辑
三个下载器的选择由编译期宏 FLASHER_USE_MCD_METHOD 和运行时 ECU 名称共同决定。当前代码中该宏定义为 1,因此 MCDFileDownloader 路径被禁用,实际使用的是运行时名称匹配策略:
#define FLASHER_USE_MCD_METHOD 1在 ECUFlasherImpl::downloadFile() 中,当 FLASHER_USE_MCD_METHOD > 0 时,通过 ECU 名称中的关键字判断文件格式:域控制器(TCAM、BGM、CDC、ACU)使用 BinFileDownloader 处理 .bin 原始二进制文件;其余所有 ECU 使用 VbfFileDownloader 处理 VBF 结构化文件。当宏定义为 0 时,回退到 MCDFileDownloader 的模拟进度路径(仅供调试)。
Sources: ECUFlasherImpl.cpp
| 条件 | 选中的下载器 | 适用场景 |
|---|---|---|
| ECU 名称含 "TCAM" || "BGM" || "CDC" || "ACU" | BinFileDownloader | 域控制器(.bin 原始二进制) |
| 其他所有 ECU | VbfFileDownloader | 标准 ECU(.vbf 结构化文件) |
FLASHER_USE_MCD_METHOD == 0(编译期切换) | MCDFileDownloader | 调试/框架原生模式 |
MCDFileDownloader:MCD3D 框架原生的刷写作业模式
MCDFileDownloader 的 downloadFile() 方法完全遵循 ASAM MCD3D 标准的刷写作业(MCDFlashJob)流程。它通过 MCDProjectProvider 获取共享的 MCD 对象(System、Project、Interface),创建逻辑连接(LogicalLink)并上线,然后基于 ECU 的数据库对象(MCDDbLocation)获取刷写作业(MCDFlashJob)定义。刷写参数——PIN 码(PA_PINCode)和文件类型标志(PA_FlashFileFlag)——通过 MCDRequestParameters 物理值接口注入,刷写数据文件路径通过 MCDDbFlashData::setActiveFileName() 绑定。
Sources: MCDFileDownloader.cpp
整个刷写过程由 job->executeSync() 同步执行,MCD3D 框架内部自动处理擦除、下载、校验等所有步骤。执行完毕后通过 MCDResult 提取 PA_JobStatuHEX(错误码)和 PA_AdditErrorInfor(附加错误信息)。这种方式的优势在于与 OTX 标准完全对齐、代码量少,但缺点是缺乏对中间过程的细粒度控制(进度事件完全依赖框架回调 onPrimitiveProgressInfo),且无法针对特定 ECU 定制刷写协议行为。
Sources: MCDFileDownloader.cpp
BinFileDownloader:手动 ISO 14229 协议的原始二进制下载器
downloadFile 主流程
BinFileDownloader 的 downloadFile() 实现了完整的 UDS 刷写序列,共分为五个阶段:
flowchart TD
A[开始 downloadFile] --> B{keyInfo 校验}
B -->|为空| X1[返回 false]
B -->|有效| C[创建 MCDCmdTrans]
C --> D["阶段1: EraseMemory(0,0,fileSize)"]
D -->|失败| X2[返回 false]
D -->|成功| E["阶段2: RequestDownload(0,0,fileSize)"]
E -->|失败| X3[返回 false]
E -->|成功| F["阶段3: transferFile(filepath, fileSize, frameSize)"]
F -->|失败| X4[返回 false]
F -->|成功| G["阶段4: RequestTransferExit"]
G -->|失败| X5[返回 false]
G -->|成功| H["阶段5: VerifyAuthenticity(keyInfo, true)"]
H -->|失败| X6[返回 false]
H -->|成功| I{fileType == 3?}
I -->|否| Y[返回 true]
I -->|是 PBL| J["FinishProgram"]
J --> K["ECU 复位 + 广播 \x11\x81"]
K --> L["等待 10s + 重连车辆"]
L --> M["EnterProgramMode + SecurityAccess"]
M --> Y阶段 1-2 的参数 (0, 0, fileSize) 表明 Bin 格式下擦除和下载请求使用全零的起始地址和长度标识符,实际由 ECU 内部自行解析。阶段 5 的签名验证使用 File 对象上的 keyInfo 字段(通过 ecuFile->getKeyInfo() 获取),第二个参数 isDomain=true 表示使用域签名模式。
Sources: BinFileDownloader.cpp
当文件类型为 3(PBL,Primary BootLoader)时,流程增加了 ECU 复位和重连逻辑:先调用 FinishProgram() 结束编程会话,然后通过 MCDCmdBoardcast 广播 \x11\x81(硬复位)命令,等待 10 秒后调用 otx_reconnect_vehicle_ex(50, 5000) 重新建立车辆通信,再等待 170 秒让 ECU 完全重启,最后重新进入编程模式并执行安全访问(支持两种安全访问模式:SecurityAccess_ECOS 和常规 SecurityAccess("FFFFFFFF", 2),根据 securityAccessMode 决定优先级)。
Sources: BinFileDownloader.cpp
transferFile:基于磁盘文件的帧传输
BinFileDownloader 的 transferFile() 从磁盘直接读取原始二进制文件,使用 fopen/fread 以二进制模式打开文件,单帧最大数据量取 nFrameSize 和 1 MiB 的较小值:
nFrameSize = MIN(1024 * 1024, nFrameSize);每一帧的结构为:1 字节命令码(0x36,即 UDS TransferData)+ 1 字节帧计数器(nCTR,0x00-0xFF 循环)+ N 字节数据。通过 MCDCmdTrans::SendFrame_Cmd36() 发送后检查响应,若收到 0x7F(否定响应)则判定失败。最后一帧(nLastFrameSize > 0)独立发送,使用实际剩余数据量。
Sources: BinFileDownloader.cpp
进度上报机制
BinFileDownloader 的进度上报采用 ECU 级别累积 策略:每发送一帧后,将 nFrameSize(或最后一帧的 nLastFrameSize)累加到 mECU->getDownloadedSize() 和 mEcuFile->increaseDownloadedSize(),然后按 ECU 总大小计算百分比 newProgress = 100 * downloadedSize / totalSize。进度事件仅在百分比变化时发布(oldProgress != newProgress),避免重复触发 UI 更新。
Sources: BinFileDownloader.cpp
VbfFileDownloader:基于 VBF 结构化头部的多块下载器
downloadFile 主流程
VbfFileDownloader 的 downloadFile() 流程与 Bin 有本质不同——它首先从 VBF 头部解析擦除信息和块结构,然后按块迭代刷写:
flowchart TD
A[开始 downloadFile] --> B[创建 MCDCmdTrans]
B --> C["解析 VBF Header: getListOfErase()"]
C --> D["遍历擦除列表<br/>每项格式: addr,size (hex)"]
D --> E["EraseMemory(addr, size)<br/>精确地址擦除"]
E --> F["阶段2: transferFile()<br/>逐块 RequestDownload + 0x36 帧"]
F --> G{VBF 版本 == 2.6?}
G -->|是| H{signatureMode?}
H -->|0 自动| I["优先 prod 签名<br/>回退 dev 签名"]
H -->|1 dev| J["VerifyAuthenticity(devSig, false)"]
H -->|2 prod| K["VerifyAuthenticity(prodSig, false)"]
G -->|否| L[跳过签名验证]
I --> M{fileType == 1?}
J --> M
K --> M
L --> M
M -->|是 SBL| N["ActivateSbl(callAddress)"]
M -->|否| O[返回 true]
N --> O与 Bin 的关键差异:
- 擦除阶段:VBF 头部
erase字段以逗号分隔"addr,size"字符串列表,每组精确指定擦除的起始地址和长度。Bin 则是全范围擦除EraseMemory(0, 0, fileSize)。 - 下载阶段:VBF 按数据块(
VBFBlock)组织,每个块有独立的起始地址(GetStartAddress())和长度(GetLength())。transferFile()内部对每个块分别调用RequestDownload(formatIdentifier, startAddress, length)→ 发送 0x36 帧 →RequestTransferExit。 - 签名验证:VBF 支持三种签名模式(0=自动选择、1=仅开发签名、2=仅生产签名),从 VBF 头部读取
sw_signature和sw_signature_dev,签名验证使用isDomain=false(非域签名模式)。仅 2.6 版本的 VBF 执行签名验证。 - SBL 激活:文件类型为 1(SBL)时,从 VBF 头部
call字段获取激活地址并调用ActivateSbl()。 - 无 PBL 流程:VBF 模式下无需 FinishProgram 和 ECU 复位重连。
Sources: VbfFileDownloader.cpp
transferFile:基于 VBF 块的分段传输
VbfFileDownloader 的 transferFile() 从内存中的 VBFBlock 对象获取数据(由 VBFParserSmall 预先解析完成),而非从磁盘实时读取:
vector<VBFBlock> vecBlock = mEcuFile->parser.GetVBFBlocks();每个 VBF 块包含三个核心属性:GetStartAddress()(起始地址)、GetLength()(数据长度)、GetBlockData()(二进制数据)。transferFile() 对每个块执行完整的"请求下载 → 数据传输 → 传输退出"子流程。帧结构与 Bin 相同:\x36 + CTR + data,但数据来源是内存中的 vector<unsigned char> 而非文件指针。
Sources: VbfFileDownloader.cpp
进度上报机制
VbfFileDownloader 的进度上报与 Bin 存在微妙差异。每帧后同样累加 nFrameSize 到 mECU->setDownloadedSize() 和 mEcuFile->increaseDownloadedSize(),但当 downloadedSize == totalSize 时强制发布 100% 事件。中间进度始终发布(不再做 oldProgress != newProgress 的去重判断),这由 #if false 预处理块中的注释代码可见——曾有过去重逻辑但当前被禁用,改为每次都发布事件。这种设计可能导致更多的 UI 刷新调用,但避免了进度倒退的视觉问题。
Sources: VbfFileDownloader.cpp
核心差异对比
| 维度 | MCDFileDownloader | BinFileDownloader | VbfFileDownloader |
|---|---|---|---|
| 文件格式 | 任意(框架自动识别) | .bin 原始二进制 | .vbf 结构化格式 |
| 诊断通信 | MCD3D FlashJob 框架 | MCDCmdTrans 手动 UDS | MCDCmdTrans 手动 UDS |
| 擦除策略 | 框架内部控制 | EraseMemory(0, 0, fileSize) 全范围 | 从 VBF 头部解析精确 (addr, size) 列表 |
| 数据分块 | 框架内部控制 | 单一连续数据流(fread) | 多个 VBFBlock(内存解析) |
| 帧命令码 | 框架内部控制 | 0x36 + CTR + 数据 | 0x36 + CTR + 数据 |
| 签名来源 | 框架内部控制 | File::getKeyInfo() | VBF Header: sw_signature / sw_signature_dev |
| 签名模式 | 框架内部控制 | isDomain=true(域签名) | isDomain=false,支持 3 种模式切换 |
| SBL 激活 | 框架内部控制 | 不支持 | ActivateSbl(call)(fileType==1) |
| PBL 复位流程 | 框架内部控制 | FinishProgram + 复位 + 重连 + 安全访问 | 不支持 |
| 进度去重 | 框架回调 onPrimitiveProgressInfo | oldProgress != newProgress | 始终发布(无去重) |
| 适用 ECU | 调试/备用路径 | TCAM, BGM, CDC, ACU | 其他所有 ECU |
Sources: BinFileDownloader.cpp, VbfFileDownloader.cpp, MCDFileDownloader.cpp
MCDCmdTrans:两个手动下载器的共享诊断通信层
MCDCmdTrans 是 BinFileDownloader 和 VbfFileDownloader 共同依赖的低层诊断命令传输类。它在构造时持有 ECU 引用,并通过 MCDProjectProvider 获取共享的 MCD 对象。其关键方法构成了 ISO 14229 刷写协议的完整命令集:
| 方法 | UDS 服务 | 说明 |
|---|---|---|
EraseMemory(addr, size) | 0x31(RoutineControl) | 擦除指定地址范围的内存 |
RequestDownload(fmtId, addr, len) | 0x34(RequestDownload) | 请求下载,协商帧大小 |
SendFrame_Cmd36(data, len) | 0x36(TransferData) | 发送单帧数据,自动重试 |
RequestTransferExit() | 0x37(RequestTransferExit) | 结束数据传输 |
VerifyAuthenticity(keyInfo, isDomain) | 0x31(RoutineControl) | RSA 签名验证 |
FinishProgram() | 0x31(RoutineControl) | 完成编程(PBL 专用) |
ActivateSbl(address) | 0x31(RoutineControl) | 激活 SBL(VBF 专用) |
SecurityAccess(pin, level) | 0x27(SecurityAccess) | Seed-to-Key 安全访问 |
EnterProgramMode() | 0x10 0x82 | 进入编程会话 |
EnterSystem() | 0x22 0xF1 0x86 + 0x10 0x02 | 检查并进入系统会话 |
Sources: MCDCmdTrans.hpp
SendFrame_Cmd36() 内置重试逻辑:最多尝试 nTryCount 次(默认 1 次),若收到 0x7F 否定响应则清空返回数据以表示失败。getFrameSize() 返回 nFrameSize(默认 0x8000 = 32768 字节),该值由 RequestDownload 与 ECU 协商后更新。
Sources: MCDCmdTrans.cpp
数据流与文件解析路径
两种下载器的数据来源路径截然不同,这决定了文件预处理阶段的差异:
flowchart LR
subgraph "Bin 路径"
A1["磁盘 .bin 文件"] --> B1["fopen/fread<br/>实时读取"]
B1 --> C1["0x36 帧组装"]
end
subgraph "VBF 路径"
A2["磁盘 .vbf 文件"] --> B2["VBFParserSmall::ParseVBFFile"]
B2 --> C2["VBFHeader 解析<br/>(erase, call, signature...)"]
B2 --> D2["VBFBlock[] 解析<br/>(startAddr, length, data)"]
C2 --> E2["downloadFile 流程控制"]
D2 --> F2["transferFile 内存读取"]
end
C1 --> G["MCDCmdTrans::SendFrame_Cmd36"]
F2 --> G
G --> H["MCDLogicalLink<br/>DoIP 物理层"]Bin 路径:文件在 transferFile() 执行时才被打开和读取,读取与发送交织进行,内存占用受 nFrameSize(最大 1 MiB)限制。VBF 路径:文件在刷写开始前即由 VBFParserSmall::ParseVBFFile() 完整解析到内存,头部元信息用于流程控制,所有块数据存储在 vector<VBFBlock> 中供逐块传输。
Sources: BinFileDownloader.cpp, VbfFileDownloader.cpp, VBFParserSmall.h
阅读建议
本文档位于刷写文件处理分类的核心位置。建议按以下路径继续深入:
- 上溯到刷写框架入口:刷写器架构总览:从 ECUFlasher 接口到多 ECU 并行刷写的继承体系 理解整体层次结构
- 理解低层诊断通信:MCDFileDownloader:基于 MCD3D 协议的 DoIP 文件下载与诊断通信 查看 MCD3D 协议细节
- 了解 VBF 解析:VBF 文件解析器:VBFHeader、VBFBlock 与 RSA 签名验证 深入 VBF 文件结构
- 查看进度事件如何被消费:事件驱动模型:刷写生命周期事件的发布与订阅机制
- 理解安全访问的密钥计算:SeedToKey 与 CRC16:诊断安全访问的密钥计算