3. MVB报文帧结构逐字段解析与十六进制转储分析

MVB帧的十六进制转储长什么样

MVB总线上的每一次主从交互,在分析仪里看到的都是一串十六进制字节。要逐字段解析,先要建立"字节位置→字段含义"的映射。MVB采用曼彻斯特编码,分析仪通常已经把位流还原成字节,因此我们面对的是解码后的帧字节序列,而不是原始电平。

一个完整的主帧-从帧交互包含两部分:主设备发出的主帧(16位,2字节)和从设备应答的从帧(长度可变,2/4/8/16/32字节)。主帧的16位结构为:主帧起始位(1) + 主帧类型(4) + 地址(12) + 校验(1),共18位,但分析仪按2字节呈现时,最高位和校验位需要按位拆解。从帧的结构为:从帧起始位(1) + 数据(16/32/64/128/256位) + 校验(1)。

下面这张图把一次"主帧+从帧"的字节流按字段切开,标出每个字段在字节中的位偏移和判读依据,这是后续写解析器的直接依据。

MVB主帧+从帧十六进制转储的字段切分 主帧(2字节,分析仪按大端呈现) 字节0 0x1A 字节1 0x3C 按位拆解(18位有效) 起始 1 主帧类型 4位 地址 12位 校验 1位 从帧(示例:16位数据,共2字节) 字节0 0xA5 字节1 0x5A 按位拆解(18位有效) 起始 1 数据 16位 校验 1位 字段判读要点 起始位:主帧/从帧首字节最高位恒为1,用于帧同步;若为0说明位对齐错误或帧边界误判。 主帧类型:4位编码,0x0=主帧请求过程数据,0x1=主帧请求消息数据,0x2=主帧请求监视数据等,决定从帧长度。 地址:12位,范围0x000~0xFFF,标识被寻址的端口/设备;需与端口配置表比对确认目标。 校验位:偶校验,覆盖起始位至数据末位;校验失败则该帧丢弃,不进入上层解析。 数据长度:由主帧类型决定(16/32/64/128/256位),解析器须先读主帧类型再确定从帧字节数。 字节序:MVB按位传输,分析仪转储时通常高位在前;多字节数据需按协议约定拼接,不可直接当整数读。

用一段Python把转储解析成结构化结果

下面给出一个可直接运行的解析器。它接收"主帧2字节 + 从帧N字节"的十六进制字符串,按位拆出起始位、类型、地址、校验,并根据主帧类型确定从帧数据长度,最后输出字典。校验采用偶校验:统计从起始位到数据末位(不含校验位本身)的1的个数,校验位使总1数为偶数。

def bits_from_hex(hex_str):
    """十六进制字符串 -> 位列表(高位在前)"""
    data = bytes.fromhex(hex_str.replace(" ", ""))
    bits = []
    for b in data:
        for i in range(7, -1, -1):
            bits.append((b >> i) & 1)
    return bits

def even_parity_ok(bits_without_parity, parity_bit):
    """偶校验:数据位1的个数 + 校验位 应为偶数"""
    return (sum(bits_without_parity) + parity_bit) % 2 == 0

# 主帧类型 -> 从帧数据位数
FRAME_TYPE_BITS = {0x0: 16, 0x1: 32, 0x2: 64, 0x3: 128, 0x4: 256}

def parse_master_frame(hex_str):
    bits = bits_from_hex(hex_str)
    assert len(bits) >= 18, "主帧至少18位"
    start = bits[0]
    ftype = int("".join(map(str, bits[1:5])), 2)
    addr  = int("".join(map(str, bits[5:17])), 2)
    parity = bits[17]
    ok = even_parity_ok(bits[0:17], parity)
    return {
        "起始位": start,
        "主帧类型": f"0x{ftype:X}",
        "地址": f"0x{addr:03X}",
        "校验位": parity,
        "校验通过": ok,
        "从帧数据位数": FRAME_TYPE_BITS.get(ftype, None),
    }

def parse_slave_frame(hex_str, data_bits):
    bits = bits_from_hex(hex_str)
    need = 1 + data_bits + 1
    assert len(bits) >= need, f"从帧需要{need}位,实际{len(bits)}位"
    start = bits[0]
    data = bits[1:1 + data_bits]
    parity = bits[1 + data_bits]
    ok = even_parity_ok(bits[0:1 + data_bits], parity)
    # 按字节拼接数据(高位在前)
    data_bytes = []
    for i in range(0, data_bits, 8):
        byte_val = int("".join(map(str, data[i:i+8])), 2)
        data_bytes.append(f"0x{byte_val:02X}")
    return {
        "起始位": start,
        "数据字节": data_bytes,
        "校验位": parity,
        "校验通过": ok,
    }

# 示例:主帧 0x1A3C,从帧 0xA55A
master = parse_master_frame("1A 3C")
print("主帧解析:", master)
slave = parse_slave_frame("A5 5A", master["从帧数据位数"])
print("从帧解析:", slave)

运行这段代码,主帧输出为:起始位1、类型0x0、地址0x123、校验通过True、从帧数据位数16;从帧输出为:起始位1、数据字节['0xA5','0x5A']、校验通过True。注意地址0x123是从0x1A3C的中间12位拆出来的,不是直接读两个字节——这正是逐字段解析与"看十六进制猜含义"的区别。

解析器里最容易出错的两处:一是把主帧的2字节直接当整数读地址,忽略了起始位和类型位占用了高5位;二是从帧长度没有先查主帧类型就固定按2字节解析,遇到32位数据帧会截断。先解析主帧、再决定从帧长度,是稳定的处理顺序。

转储分析时的判读流程

拿到一段转储,按以下顺序判读,可以快速定位是帧结构问题还是数据内容问题:

  1. 看起始位:主帧首字节最高位应为1。若为0,先怀疑分析仪的位对齐或帧边界设置,不要急着改解析代码。
  2. 拆主帧类型与地址:用上面的位切片逻辑取出4位类型和12位地址,与端口配置表核对,确认这次交互寻址的对象是否符合预期。
  3. 验主帧校验:偶校验不通过,该主帧应被从设备忽略,转储里后续不应出现对应从帧;若仍出现从帧,说明校验逻辑或转储截取有误。
  4. 按类型定从帧长度:过程数据主帧通常对应16或32位从帧,消息数据对应更长从帧。长度对不上,优先查主帧类型解析是否正确。
  5. 验从帧校验并提取数据:校验通过后按字节输出数据,再交给上层做端口映射解释。

结构化解析结果示例

把解析器输出整理成表格,便于批量比对多帧转储。下表是两帧示例的解析结果(示例数据,用于说明字段对应关系):

帧原始十六进制起始位类型/数据地址校验结论
主帧#11A 3C10x0(过程数据)0x123通过寻址0x123,期待16位从帧
从帧#1A5 5A10xA5,0x5A—通过数据有效,交上层解释
主帧#22B 7E10x1(消息数据)0x37E通过寻址0x37E,期待32位从帧
从帧#23C 4D 5E 6F10x3C,0x4D,0x5E,0x6F—通过4字节数据,长度与类型匹配

表中主帧#2的类型为0x1,对应32位从帧,因此从帧#2解析出4个数据字节,长度自洽。若某帧主帧类型为0x1但从帧只有2字节,就是长度不匹配的典型异常,应回到主帧类型位重新核对,而不是怀疑数据内容。

转储分析中不要用"看起来像"来判断字段。例如0x1A3C的低12位恰好是0x23C还是0x123,取决于位切片的起止位置,必须按协议定义的位偏移计算,不能凭字节直觉。建议把位切片函数单独测试几组已知帧,确认偏移无误后再批量解析。