Skip to content

CBinary 是 OTX 代理插件中用于二进制数据缓冲区操作的轻量级工具类。它将原始字节数组 (uint8_t*) 和长度 (uint16_t) 封装为一个可拷贝、可比较、可拼接的值类型,简化了底层二进制数据(如诊断命令帧、安全访问种子/密钥、DTC 应答数据)的构建、传输与解析流程。CBinary 在刷写框架的 MCD 诊断通信层中承担核心角色——所有诊断请求与响应的载体均为 CBinary 对象,同时在 DTC 故障码管理模块中用于存储 ECU 返回的原始应答数据。

Sources: binary.h

设计动机与架构定位

在汽车诊断与 ECU 刷写场景中,数据交换的基本单位是可变长度的字节序列:UDS 诊断帧、安全访问的种子 (Seed) 与密钥 (Key)、TransferData (0x36) 的数据块、DTC 读取应答报文等。C++ 标准库虽然提供了 std::vector<uint8_t>std::string,但它们各有局限——std::vector 内存开销较大、std::string 语义上不适合二进制数据且遇到 \0 会截断。

CBinary 的设计遵循 零开销抽象 原则:仅持有两个成员——指向堆内存的指针 m_pData 和 16 位长度 m_nSize。它牺牲了动态扩容的效率(每次 Append 都重新分配内存),换取了极简的内存模型和确定性的生命周期,适用于缓冲区最大不超过 65535 字节(64KB)的场景——这恰好是 UDS 协议中单帧最大长度的约束。

Sources: binary.h, binary.cpp

在项目中,CBinary 依赖于 algorithm.h 中定义的 MIN 宏(用于 operator < 的词法比较和 ReadBuffer 的边界裁剪),其自身位于底层工具层,被上层诊断通信模块和 DTC 管理模块引用:

mermaid
graph TD
    A[CBinary<br>binary.h / binary.cpp] --> B[algorithm.h<br>MIN 宏]
    A --> C[标准库<br>stdlib.h, string.h]
    
    D[MCDCmdTrans<br>MCD 诊断命令传输] --> A
    E[MCDCmdBoardcast<br>MCD 广播命令] --> A
    F[jidu_dtc.cpp<br>DTC 故障码管理] --> A
    G[jidu_macro.h<br>公共类型定义] --> A
    H[VBFBlock.h<br>VBF 数据块] --> A

    style A fill:#e1f5fe,stroke:#0288d1,stroke-width:2px
    style D fill:#fff3e0,stroke:#f57c00
    style E fill:#fff3e0,stroke:#f57c00
    style F fill:#f3e5f5,stroke:#7b1fa2

Sources: binary.cpp, algorithm.h

核心数据结构与内存模型

CBinary 仅有两个公开成员变量:m_pData(指向堆上分配的 uint8_t 数组)和 m_nSizeuint16_t 类型,记录数组长度)。这种极简设计意味着:

  • 默认构造后 m_pData = NULLm_nSize = 0
  • 所有构造路径最终汇集到 Init() 方法
  • Init()iLength == 0 时保持 NULL 状态,否则 new uint8_t[iLength] 分配内存
  • Empty() 通过 delete[] 释放内存并将指针置 NULL
  • 析构函数直接调用 Empty()

Source: binary.cpp

内部缓冲区无容量预留机制(无 m_nCapacity 成员),这意味着每次 Append 操作都会触发完整的「分配新内存 → 拷贝旧数据 → 拷贝新数据 → 释放旧内存」流程。这一设计选择简化了内存管理逻辑,但使得频繁追加操作的复杂度为 O(n²)。在实际使用场景中,CBinary 对象通常在创建时就确定大小(通过构造函数传入完整数据),追加操作主要用于构建协议帧头后附加载荷,次数有限。

构造与初始化 API

CBinary 提供了五类构造函数和两种缓冲写入方式,覆盖了从零创建、从原始指针拷贝、值拷贝等场景:

构造函数签名用途
默认构造CBinary()创建空缓冲区,m_pData 为 NULL
定长构造CBinary(uint16_t iLength)分配 iLength 字节空间,初始化为全零
C 字符串构造CBinary(const char *pBuffer, uint16_t iLength)从 C 风格字符串(可含 \0)拷贝数据
字节数组构造CBinary(uint8_t *pBuffer, uint16_t iLength)从 uint8_t 指针拷贝数据
拷贝构造CBinary(const CBinary& binData)深拷贝另一个 CBinary 对象

Sources: binary.h, binary.cpp

实际项目中的应用模式非常丰富。在诊断通信层,常使用内联十六进制字面量快速构造命令帧:

cpp
// 进入编程会话 (0x10 0x02)
CBinary binReboot("\x10\x82", 2);

