4. MVB过程数据报文端口配置与主从数据交换验证

过程数据端口:先定“谁写谁读”,再谈周期交换

MVB 过程数据(Process Data)的交换不是“发一包数据过去”这么简单,它依赖端口(Port)配置建立源与宿的映射关系。主设备周期性地发出主帧,被寻址的从设备用配置好的源端口数据填充从帧返回;接收方则从自己的宿端口取数据。端口属性配错,现象往往是“总线有帧、数据不变”或“数据错位”,而不是通信完全中断。

本例使用一个可复现的教学配置:主设备(Bus Administrator,BA)与一台从设备之间建立两组过程数据端口——一组 16 位源端口到宿端口(用于状态字),一组 32 位源端口到宿端口(用于计数值)。我们通过端口属性表、周期交换时序和一致性比对三步完成验证。

主设备(BA) 主帧调度表 周期 2 ms / 4 ms 主帧地址 0x21 / 0x22 宿端口(Sink) 0x3101 状态字 16 位 0x3102 计数值 32 位 一致性比对 期望值 vs 宿端口值 逐周期递增校验 从设备 从帧响应 按主帧地址回填源端口 F-code 与长度匹配 源端口(Source) 0x2101 状态字 16 位 0x2102 计数值 32 位 应用数据源 状态字:0x00A5 计数:每周期 +1 主帧:地址 0x21 / 0x22,请求源端口数据 从帧:源端口 0x2101 / 0x2102 数据回传 端口映射:0x2101→0x3101,0x2102→0x3102 验证要点:宿端口值应随源端口逐周期更新;长度、端口号、F-code 三者任一不匹配都会导致数据不刷新或错位

端口属性配置:三张表要对得上

过程数据端口配置的核心是让主从两侧对“同一组数据”达成一致。工程上通常需要三处信息对齐:端口号(源/宿配对)、数据长度(16/32/64/128/256 位)、以及主帧使用的F-code(帧功能码,决定从设备用哪个端口应答)。本例配置如下:

数据项从设备源端口主设备宿端口长度主帧地址F-code周期
状态字0x21010x310116 位0x210x0C2 ms
计数值0x21020x310232 位0x220x0D4 ms

配置时最容易出错的是长度与 F-code 的对应:F-code 隐含了应答帧的数据长度,若从设备源端口声明为 32 位而主帧 F-code 指示 16 位,从设备会按较短长度应答,主设备宿端口高位将保持旧值,表现为“数据只更新一半”。

周期交换与一致性验证

配置完成后,主设备按调度表周期性发送主帧 0x21 与 0x22,从设备在收到主帧后于规定响应时间内回填源端口数据。验证分两层:

  1. 映射验证:确认主帧 0x21 的应答确实来自源端口 0x2101,而不是相邻端口。可通过让从设备在两个源端口写入不同特征值(如 0x00A5 与 0x5A5A)来区分。
  2. 一致性验证:从设备计数值每周期 +1,主设备宿端口读到的值应呈单调递增且无跳变、无停滞。连续采集 100 个周期,检查差值恒为 1(考虑 32 位回绕)。
若宿端口值恒定不变,先查主帧地址与 F-code 是否与从设备源端口声明一致;若值周期性跳变或错位,优先怀疑长度配置不匹配导致的高低字节拼接错误。

可执行的验证脚本(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 大端约定与本地解析顺序
数据周期性跳变两个源端口映射到同一宿端口检查端口号唯一性与映射表
端口号在 MVB 中具有方向语义,源端口与宿端口不能混用。配置表应作为版本受控文件管理,主从两侧同时更新,避免单侧修改导致“看起来在通信、实际数据错位”。