仪表调试软件 技术规格说明书
文档版本:v1.0 适用范围:基于串口 Modbus RTU 的现场仪表调试 / 配置 / 校准软件 读者对象:嵌入式工程师、现场调试人员、测试人员 约定:文中"上位机"指运行本软件的 PC;"仪表"指被调试设备(从机)。所有数值为十六进制除非另有说明;多字节寄存器按**大端(高字节在前)**传输;CRC16 低字节在前。
1. 概述
1.1 软件定位
本软件是运行于 Windows 的桌面工具,通过 RS485 串口与现场仪表通信,完成参数读写、实时量监视、电能管理、版本识别、计量校准等调试任务。其核心设计目标是:将分散在界面各处的通信逻辑收敛为一张命令表 + 统一的收发通道,使新增指令只需改表、更换通信介质只需替换传输实现。1.2 功能清单
1.3 运行环境与通信参数
2. 软件总体设计
2.1 分层架构
2.2 模块职责
| | | |
|---|
| MainForm / Tabs / LogPanel | | |
| | | |
| | | |
| | | , SerialTransport, MockTransport |
| | | |
2.3 关键设计机制
:将原先分散的 100 余处switch分支收敛为Cmd(命令)与Field(字段)两张表。新增一条读取指令仅需向表中追加一条记录,收发与解析逻辑零改动。:连接后通过0x11读取软件识别码与版本号,一次性计算出"是否支持 5~8 费率 / 新版写电量寄存器 / 电能小数位"等能力开关,替代散落在多处、彼此可能不一致的字面量判断。:字节收发被抽象为IByteTransport接口。无硬件时由MockTransport假从站模拟响应,可在不接仪表的情况下跑通全部界面与逻辑(演示模式)。:把"组帧→发送→接收→CRC 校验"封装为单步调用;校验失败自动重发,超过重试上限抛出超时异常,调用方不会拿到未经验证的数据。3. 通信协议
3.1 物理层
上位机经 USB 转 RS485 适配器连接仪表 A/B 双绞线。软件侧使用System.IO.Ports.SerialPort实现,读操作采用"累计收满期望字节或超时返回"策略,超时时间内未收满则按实际长度返回并由上层判定。3.2 帧格式
示例(读实时量请求):01 03 00 61 00 1A [CRC_L] [CRC_H],共 8 字节。
3.3 CRC16
处理:每字节与当前 CRC 低 8 位异或,随后右移 8 次,移出位为 1 时与0xA001异或回填:计算所得 16 位值,低字节写入CRC_L,高字节写入CRC_H接收端以"除最后 2 字节外的全部内容"重算 CRC,并与帧尾 2 字节比较,一致方为有效帧。3.4 功能码总表
4. 指令参考
每条指令给出:功能、请求帧、响应帧、字段说明、备注。4.1 读保持寄存器(0x03,通用)
请求:[ADDR] 03 [REG_H] [REG_L] [CNT_H] [CNT_L] [CRC_L] [CRC_H]响应:[ADDR] 03 [LEN] [DATA: LEN 字节] [CRC_L] [CRC_H],其中LEN = CNT × 2。4.2 读实时量
请求:01 03 00 61 00 1A [CRC]响应数据区:52字节,字段如下(偏移相对数据区首字节):4.3 读电能块
结构:6 个块(正向/反向 有功·无功 × 总/尖/峰/平/谷…),每块20字节 =4字节总值 +8×2字节费率。总值缩放100。4.4 读版本(0x11)
请求(固定握手码):[ADDR] 11 FE DC BA 98 76 54 32 10 [CRC]响应:15字节;[3..4]为软件识别码,[7]/[8]携带版本信息,供能力协商使用。4.5 写多个寄存器(0x10)
请求:[ADDR] 10 [REG_H] [REG_L] [CNT_H] [CNT_L] [BYTE_LEN] [DATA] [CRC]响应(回显):[ADDR] 10 [REG_H] [REG_L] [CNT_H] [CNT_L] [CRC],共 8 字节。4.6 清零(0xAA,写命令寄存器)
请求:[ADDR] AA [REG_H] [REG_L] 00 01 02 [HIGH] [LOW] [CRC]4.7 写电量块(0xAB)
请求:[ADDR] AB [REG_H] [REG_L] [CNT_H] [CNT_L] [BYTE_LEN] [4×CNT 字节大端] [CRC]用于直接写入正向/反向有功电量等,数值为原始 4 字节大端整型。4.8 下装计量参数(0x98)
请求:[ADDR] 98 [REG_H] [REG_L] 00 03 06 [Un_H] [Un_L] [Ib_H] [Ib_L] [Ec_H] [Ec_L] [CRC]REG = 0x6400 + 组号(1..4);Un = 额定电压×10,Ib = 额定电流×100,Ec为脉冲常数原值。直接接入与互感器接入的Ib/Ec取值不同,由接法决定。4.9 校准(0x99)
请求:[ADDR] 99 [REG_H] [REG_L] 00 01 02 00 00 [CRC]增益校准:REG = 0x6400 + 组号,标准源置1.0Un / 1.0Ib / PF=1.0后下发。相位校准:REG = 0x6500 + 组号,标准源切PF=0.5L后下发。5. 关键业务流程(软件设计)
5.1 连接与能力协商
依据结果计算能力开关(费率支持、写电量寄存器、小数位);5.2 校准流程
校准须严格按序执行,且0x98与0x99之间、各组之间需留出等待时间(建议 ≥ 3 s):步骤1 0x98 下装量程(直接接入 1A/400,互感器接入 10A/10000) └─ 等待 ≥ 3s步骤2 0x99 增益校准(标准源 1.0Un/1.0Ib/PF=1.0)步骤3 0x99 相位校准(标准源切 PF=0.5L)
5.3 实时刷新流程
定时器触发 → Link.Read("Realtime") → 0x03 组帧发送 → Transact 接收校验 → Decode 按字段换算 → UI 刷新。任意一次 CRC 失败由Transact透明重试,UI 层无感知;连续失败则停止刷新并提示超时。6. 数据解析规范
字段解析统一跳过响应前 3 字节(地址/功能码/长度),按Field定义处理::为true时按补码解释(最高位为 1 则减去2^(Size×8));7. 附录
A. 常用寄存器速查
B. 异常与处理
C. 演示模式
设置MockTransport作为传输实现(界面勾选"演示模式"或启动参数--demo),由内置假从站按寄存器地址返回模拟数据。该模式下所有 UI 流程、指令编排、字段解析均与真实环境一致,仅数据来源为本地生成。8. 各指令完整代码实现(C#)
以下为协议层与业务层的完整可运行代码(已去除品牌相关命名,统一以 Meter* 表示)。 运行依赖:.NET 6+,串口使用 System.IO.Ports。核心规律:每一条指令 = 一个组帧函数 + 一次 Transact 收发 + 一次按字段解析。 所有多字节整数均按**大端(高字节在前)**传输;CRC16 低字节在前。
8.1 基础设施:传输接口与 CRC16
IByteTransport只定义收发两件事,业务层不关心底层是真实串口还是假从站:using System;using System.Collections.Generic;using System.Linq;///<summary>字节传输最小抽象:接上 SerialPort 即可,也可接 Mock 假从站。</summary>public interface IByteTransport{ voidWrite(byte[] frame); ///<summary>读取正好 count 字节;超时或不足时返回实际读到的字节数。</summary> intRead(byte[] buffer, int count);}
ModbusRtu是协议层主体,集中实现 CRC 与全部组帧/解帧逻辑:public enum EFuncCode : byte{ ReadHolding = 0x03, // 标准:读保持寄存器 WriteMultiple = 0x10, // 标准:写多个寄存器(本软件连写 1 个也用它) PrivateWriteCmd = 0xAA, // 私有:写命令寄存器(清零) PrivateWriteBlock = 0xAB, // 私有:写电量等大块数据 PrivateReadVer = 0x11, // 私有:读版本 PrivateDownload = 0x98, // 私有:下装计量参数 PrivateCalibrate = 0x99 // 私有:增益/相位校准}public static class ModbusRtu{ ///<summary>标准 Modbus CRC16:多项式 0xA001,初值 0xFFFF,回填低字节在前。</summary> publicstaticushortCrc16(ReadOnlySpan<byte> data) { ushort crc = 0xFFFF; foreach (byte b in data) { crc ^= b; for (int i = 0; i < 8; i++) crc = (crc & 1) != 0 ? (ushort)((crc >> 1) ^ 0xA001) : (ushort)(crc >> 1); } return crc; } ///<summary>在帧体末尾补 2 字节 CRC(低、高)。</summary> publicstaticbyte[] Finish(List<byte> body) { ushort crc = Crc16(body.ToArray()); body.Add((byte)(crc & 0xFF)); body.Add((byte)(crc >> 8)); return body.ToArray(); } ///<summary>0x03 读保持寄存器。帧:地址 功能码 寄存器H 寄存器L 数量H 数量L + CRC。</summary> publicstaticbyte[] ReadHolding(byte addr, ushort reg, ushort count) { var f = new List<byte> { addr, (byte)EFuncCode.ReadHolding, (byte)(reg >> 8), (byte)reg, (byte)(count >> 8), (byte)count }; return Finish(f); } ///<summary>0x10 写多个寄存器。帧:地址 功能码 寄存器H 寄存器L 数量H 数量L 字节数 数据... + CRC。</summary> publicstaticbyte[] WriteMultiple(byte addr, ushort reg, paramsushort[] values) { var f = new List<byte> { addr, (byte)EFuncCode.WriteMultiple, (byte)(reg >> 8), (byte)reg, (byte)(values.Length >> 8), (byte)values.Length, (byte)(values.Length * 2) }; foreach (var v in values) { f.Add((byte)(v >> 8)); f.Add((byte)v); } return Finish(f); } ///<summary>0xAA 私有:写命令寄存器(清零)。固定 9 字节数据区,回显 8 字节。</summary> publicstaticbyte[] PrivateCommand(byte addr, ushort reg, byte high = 0, byte low = 0) => Finish(new List<byte> { addr, (byte)EFuncCode.PrivateWriteCmd, (byte)(reg >> 8), (byte)reg, 0x00, 0x01, 0x02, high, low }); ///<summary>0x11 私有:读版本。固定握手码 FE DC BA 98 76 54 32 10,回 15 字节。</summary> publicstaticbyte[] ReadVersion(byte addr) => Finish(new List<byte> { addr, (byte)EFuncCode.PrivateReadVer, 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10 }); ///<summary>0x98 私有:下装计量参数。group 1..4 → 寄存器 0x6400+group;Un 已×10、Ib 已×100。</summary> publicstaticbyte[] DownloadMeterPara(byte addr, intgroup, short un, short ib, short ec) { var reg = (ushort)(0x6400 + group); var f = new List<byte> { addr, (byte)EFuncCode.PrivateDownload, (byte)(reg >> 8), (byte)reg, 0x00, 0x03, 0x06, (byte)(un >> 8), (byte)un, (byte)(ib >> 8), (byte)ib, (byte)(ec >> 8), (byte)ec }; return Finish(f); } ///<summary>0x99 私有:校准。phase=true 用 0x6500 组,否则 0x6400 组;发空数据触发。</summary> publicstaticbyte[] Calibrate(byte addr, bool phase, intgroup) { var reg = (ushort)((phase ? 0x6500 : 0x6400) + group); return Finish(new List<byte> { addr, (byte)EFuncCode.PrivateCalibrate, (byte)(reg >> 8), (byte)reg, 0x00, 0x01, 0x02, 0x00, 0x00 }); } ///<summary>0xAB 私有:写电量块。values 为 4 字节大端原始整型,按 4 字节/个写入。</summary> publicstaticbyte[] WriteEnergyBlock(byte addr, ushort reg, paramsuint[] values) { var f = new List<byte> { addr, (byte)EFuncCode.PrivateWriteBlock, (byte)(reg >> 8), (byte)reg, (byte)(values.Length >> 8), (byte)values.Length, (byte)(values.Length * 4) }; foreach (var v in values) { f.Add((byte)(v >> 24)); f.Add((byte)(v >> 16)); f.Add((byte)(v >> 8)); f.Add((byte)v); } return Finish(f); } ///<summary>按字段定义解析响应:跳过地址/功能码/长度 3 字节头,从数据区首字节起取。</summary> publicstatic Dictionary<string, double> Decode(byte[] response, MeterCmd cmd) { var result = new Dictionary<string, double>(); foreach (var f in cmd.Fields) { long raw = 0; for (int i = 0; i < f.Size; i++) raw = (raw << 8) | response[3 + f.Offset + i]; if (f.Signed && raw > (1L << (f.Size * 8 - 1)) - 1) // 有符号补码修正 raw -= 1L << (f.Size * 8); result[f.Name] = raw / f.Scale; } return result; }}
8.2 指令一 · 读保持寄存器 0x03(含字段表与解析)
读指令靠"命令表"描述寄存器与字段,新增一条只读指令只需往表加一条记录,收发/解析逻辑零改动:///<summary>响应数据区里的一个字段:相对数据区首字节的偏移、长度、缩放、符号、单位。</summary>publicsealedrecordMeterField(string Name, int Offset, int Size, double Scale, bool Signed = false, string Unit = "");///<summary>一条读指令:名称、功能码、寄存器、数量、期望响应长度、字段定义。</summary>publicsealedrecordMeterCmd(string Name, EFuncCode Func, ushort Reg, ushort Count, int RespLen, IReadOnlyList<MeterField> Fields){ publicstatic MeterCmd Read(string name, ushort reg, ushort count, params MeterField[] fields) => new(name, EFuncCode.ReadHolding, reg, count, 5 + count * 2, fields);}public static class MeterCommands{ public static readonly IReadOnlyDictionary<string, MeterCmd> Table = new[] { // 实时量:@0x0061,26 个寄存器,响应数据区 52 字节 MeterCmd.Read("Realtime", 0x0061, 26, new("Ua", 0, 2, 10, false, "V"), new("Ub", 2, 2, 10, false, "V"), new("Uc", 4, 2, 10, false, "V"), new("Ia", 6, 2, 100, false, "A"), new("Ib", 8, 2, 100, false, "A"), new("Ic", 10, 2, 100, false, "A"), new("Pa", 12, 2, 1000, true, "kW"), new("Pb", 14, 2, 1000, true, "kW"), new("Pc", 16, 2, 1000, true, "kW"), new("Pt", 18, 2, 1000, true, "kW"), new("Qa", 20, 2, 1000, true, "kvar"), new("Qb", 22, 2, 1000, true, "kvar"), new("Qc", 24, 2, 1000, true, "kvar"), new("Qt", 26, 2, 1000, true, "kvar"), new("Sa", 28, 2, 1000, false, "kVA"), new("Sb", 30, 2, 1000, false, "kVA"), new("Sc", 32, 2, 1000, false, "kVA"), new("St", 34, 2, 1000, false, "kVA"), new("PFa", 36, 2, 1000, true), new("PFb", 38, 2, 1000, true), new("PFc", 40, 2, 1000, true), new("PFt", 42, 2, 1000, true), new("F", 44, 2, 100, false, "Hz"), new("Uab", 46, 2, 10, false, "V"), new("Ubc", 48, 2, 10, false, "V"), new("Uca", 50, 2, 10, false, "V")), // 电能块:@0x0000,52 个寄存器,每块 4 字节总值 + 8 个 2 字节费率 MeterCmd.Read("Energy", 0x0000, 52, new("EP_T", 0, 4, 100), new("EP_I", 20, 4, 100), new("EP_E", 40, 4, 100), new("EQ_T", 60, 4, 100), new("EQ_I", 80, 4, 100), new("EQ_E", 100, 4, 100)), }.ToDictionary(c => c.Name, c => c);}
读取动作(位于业务层MeterLink,见 8.9)就是"查表 → 组帧 → 收发 → 解析"四步:public Dictionary<string, double> Read(string cmdName){ var cmd = MeterCommands.Table[cmdName]; byte[] frame = ModbusRtu.ReadHolding(Address, cmd.Reg, cmd.Count); byte[] resp = Transact(frame, cmd.RespLen); // 带重试与 CRC 校验 return ModbusRtu.Decode(resp, cmd); // 换算成带单位的物理量}
8.3 指令二 · 写多个寄存器 0x10
写参数走0x10。下面以"设置运行控制字(@0x0091,1 个寄存器)"为例,演示把界面上的若干选项重新按位拼回一个ushort再下发:// 界面选项 → 寄存器值ushortBuildRunWord(bool threePhase3L, bool singleRate, bool reverseFlow, bool refVector, bool rmsAlg){ int high = (threePhase3L ? 1 : 0) | (singleRate ? 2 : 0); int low = (reverseFlow ? 1 : 0) | (refVector ? 2 : 0) | (rmsAlg ? 8 : 0); return (ushort)((high << 8) | low);}// 下发并回显ushort word = BuildRunWord(...);byte[] frame = ModbusRtu.WriteMultiple(Address, 0x0091, word);bool ok = Transact(frame, 8).Length == 8; // 0x10 正确回显固定 8 字节
校时(@0x003C)则是把DateTime拆成 3 个ushort再写:publicboolWriteTime(DateTime t){ var values = new ushort[] { (ushort)((t.Second << 8) | t.Minute), // 秒(高)分(低) (ushort)((t.Hour << 8) | t.Day), // 时(高)日(低) (ushort)((t.Month << 8) | (t.Year - 2000)) // 月(高)年(低) }; return Transact(ModbusRtu.WriteMultiple(Address, 0x003C, values), 8).Length == 8;}
8.4 指令三 · 清零 0xAA(写命令寄存器)
清零不是任意写,而是写特定的命令寄存器值。先用枚举固定这些"魔法数":///<summary>0xAA 功能码下可用的命令寄存器。</summary>public enum MeterCmdReg : ushort{ ClearEnergy = 0x0001, // 电能清零 ClearHistoryEnergy = 0x0002, // 历史电能清零 ClearDemand = 0x0003, // 当前需量清零 ClearHistoryDemand = 0x0005, // 历史需量清零 ClearAll = 0x6406 // 全部清零}publicboolClear(MeterCmdReg reg) => Transact(ModbusRtu.PrivateCommand(Address, (ushort)reg), 8).Length == 8;publicboolClearEnergy() => Clear(MeterCmdReg.ClearEnergy);
8.5 指令四 · 写电量块 0xAB
用于直接把电量值写回仪表。注意入参是原始 4 字节大端整型(即"真实值 × 小数位"后的整数),不是物理量:// 把物理量电量(kWh)按小数位转成原始 uint 再下发publicboolWriteEnergy(ushort reg, double kwh, int scale){ uint raw = (uint)Math.Round(kwh * scale); // 例如 scale=100 → 123.45kWh → 12345 return Transact(ModbusRtu.WriteEnergyBlock(Address, reg, raw), 8).Length == 8;}// 调用示例:写正向有功(能力协商决定用新版 0xE212 还是旧版 0x000A)WriteEnergy(caps.EnergyWriteReg, 123.45, 100);
8.6 指令五 · 读版本 0x11(能力协商)
连接后第一步读版本,把"软件识别码 + 版本"算成能力开关,后续指令据此选择寄存器与小数位,避免散落各处的字面量判断:///<summary>连上后一次性算出的能力开关。</summary>publicsealedrecordMeterCaps(ushort SoftKey, byte Version){ public bool SupportsRate5to8 => SoftKey == 1766 && Version > 105; // 支持 5~8 费率 public ushort EnergyWriteReg => SupportsRate5to8 ? (ushort)0xE212 : (ushort)0x000A; public int EnergyWriteCount => SupportsRate5to8 ? 9 : 5;}public MeterCaps ReadVersion(){ var r = Transact(ModbusRtu.ReadVersion(Address), 15); // 响应:r[3..4]=软件识别码,r[7]=版本高位,r[8]=版本低位 return new MeterCaps((ushort)((r[3] << 8) | r[4]), r[8]);}
8.7 指令六 · 下装计量参数 0x98
校准前必须先下装量程(额定电压/电流/脉冲常数)。直接接入与互感器接入的取值不同,由接法决定:public enum MeterMode { Direct = 0, Ct = 1 } // 直接接入 / 互感器接入///<summary>按接法给出 0x98 要下发的原始值:Un=V×10,Ib=A×100,Ec 原值。</summary>publicstatic (short Un, short Ib, short Ec) InitParams(MeterMode mode, double unVolts = 220) => mode == MeterMode.Direct ? ((short)Math.Round(unVolts * 10), (short)100, (short)400) // 1A / 400 : ((short)Math.Round(unVolts * 10), (short)1000, (short)10000); // 10A / 10000publicboolInitialize(MeterMode mode, double unVolts = 220){ var (un, ib, ec) = InitParams(mode, unVolts); return Transact(ModbusRtu.DownloadMeterPara(Address, 1, un, ib, ec), 8).Length == 8;}
8.8 指令七 · 校准 0x99
增益与相位校准各发一次0x99,必须按序,且0x98之后至少等待 3 秒:///<summary>第 2 步:增益校准(标准源 1.0Un / 1.0Ib / PF=1.0)。</summary>publicboolCalibrateGain(intgroup = 1) => Transact(ModbusRtu.Calibrate(Address, phase: false, group), 8).Length == 8;///<summary>第 3 步:相位校准(标准源切 PF=0.5L 后发)。</summary>publicboolCalibratePhase(intgroup = 1) => Transact(ModbusRtu.Calibrate(Address, phase: true, group), 8).Length == 8;
8.9 业务编排:MeterLink 全貌
MeterLink把上面所有指令收口到一个类,对外只暴露语义方法;Transact统一负责"发→收→CRC 校验→失败重试":///<summary>把"命令 + 收发 + 校验"包成一步,调用方只拿到可信数据或明确异常。</summary>public sealed class MeterLink{ private readonly IByteTransport _io; public byte Address { get; set; } public int Retries { get; set; } = 2; publicMeterLink(IByteTransport transport, byte address = 1) { _io = transport; Address = address; } ///<summary>核心收发:校验失败自动重发,超过 Retries 抛 TimeoutException。</summary> publicbyte[] Transact(byte[] request, int expectedLen) { for (int attempt = 0; ; attempt++) { _io.Write(request); var resp = new byte[expectedLen]; int n = _io.Read(resp, expectedLen); bool crcOk = n == expectedLen && ModbusRtu.Crc16(resp.AsSpan(0, expectedLen - 2)) == (ushort)(resp[expectedLen - 2] | (resp[expectedLen - 1] << 8)); if (crcOk) return resp; if (attempt >= Retries) throw new TimeoutException("设备无响应或校验错误"); } } // —— 读系列 —— public Dictionary<string, double> Read(string cmdName) { var cmd = MeterCommands.Table[cmdName]; return ModbusRtu.Decode(Transact(ModbusRtu.ReadHolding(Address, cmd.Reg, cmd.Count), cmd.RespLen), cmd); } public Dictionary<string, double> ReadRealtime() => Read("Realtime"); public Dictionary<string, double> ReadEnergy() => Read("Energy"); // —— 写系列 —— publicboolWrite(ushort reg, paramsushort[] values) => Transact(ModbusRtu.WriteMultiple(Address, reg, values), 8).Length == 8; publicboolWriteTime(DateTime t) { /* 见 8.3 */ return false; } // —— 清零 / 版本 / 校准 / 下装 —— publicboolClear(MeterCmdReg reg) => Transact(ModbusRtu.PrivateCommand(Address, (ushort)reg), 8).Length == 8; public MeterCaps ReadVersion() { /* 见 8.6 */ return default; } publicboolInitialize(MeterMode mode, double un = 220) { /* 见 8.7 */ return false; } publicboolCalibrateGain(int g = 1) => Transact(ModbusRtu.Calibrate(Address, false, g), 8).Length == 8; publicboolCalibratePhase(int g = 1) => Transact(ModbusRtu.Calibrate(Address, true, g), 8).Length == 8; publicboolWriteEnergy(ushort reg, paramsuint[] values) => Transact(ModbusRtu.WriteEnergyBlock(Address, reg, values), 8).Length == 8;}
调用顺序示例(校准流程):Initialize(接法) → 等待 ≥3s → CalibrateGain() → CalibratePhase()。任何一步返回 false 或抛异常都应立即停止并提示。