4. MVB过程数据报文端口配置与主从数据交换验证
过程数据端口:先定“谁写谁读”,再谈周期交换
MVB 过程数据(Process Data)的交换不是“发一包数据过去”这么简单,它依赖端口(Port)配置建立源与宿的映射关系。主设备周期性地发出主帧,被寻址的从设备用配置好的源端口数据填充从帧返回;接收方则从自己的宿端口取数据。端口属性配错,现象往往是“总线有帧、数据不变”或“数据错位”,而不是通信完全中断。
本例使用一个可复现的教学配置:主设备(Bus Administrator,BA)与一台从设备之间建立两组过程数据端口——一组 16 位源端口到宿端口(用于状态字),一组 32 位源端口到宿端口(用于计数值)。我们通过端口属性表、周期交换时序和一致性比对三步完成验证。
端口属性配置:三张表要对得上
过程数据端口配置的核心是让主从两侧对“同一组数据”达成一致。工程上通常需要三处信息对齐:端口号(源/宿配对)、数据长度(16/32/64/128/256 位)、以及主帧使用的F-code(帧功能码,决定从设备用哪个端口应答)。本例配置如下:
| 数据项 | 从设备源端口 | 主设备宿端口 | 长度 | 主帧地址 | F-code | 周期 |
|---|---|---|---|---|---|---|
| 状态字 | 0x2101 | 0x3101 | 16 位 | 0x21 | 0x0C | 2 ms |
| 计数值 | 0x2102 | 0x3102 | 32 位 | 0x22 | 0x0D | 4 ms |
配置时最容易出错的是长度与 F-code 的对应:F-code 隐含了应答帧的数据长度,若从设备源端口声明为 32 位而主帧 F-code 指示 16 位,从设备会按较短长度应答,主设备宿端口高位将保持旧值,表现为“数据只更新一半”。
周期交换与一致性验证
配置完成后,主设备按调度表周期性发送主帧 0x21 与 0x22,从设备在收到主帧后于规定响应时间内回填源端口数据。验证分两层:
- 映射验证:确认主帧 0x21 的应答确实来自源端口 0x2101,而不是相邻端口。可通过让从设备在两个源端口写入不同特征值(如 0x00A5 与 0x5A5A)来区分。
- 一致性验证:从设备计数值每周期 +1,主设备宿端口读到的值应呈单调递增且无跳变、无停滞。连续采集 100 个周期,检查差值恒为 1(考虑 32 位回绕)。
可执行的验证脚本(Python 伪总线接口)
下面用一段自洽的 Python 脚本模拟上述验证逻辑。它不依赖真实硬件,而是把“主帧请求—从帧应答—宿端口更新”抽象为函数,便于在没有总线设备时先验证配置表与比对逻辑是否正确。真实工程中把 read_sink_port 替换为对 MVB 接口卡的读取即可。
from dataclasses import dataclass
@dataclass
class PortCfg:
src_port: int
sink_port: int
length_bits: int
master_frame: int
fcode: int
CFG = [
PortCfg(0x2101, 0x3101, 16, 0x21, 0x0C),
PortCfg(0x2102, 0x3102, 32, 0x22, 0x0D),
]
class Slave:
def __init__(self):
self.counter = 0
self.status = 0x00A5
def respond(self, frame_addr, fcode):
# 仅当地址与 F-code 都匹配时才回填对应源端口
for c in CFG:
if c.master_frame == frame_addr and c.fcode == fcode:
if c.src_port == 0x2101:
return self.status & ((1 << c.length_bits) - 1)
if c.src_port == 0x2102:
return self.counter & ((1 << c.length_bits) - 1)
return None
def tick(self):
self.counter = (self.counter + 1) & 0xFFFFFFFF
class Master:
def __init__(self):
self.sink = {c.sink_port: None for c in CFG}
def poll(self, slave, frame_addr, fcode):
for c in CFG:
if c.master_frame == frame_addr and c.fcode == fcode:
val = slave.respond(frame_addr, fcode)
if val is not None:
self.sink[c.sink_port] = val
return val
return None
slave, master = Slave(), Master()
prev = None
ok = True
for cycle in range(100):
master.poll(slave, 0x21, 0x0C)
master.poll(slave, 0x22, 0x0D)
cur = master.sink[0x3102]
if prev is not None and ((cur - prev) & 0xFFFFFFFF) != 1:
ok = False
print(f"cycle {cycle}: 计数值不连续 prev={prev:#x} cur={cur:#x}")
break
prev = cur
slave.tick()
print("状态字宿端口:", hex(master.sink[0x3101]))
print("计数值宿端口:", hex(master.sink[0x3102]))
print("一致性验证:", "通过" if ok else "失败")
预期输出中状态字宿端口为 0x00a5,计数值宿端口为 0x64(100 次递增后的值),一致性验证为“通过”。若把 CFG 中 0x2102 的 length_bits 改为 16,脚本仍能运行,但计数值会在 0xFFFF 后回绕,差值校验将失败——这正是长度配置错误的典型表现。
结果判读与常见配置错误
| 现象 | 可能原因 | 排查动作 |
|---|---|---|
| 宿端口值始终为初始值 | 主帧地址或 F-code 与源端口声明不符 | 核对主帧调度表与从设备端口属性表 |
| 数据只更新低 16 位 | 长度配置为 16 位但实际数据为 32 位 | 统一源/宿端口长度与 F-code 隐含长度 |
| 数据高低字节颠倒 | 字节序约定不一致 | 确认 MVB 大端约定与本地解析顺序 |
| 数据周期性跳变 | 两个源端口映射到同一宿端口 | 检查端口号唯一性与映射表 |