// 读取数据 (0x22 0xF1 0x86)
CBinary binReadData("\x22\xF1\x86", 3);

// 安全访问请求 Seed (0x27 0x01)
CBinary bin2701("\x27\x01", 2);

Sources: MCDCmdTrans.cpp, MCDCmdTrans.cpp, MCDCmdTrans.cpp

在 DTC 管理模块中,CBinary 直接从 ECU 返回的原始字节流构造,并存入全局映射表:

cpp
// dtcBytefiled 来自诊断响应,inLen 为其长度
m_allEcuDtcAck[ecuName] = CBinary(dtcBytefiled, inLen);

Sources: jidu_dtc.cpp

WriteBuffer 方法相当于「先清空再写入」,是 Empty() + Append() 的组合操作,适用于需要覆盖整个缓冲区内容的场景(如接收诊断响应后替换旧数据)。

Sources: binary.cpp

元素访问与修改

CBinary 提供了三种元素访问途径和两种修改方式,覆盖只读访问、可写引用访问、边界安全的 Get/Set 操作:

方法签名特性
operator[] (const)uint8_t operator[](uint16_t nIndex) const只读访问,按值返回
operator[] (非 const)uint8_t& operator[](uint16_t nIndex)可写引用,支持 bin[i] = 0xFF
GetAtuint8_t GetAt(uint16_t nIndex)带断言的只读访问
SetAtvoid SetAt(uint16_t nIndex, uint8_t ucNewElement)带断言的单字节修改
Addvoid Add(uint8_t ucNewElement)在末尾追加一个字节

Sources: binary.h, binary.h, binary.h, binary.cpp, binary.cpp

在刷写框架中,operator[] 的非 const 版本被大量用于按字节精确构建协议帧。例如 RequestDownload 方法中构建 11 字节请求帧的方式:

cpp
CBinary binRequestDownloadCmd("\x34\x00\x44\x00\x00\x00\x00\x00\x00\x00\x00", 11);
binRequestDownloadCmd[1] = formatIdentifer & 0xFF;
// ... 后续按位填充内存地址和数据长度

Sources: MCDCmdTrans.cpp

类似的模式也出现在 EraseMemory 的地址与长度编码 SecurityAccess 的安全级别字节调整中,这种「模板帧 + 逐字节覆写」的模式是构建 UDS 诊断帧的惯用方法。

Sources: MCDCmdTrans.cpp, MCDCmdTrans.cpp

数据追加 (Append) 机制

Append 是 CBinary 的核心扩展方法,实现「分配新缓冲区 → 拷贝旧数据 → 追加新数据 → 释放旧缓冲区」的完整流程:

cpp
void CBinary::Append(uint8_t *pBuffer, uint16_t iLength) {
    if (pBuffer == NULL || iLength <= 0) return;
    uint16_t nTemp = m_nSize + iLength;
    uint8_t *pTemp = new uint8_t[nTemp];    // 分配新空间
    if (m_pData) memcpy(pTemp, m_pData, m_nSize);  // 拷贝旧数据
    memcpy(pTemp + m_nSize, pBuffer, iLength);    // 追加新数据
    Empty();            // 释放旧内存
    m_pData = pTemp;    // 指向新内存
    m_nSize = nTemp;
}

Sources: binary.cpp

Append 同时接受 uint8_t*const char* 两种指针类型,后者内部转发到前者。在 SecurityAccess 方法中,Append 用于从诊断响应报文 (0x67 0x01 + 3字节种子) 中提取 3 字节种子值:

cpp
// rf 是 0x27 01 的响应帧,种子从第 3 字节开始
binSeed.Append(rf.m_pData + 2, 3);

Sources: MCDCmdTrans.cpp

VerifyAuthenticity 中,Append 用于在命令头后拼接签名数据:

cpp
binSend = CBinary("\x31\x01\x02\x08", 4);
binSend.Append(strKeyInfo.c_str(), strKeyInfo.length());

Sources: MCDCmdTrans.cpp

数据读取 (ReadBuffer) 方法

ReadBuffer 将内部缓冲区的数据拷贝到外部提供的缓冲区中。其关键设计在于 iLength = 0xFFFF 的哨兵值语义——当调用方传入 0xFFFF(即 65535,恰为 uint16_t 最大值)时,方法自动计算「从起始位置到缓冲区末尾」的长度:

cpp
if (iLength == 0xFFFF) iLength = m_nSize - iStart;
else if ((iStart + iLength) > m_nSize) iLength = m_nSize - iStart;

这种设计允许调用方在不知道确切剩余长度的情况下,一次性读取所有剩余数据。数据通过逐字节循环拷贝到目标缓冲区中。

