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)。
下面这张图把一次"主帧+从帧"的字节流按字段切开,标出每个字段在字节中的位偏移和判读依据,这是后续写解析器的直接依据。
用一段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位拆出来的,不是直接读两个字节——这正是逐字段解析与"看十六进制猜含义"的区别。
转储分析时的判读流程
拿到一段转储,按以下顺序判读,可以快速定位是帧结构问题还是数据内容问题:
- 看起始位:主帧首字节最高位应为1。若为0,先怀疑分析仪的位对齐或帧边界设置,不要急着改解析代码。
- 拆主帧类型与地址:用上面的位切片逻辑取出4位类型和12位地址,与端口配置表核对,确认这次交互寻址的对象是否符合预期。
- 验主帧校验:偶校验不通过,该主帧应被从设备忽略,转储里后续不应出现对应从帧;若仍出现从帧,说明校验逻辑或转储截取有误。
- 按类型定从帧长度:过程数据主帧通常对应16或32位从帧,消息数据对应更长从帧。长度对不上,优先查主帧类型解析是否正确。
- 验从帧校验并提取数据:校验通过后按字节输出数据,再交给上层做端口映射解释。
结构化解析结果示例
把解析器输出整理成表格,便于批量比对多帧转储。下表是两帧示例的解析结果(示例数据,用于说明字段对应关系):
| 帧 | 原始十六进制 | 起始位 | 类型/数据 | 地址 | 校验 | 结论 |
|---|---|---|---|---|---|---|
| 主帧#1 | 1A 3C | 1 | 0x0(过程数据) | 0x123 | 通过 | 寻址0x123,期待16位从帧 |
| 从帧#1 | A5 5A | 1 | 0xA5,0x5A | — | 通过 | 数据有效,交上层解释 |
| 主帧#2 | 2B 7E | 1 | 0x1(消息数据) | 0x37E | 通过 | 寻址0x37E,期待32位从帧 |
| 从帧#2 | 3C 4D 5E 6F | 1 | 0x3C,0x4D,0x5E,0x6F | — | 通过 | 4字节数据,长度与类型匹配 |
表中主帧#2的类型为0x1,对应32位从帧,因此从帧#2解析出4个数据字节,长度自洽。若某帧主帧类型为0x1但从帧只有2字节,就是长度不匹配的典型异常,应回到主帧类型位重新核对,而不是怀疑数据内容。
0x1A3C的低12位恰好是0x23C还是0x123,取决于位切片的起止位置,必须按协议定义的位偏移计算,不能凭字节直觉。建议把位切片函数单独测试几组已知帧,确认偏移无误后再批量解析。