Sources: binary.cpp

插入与删除操作(条件编译)

InsertAtRemoveAt 两个方法受到 #ifndef _BOOT 条件编译保护——在 Bootloader 场景下禁用。这两个方法用于在缓冲区的指定位置插入或删除指定数量的字节:

  • InsertAt:在 nIndex 位置插入 nCount 个值为 ucNewElement 的字节,通过「分配新内存 → 分段拷贝 → 释放旧内存」完成
  • RemoveAt:从 nIndex 位置删除 nCount 个字节,使用 memmove 将后续数据前移

Sources: binary.cpp

这种条件编译策略暗示了 _BOOT 宏标记的是存储空间严格受限的 Bootloader 环境——InsertAt 的临时分配 + 拷贝策略会在内存紧张时造成风险,因此在引导环境中被裁剪。

运算符重载体系

CBinary 实现了丰富的运算符重载,使其表现出值语义(Value Semantics),在诊断通信代码中可以像基本类型一样自然地组合和比较二进制数据。

拼接运算符 (+ 和 +=)

operator + 创建新对象(不修改原对象),operator += 在左操作数上原地追加。两者都支持 CBinary 与 CBinary 的拼接以及 CBinary 与单个 uint8_t 的拼接:

运算符语义是否修改原对象
bin1 + bin2将 bin1 和 bin2 的内容拼接为新 CBinary
bin1 + 0xFF在 bin1 末尾追加一个字节,返回新对象
bin1 += bin2将 bin2 追加到 bin1 末尾
bin1 += 0xFF在 bin1 末尾追加一个字节

Sources: binary.cpp

SeedToKey 方法中,operator + 用于将种子和掩码拼接后进行密钥计算:

cpp
CBinary binData = binSeed + binMask;

VerifyAuthenticity 中,operator += 用于将十六进制解码后的签名追加到命令头后:

cpp
binSend += CBinary(tempBuffer, rLen);

Sources: MCDCmdTrans.cpp, MCDCmdTrans.cpp

比较运算符 (<, <=, >=, >, ==)

比较运算符实现了字典序 (lexicographic) 比较,与 std::string 的比较语义一致:从第一个字节开始逐字节比较,若所有公共字节相等则较短的缓冲区判定为较小值。

operator < 的实现清晰地展示了这一逻辑:

cpp
// NULL 处理:NULL 小于任何非 NULL
if (binData.m_pData == NULL) return false;
else if (this->m_pData == NULL) return true;

// 逐字节比较公共前缀
uint16_t nLength = MIN(this->m_nSize, binData.m_nSize);
for (int i = 0; i < nLength; i++) {
    if (this->m_pData[i] < binData.m_pData[i]) return true;
    if (this->m_pData[i] > binData.m_pData[i]) return false;
}
// 公共前缀相等,短者为小
return (this->m_nSize < binData.m_nSize) ? true : false;

Sources: binary.cpp

operator == 采用短路求值策略:先比较指针(两 NULL 相等,一 NULL 一非 NULL 不等),再比较长度(不等则直接返回 false),最后逐字节比对。

operator <=operator >= 同样受 #ifndef _BOOT 保护——这些复合比较运算符通过组合 <== 实现,在 Bootloader 中通过直接使用 <== 代替以减小代码体积。

Sources: binary.cpp, binary.cpp

赋值运算符 (=)

operator = 先清空当前对象再进行深拷贝,等价于 Empty() + Append() 的组合:

cpp
void CBinary::operator = (const CBinary& binData) {
    Empty();
    Append(binData.m_pData, binData.m_nSize);
}

Sources: binary.cpp

查询与导出方法

CBinary 提供了简洁的查询接口用于获取内部状态和导出数据:

方法返回类型说明
GetSize()uint16_t返回缓冲区字节数
GetBuffer()uint8_t*返回指向内部缓冲区的裸指针
AsString()const char*将内部数据强制解释为 C 字符串
IsEmpty()bool判断缓冲区是否为空(NULL 或长度为 0)

Sources: binary.h, binary.cpp, binary.cpp

GetBuffer() 返回的裸指针直接暴露内部内存,调用方可在知晓长度的情况下进行读取(如传给 Tracer::hex 进行十六进制日志输出、传给 MyStringUtils::BufferToString 进行字符串转换)。在 DTC 报告模块中,GetBuffer()GetSize() 配合使用以获取 ECU 应答的完整原始数据:

cpp
jd_report_addNokItem(..., m_allEcuDtcAck[ecuName].GetBuffer(), m_allEcuDtcAck[ecuName].GetSize(), ...);

Sources: jidu_dtc.cpp

在诊断通信层中的核心应用

CBinary 在 MCDCmdTransMCDCmdBoardcast 中承担了诊断命令帧的请求封装响应解析的双重角色。

发送命令 (sendCommand)

sendCommand(CBinary binSend) 的执行流程展示了 CBinary 如何桥接 OTX 代理内部数据模型与 MCD3D 诊断协议库的 O_BYTEFIELD 类型:

cpp
CBinary MCDCmdTrans::sendCommand(CBinary binSend) {
    CBinary rfData;                                     // 空响应缓冲区
    otx::O_BYTEFIELD dSend;
    for (int i = 0; i < binSend.GetSize(); i++)         // CBinary → O_BYTEFIELD
        dSend.push_back(binSend.GetAt(i));
    pdu.setBytefield(dSend);                            // 设置请求 PDU
    request->enterPDU(pdu);                             // 注入请求
    // ... 执行诊断通信 ...
    otx::O_BYTEFIELD dRecv = response->getResponseMessage().getBytefield();
    rfData.Init(dRecv.data(), dRecv.size());             // O_BYTEFIELD → CBinary
    return rfData;
}

Sources: MCDCmdTrans.cpp

这里的双向转换模式——CBinary → O_BYTEFIELD(发送)和 O_BYTEFIELD → CBinary(接收)——体现了 CBinary 作为「项目内部统一二进制数据表示」的设计意图:所有外部协议库的数据在边界处转换为 CBinary,内部代码仅操作 CBinary。

响应帧验证模式

诊断响应帧的验证通常遵循「检查非空 → 检查首字节(正响应 = 请求 SID + 0x40,负响应 = 0x7F)→ 检查后续字节」的链式模式:

cpp
CBinary binRecv = SendFrame(binSend, 1);
if (binRecv.IsEmpty() || binRecv[0] != binSend[0] + 0x40) {
    return false;  // 负响应或通信失败
}
// 正响应,继续解析...

Sources: MCDCmdTrans.cpp

这一模式在 EraseMemoryRequestTransferExitActivateSblFinishProgram 等方法中反复出现,构成了刷写框架中诊断响应验证的通用范式。

在 DTC 管理模块中的应用

jidu_dtc.cpp 中,CBinary 用于存储 ECU 返回的 DTC 读取应答原始数据:

cpp
static map<std::string, CBinary> m_allEcuDtcAck;

// 存储 DTC 应答
m_allEcuDtcAck[ecuName] = CBinary(dtcBytefiled, inLen);

// 报告时导出
jd_report_addNokItem(..., m_allEcuDtcAck[ecuName].GetBuffer(), m_allEcuDtcAck[ecuName].GetSize(), ...);

Sources: jidu_dtc.cpp, jidu_dtc.cpp, jidu_dtc.cpp

这里 CBinary 表现出值语义的优势std::map 中存储 CBinary 对象而非指针,深拷贝确保数据生命周期独立于来源缓冲区,避免了悬空指针风险。当 ECU 多次读取 DTC 时,新 CBinary 赋值会自动释放旧数据。

条件编译与环境适配

CBinary 通过 _BOOT_COCOS 两个预处理器宏适配不同的编译环境:

影响范围说明
#ifndef _BOOTInsertAtRemoveAtoperator <=operator >=Bootloader 环境裁剪复杂操作和冗余比较运算符
_COCOS定义后设为 1标记 Cocos2d-x 游戏引擎环境(此项目中恒为 1)
ASSERT_EX(x)多处空宏定义,调试断言在生产构建中不产生代码

Sources: binary.h, binary.cpp, binary.cpp

局限性与设计取舍

CBinary 的直接性和简洁性也带来了以下局限:

  • 64KB 上限uint16_t 长度类型将缓冲区最大容量限制在 65535 字节,不适用于传输大型刷写文件(刷写文件通过分块传输绕过此限制,CBinary 仅承载单个传输帧)
  • 无容量预留:每次 Append 都触发完整内存重新分配,频繁追加时效率低。可通过预分配定长缓冲区后使用 operator[] 逐字节填充来规避
  • 直接暴露裸指针GetBuffer() 返回的 uint8_t*AsString() 返回的 const char* 不携带长度信息,调用方需自行通过 GetSize() 管理边界
  • 无越界检查operator[] 仅依赖 ASSERT_EX(生产构建中为空宏),越界访问将导致未定义行为

这些取舍反映了 CBinary 的设计哲学:在受控的 ECU 诊断场景中,数据帧大小可预测、操作频率适中,优先保证代码简洁性和内存布局的确定性,而非通用性。

Sources: binary.h, binary.cpp

阅读下一步

CBinary 作为底层数据容器,在项目中主要被以下模块消费: