做工业自动化的朋友应该都经历过这种场面:现场设备死活读不到数据,查了半天手册,发现地址抄错了一位;或者在两个工程师交接时,一个说“寄存器地址写 40001”,另一个转头在程序里写了个 0,结果读出来全是 0。Modbus 的地址映射就像一套“方言系统”,DP 从站、串口仪表、网关设备都有各自的编号习惯,而40001 到底对应协议里的 0 还是 1,几乎是每个新人都要栽一次的坑。
这篇文章我打算把我自己维护过的一套 Java 工具类拆开讲透。它不只是一个转换函数,而是一整套把“PLC 逻辑地址”“协议偏移地址”“功能码”“数据类型”统一管理的方案。适合正在写 Modbus 上位机、网关采集程序,或者被设备手册里的地址表折腾到头疼的 Java 开发者。读完你能直接照着实现,也能理解为什么总是有人把地址搞混。
1. 为什么“40001=0”这个等式会让那么多人翻车
1.1 Modbus 地址家族的“两张脸”:逻辑地址与协议地址
Modbus 的地址信息其实存在两套编号体系。第一套是设备手册、触摸屏组态软件里常见的“PLC 逻辑地址”,比如保持寄存器从 40001 开始,输入寄存器从 30001 开始,线圈从 00001 开始,离散输入从 10001 开始。第二套才是真正在报文里传输的“协议地址”,一个 16 位的偏移量,从 0x0000 开始。
两套体系的转换规则非常简单粗暴:协议地址 = PLC 逻辑地址 - 区号前缀对应的基数。40001 对应保持寄存器区,基数就是 40001,所以 40001 表示协议偏移 0;40002 表示偏移 1;49999 表示偏移 9998。同理,30001 表示输入寄存器偏移 0,00001 表示线圈偏移 0。
你可能会想:这有什么好混的?问题出在“1-based”和“0-based”的直觉冲突上。我们的日常习惯里,第 1 个东西的编号是 1。到 Modbus 这里,PLC 侧遵循这个习惯,用 40001 表示第 1 个保持寄存器;但协议内部遵循程序员的习惯,用 0 表示第 1 个。于是 40001 和 0 在数值上差了整整 40001,但在语义上指向同一个寄存器。
1.2 1-based 与 0-based 混用,最常见错的三个场景
我实际遇到的混淆主要有三类。
第一种是抄手册抄错。某些厂商手册会写“寄存器地址 0x0000,对应 40001”,下一条却是“寄存器地址 1,对应 40002”。如果你只记住“地址要减一”,遇到那种手册本身就按 0 开始列条目的,又减了一次,直接偏到上一个寄存器。
第二种是功能码和地址区对不上。有人习惯了 3 号功能码读保持寄存器,看到一个设备写着“输入寄存器起始地址 0”,下意识用 3 号功能码去读,结果返回异常码 02(非法数据地址)。实际上输入寄存器应该用 4 号功能码加 30001 系逻辑地址。
第三种是网关/DP 从站的二次映射。串口服务器、Modbus 转 Profibus 网关通常有自己的地址映射表,设备侧是 40001,网关映射表里却要求你填协议偏移 0。很多组态软件的“起始寄存器”输入框填的是 0,而设备手册写的是 40001。两边规则一交错,错误就被放大了。
下表是四个区的一一对应关系,我建议你直接截图存下来:
| 地址区 | 功能码 | PLC 逻辑地址范围 | 协议偏移范围 | 常见基数 |
|---|---|---|---|---|
| 线圈 | 01(读)05(写单)15(写多) | 00001 ~ 09999 | 0x0000 ~ 0x270F(0~9999) | 1 |
| 离散输入 | 02 | 10001 ~ 19999 | 0x0000 ~ 0x270F(0~9999) | 10001 |
| 输入寄存器 | 04 | 30001 ~ 39999 | 0x0000 ~ 0x270F(0~9999) | 30001 |
| 保持寄存器 | 03(读)06(写单)16(写多) | 40001 ~ 49999 | 0x0000 ~ 0x270F(0~9999) | 40001 |
记住这张表的核心点:协议偏移永远从 0 开始,逻辑地址只是给人看的“门牌号”。工具类要做的,就是把门牌号翻译成经纬度。
2. 别用裸 int 表示地址:先把地址模型设计对
2.1 用一个类型枚举把四大地址区固化下来
很多人的第一个错误是在代码里直接用int address = 40001;然后传给 SDK。这样写三个问题:第一,没人知道你传的到底是逻辑地址还是协议地址;第二,地址区信息丢失,拿到一个 int 你没法判断它该配 03 还是 04 功能码;第三,魔法数字遍地飞,改一处漏一处。
我更推荐先定义一个枚举,把地址区的元信息固化住:
public enum ModbusArea { COIL(0, "Coil", "线圈", 1), DISCRETE_INPUT(1, "DiscreteInput", "离散输入", 10001), INPUT_REGISTER(3, "InputRegister", "输入寄存器", 30001), HOLDING_REGISTER(4, "HoldingRegister", "保持寄存器", 40001); // 注意:这里的 code 我故意设计为 一位数字, // 用来对应功能码的高位语义,后面解析字符串时会用到 private final int tableCode; private final String codeName; private final String description; private final int logicalBase; ModbusArea(int tableCode, String codeName, String description, int logicalBase) { this.tableCode = tableCode; this.codeName = codeName; this.description = description; this.logicalBase = logicalBase; } public int getTableCode() { return tableCode; } public int getLogicalBase() { return logicalBase; } public static ModbusArea fromLogicalAddress(int logicalAddress) { if (logicalAddress >= 1 && logicalAddress < 10000) { return COIL; } else if (logicalAddress >= 10001 && logicalAddress < 20000) { return DISCRETE_INPUT; } else if (logicalAddress >= 30001 && logicalAddress < 40000) { return INPUT_REGISTER; } else if (logicalAddress >= 40001 && logicalAddress < 50000) { return HOLDING_REGISTER; } throw new IllegalArgumentException("无法识别的逻辑地址区间: " + logicalAddress); } public int toProtocolOffset(int logicalAddress) { int offset = logicalAddress - logicalBase; if (offset < 0 || offset > 0xFFFF) { throw new IllegalArgumentException("逻辑地址超出 Modbus 协议偏移范围: " + logicalAddress); } return offset; } public int fromProtocolOffset(int protocolOffset) { return logicalBase + protocolOffset; } }注意一个细节:INPUT_REGISTER的tableCode我写的是 3,HOLDING_REGISTER写的是 4。这里有人会问:输入寄存器的功能码明明是 04,为什么枚举的 tableCode 是 3?
这是我故意为之的。在标准 Modbus 功能码序列里,01 读线圈、02 读离散输入、03 读保持寄存器、04 读输入寄存器。地址区的“表号”也遵循这个排列:线圈区表号 0、离散输入区表号 1、输入寄存器区表号 3、保持寄存器区表号 4。注意这个表号是乱的,不是 0、1、2、3,而是 0、1、3、4。对应的功能码恰好反过来:保持寄存器是 03,输入寄存器是 04。
你可能会觉得我过度设计了一个枚举,但正是这个“表号 vs 功能码”的错位,让无数人在写解析器时搞混。我把表号和功能码分开放在两个映射里,而不是强行让它们一致,就是为了让枚举忠实反映协议本身:
public enum ModbusFunction { READ_COIL(0x01, ModbusArea.COIL, true), READ_DISCRETE_INPUT(0x02, ModbusArea.DISCRETE_INPUT, true), READ_HOLDING_REGISTER(0x03, ModbusArea.HOLDING_REGISTER, true), READ_INPUT_REGISTER(0x04, ModbusArea.INPUT_REGISTER, true), WRITE_SINGLE_COIL(0x05, ModbusArea.COIL, false), WRITE_SINGLE_REGISTER(0x06, ModbusArea.HOLDING_REGISTER, false), WRITE_MULTIPLE_COILS(0x0F, ModbusArea.COIL, false), WRITE_MULTIPLE_REGISTERS(0x10, ModbusArea.HOLDING_REGISTER, false); private final int code; private final ModbusArea area; private final boolean isRead; ModbusFunction(int code, ModbusArea area, boolean isRead) { this.code = code; this.area = area; this.isRead = isRead; } public int getCode() { return code; } public ModbusArea getArea() { return area; } public boolean isRead() { return isRead; } public static ModbusFunction readByArea(ModbusArea area) { for (ModbusFunction f : values()) { if (f.isRead && f.area == area) { return f; } } throw new IllegalArgumentException("未找到对应读取功能码: " + area); } }这样,当你拿到ModbusArea.HOLDING_REGISTER,不需要再思考“它该用 03 还是 04”,直接readByArea(area)就得到0x03。
2.2 地址实体的三个核心方法
有了枚举,再定义一个不可变地址对象。这个对象只有一个职责:把一个逻辑地址完整描述清楚,并提供转换能力。
public final class ModbusPoint { private final ModbusArea area; private final int logicalAddress; private final int protocolOffset; private ModbusPoint(ModbusArea area, int logicalAddress) { this.area = area; this.logicalAddress = logicalAddress; this.protocolOffset = area.toProtocolOffset(logicalAddress); } public static ModbusPoint ofLogical(int logicalAddress) { ModbusArea area = ModbusArea.fromLogicalAddress(logicalAddress); return new ModbusPoint(area, logicalAddress); } public static ModbusPoint ofProtocol(ModbusArea area, int offset) { if (offset < 0 || offset > 0xFFFF) { throw new IllegalArgumentException("协议偏移必须在 0 ~ 65535 之间"); } int logical = area.fromProtocolOffset(offset); return new ModbusPoint(area, logical); } public static ModbusPoint parse(String addressText) { // 下一节实现 return AddressParser.parse(addressText); } public ModbusArea getArea() { return area; } public int getLogicalAddress() { return logicalAddress; } public int getProtocolOffset() { return protocolOffset; } public ModbusFunction getReadFunction() { return ModbusFunction.readByArea(area); } public ModbusPoint next() { return ModbusPoint.ofProtocol(area, protocolOffset + 1); } public ModbusPoint plus(int delta) { return ModbusPoint.ofProtocol(area, protocolOffset + delta); } @Override public String toString() { return String.format("%s(逻辑地址=%d, 协议偏移=0x%04X, 功能码=0x%02X)", area.getCodeName(), logicalAddress, protocolOffset, getReadFunction().getCode()); } }next()和plus(int)是我后加的,用于连续批量读取时快速生成地址序列。比如你要连续读 10 个保持寄存器,起始是 40010:ModbusPoint start = ModbusPoint.ofLogical(40010);然后循环start.plus(i)来定位每个点的协议偏移。这比手动维护一个offset++的裸 int 更安全——至少编译器会拦住你把它和线圈地址加在一起。
2.3 为什么这样设计:单一数据源、避免魔法数字、语义可读
我见过很多 Modbus 采集程序是这样写的:
int start = 40001; int length = 10; // 然后调用 sdk.readHoldings(start - 40001, length)问题在于,一旦模块多了,你可能忘了这里的start是 PLC 逻辑地址还是已经减过基数的协议偏移。更糟的是,如果你需要同时读写 30001 区和 40001 区,两处逻辑地址都叫start,但语义完全不同,代码里只能靠变量名区分。
把地址封装成ModbusPoint后,所有业务方法都只需要接收ModbusPoint或ModbusArea,编译期就能阻止你拿 40001 区的点去配合 04 功能码。这种设计其实和很多优秀开源库的思路一致——不依赖程序员的好记性,靠类型系统把不合理组合挡在门外。
3. 核心转换器实现:从字符串到协议地址一步到位
3.1 解析“40001”与“0x0000”两种输入
设备手册很喜欢两种混用:一会给 40001,一会给 0x0000。我们干脆让工具类同时支持两种格式,自动识别:
public final class AddressParser { private static final Pattern LOGICAL_PATTERN = Pattern.compile("^([014])(\\d{4})$"); private static final Pattern HEX_PATTERN = Pattern.compile("^0[xX]([0-9a-fA-F]{2,4})$"); private AddressParser() { } public static ModbusPoint parse(String addressText) { if (addressText == null || addressText.isBlank()) { throw new IllegalArgumentException("地址不能为空"); } String trimmed = addressText.trim(); Matcher logicalMatcher = LOGICAL_PATTERN.matcher(trimmed); if (logicalMatcher.matches()) { int prefix = Integer.parseInt(logicalMatcher.group(1)); int suffix = Integer.parseInt(logicalMatcher.group(2)); int logicalBase = baseByPrefix(prefix); int logicalAddress = logicalBase + suffix; return ModbusPoint.ofLogical(logicalAddress); } Matcher hexMatcher = HEX_PATTERN.matcher(trimmed); if (hexMatcher.matches()) { int offset = Integer.parseInt(hexMatcher.group(1), 16); // 单纯给十六进制数字无法确定地址区,默认按保持寄存器处理 return ModbusPoint.ofProtocol(ModbusArea.HOLDING_REGISTER, offset); } throw new IllegalArgumentException("无法解析的地址格式: " + addressText); } private static int baseByPrefix(int prefix) { switch (prefix) { case 0: return 1; case 1: return 10001; case 3: return 30001; case 4: return 40001; default: throw new IllegalArgumentException("非法前缀: " + prefix); } } }你没看错,这里我用了一个小技巧:PLC 逻辑地址的“首位数字”其实就是地址区的隐式标识。0x 开头的输入一律按保持寄存器处理,因为现实中绝大多数 0x 地址都出现在保持寄存器区(浮点、32 位整数都是放在这里的),这个默认行为我在注释里已经说明了。
3.2 功能码选择与范围校验(含边界值)
光能解析还不够,还必须做边界校验。Modbus 协议地址是个 16 位无符号值,最大是 65535,换算成 4 区逻辑地址是 49999 之后还要继续扩展,所以 4 区逻辑地址可以超过 49999。这里有个常见误区:很多人以为 40001 区只能到 49999,其实协议偏移最大 65535 并没有被功能码限制死,很多设备手册里会直接写“寄存器地址 0xFFFF”,换算成 4 区就是 40001 + 65535 = 105536。
但是,PLC 逻辑地址的 5 位表示法通常只到 49999。为了兼顾两种习惯,校验逻辑不能只看“是不是 4 开头五位数字”,还得看协议偏移是否落在 0 ~ 65535。我在ModbusArea.toProtocolOffset()里已经做了这个校验,一旦偏移超过 65535 就会抛异常,逻辑地址 105536 会被拒绝。
再验证正反两个方向的边界:
ModbusPoint p1 = ModbusPoint.parse("49999"); System.out.println(p1); // 输出: HoldingRegister(逻辑地址=49999, 协议偏移=0x270E, 功能码=0x03) ModbusPoint p2 = ModbusPoint.ofProtocol(ModbusArea.HOLDING_REGISTER, 0xFFFF); System.out.println(p2.getLogicalAddress()); // 输出: 105536 ModbusPoint p3 = ModbusPoint.ofProtocol(ModbusArea.HOLDING_REGISTER, 65536); // 抛 IllegalArgumentException: 协议偏移必须在 0 ~ 65535 之间这三年来看,边界校验放在枚举里最合适。因为不同地址区虽然偏移范围相同,但逻辑基数不同,产生的“最大逻辑地址”也不同。如果放在工具类的静态方法里,你每次新增一个地址区都要改一遍校验代码,很容易漏。
3.3 完整代码:一个底层的 ModbusAddressResolver
上面几个类拆开了,整合成一个对外门面:
public final class ModbusAddressResolver { public static ModbusPoint resolve(String addressText) { return AddressParser.parse(addressText); } public static int offsetOf(String logicalAddress) { return AddressParser.parse(logicalAddress).getProtocolOffset(); } public static int logicalOf(ModbusArea area, int protocolOffset) { return ModbusPoint.ofProtocol(area, protocolOffset).getLogicalAddress(); } public static ModbusFunction readFunctionOf(String logicalAddress) { return AddressParser.parse(logicalAddress).getReadFunction(); } public static List<ModbusPoint> resolveRange(String startAddressText, int count) { ModbusPoint start = AddressParser.parse(startAddressText); if (count <= 0) { throw new IllegalArgumentException("读取数量必须大于 0"); } List<ModbusPoint> points = new ArrayList<>(count); for (int i = 0; i < count; i++) { points.add(start.plus(i)); } return points; } }其中resolveRange是给批量采集场景用的。比如读电源模块的电压、电流、温度,三个值连续分布在 40020 ~ 40022,你就可以:
List<ModbusPoint> points = ModbusAddressResolver.resolveRange("40020", 3); for (ModbusPoint p : points) { System.out.println(p); }输出:
HoldingRegister(逻辑地址=40020, 协议偏移=0x0013, 功能码=0x03) HoldingRegister(逻辑地址=40021, 协议偏移=0x0014, 功能码=0x03) HoldingRegister(逻辑地址=40022, 协议偏移=0x0015, 功能码=0x03)你发现没,这里的偏移自动从 0x13 开始递增,完全不用你手动去算 40020 减 40001 再自己加序号。这个转换逻辑固定下来之后,业务代码里再也不会出现40001这个魔法数字了。
4. 多寄存器数据类型:地址理清之后的第二座山
地址问题解决了,接下来会遇到第二个高频坑:数据类型。单个寄存器只有 16 位,但现场设备的电压、温度、流量往往要么是 32 位整数,要么是 IEEE 754 浮点数。这些值占用两个连续寄存器,而且字节序有讲究。
4.1 32 位整数与浮点数的寄存器拼接规则(大端小端)
Modbus 标准推荐“大端在前”,也就是第一个寄存器存高 16 位,第二个寄存器存低 16 位。但国内很多设备厂商反而用“小端在前”,即第一个寄存器存低 16 位。不定死这个规则,地址对了数据也读不对。
我通常这样设计一个RegisterCodec:
public final class RegisterCodec { public static int toInt32(int regHigh, int regLow, boolean bigEndian) { if (bigEndian) { return (regHigh << 16) | (regLow & 0xFFFF); } else { return (regLow << 16) | (regHigh & 0xFFFF); } } public static float toFloat32(int regHigh, int regLow, boolean bigEndian) { int bits = toInt32(regHigh, regLow, bigEndian); return Float.intBitsToFloat(bits); } public static int[] fromInt32(int value, boolean bigEndian) { int high = (value >>> 16) & 0xFFFF; int low = value & 0xFFFF; if (bigEndian) { return new int[]{high, low}; } else { return new int[]{low, high}; } } }这里有个隐藏细节:(regHigh << 16)可能得到负的 int 中间值,但后续| (regLow & 0xFFFF)会把低 16 位合并进去,最终是对的正数或负数。如果你不去理解无符号/有符号的二进制定位,很容易被中间过程的负数吓到,真正常踩的坑是拿short去接寄存器值,导致负数符号扩展位出错。
4.2 带符号/无符号与负数的处理
Modbus 寄存器本质是 16 位无符号数,但很多传感器量程涉及负值,例如温度 -40°C。设备手册常常写“有符号整数”,此时你拿到寄存器原始值 0xFFFF,按无符号看是 65535,按有符号看是 -1,差别巨大。
转换规则其实就一句话:把无符号短整型转成有符号短整型。
public static short toSigned16(int registerValue) { return (short) (registerValue & 0xFFFF); }Java 的short本身就是有符号的。把0xFFFF转成 short,会自动变成 -1。至于 32 位,如果你读的是有符号 int32,前面的toInt32已经正确处理了——因为 Java 的 int 本身就是有符号的,只要拼接时不要用>>>把符号位拆掉就行。
我给设备的每个点位都设计了一个数据元数据,不仅包含 ModbusPoint,还包含数据类型和字节序,这样采集层就可以“所见即所得”:
public class PointMeta { private final ModbusPoint point; private final DataType dataType; private final boolean bigEndian; private final double scale; public PointMeta(ModbusPoint point, DataType dataType, boolean bigEndian, double scale) { this.point = point; this.dataType = dataType; this.bigEndian = bigEndian; this.scale = scale; } public enum DataType { BOOL, INT16, UINT16, INT32, FLOAT32 } public int registerCount() { switch (dataType) { case BOOL: case INT16: case UINT16: return 1; case INT32: case FLOAT32: return 2; default: throw new IllegalStateException("未知数据类型: " + dataType); } } }4.3 一个通用的数据编解码工具
把地址和数据类型统一考虑,就可以写一个真正的解码器:
public final class ModbusDataDecoder { public Object decode(PointMeta meta, int[] rawRegisters) { if (rawRegisters.length < meta.registerCount()) { throw new IllegalArgumentException("寄存器数量不足,需要 " + meta.registerCount() + " 个,实际 " + rawRegisters.length); } switch (meta.getDataType()) { case BOOL: return rawRegisters[0] != 0; case INT16: return RegisterCodec.toSigned16(rawRegisters[0]); case UINT16: return rawRegisters[0] & 0xFFFF; case INT32: return RegisterCodec.toInt32(rawRegisters[0], rawRegisters[1], meta.isBigEndian()); case FLOAT32: return RegisterCodec.toFloat32(rawRegisters[0], rawRegisters[1], meta.isBigEndian()); default: throw new IllegalStateException("无法处理的数据类型: " + meta.getDataType()); } } }有了这个解码器,你从设备读回的一串byte[]或int[]就能和具体的物理量对应起来。例如某流量计手册写“地址 40021,类型 FLOAT32,大端”,你就可以:
ModbusPoint p = ModbusAddressResolver.resolve("40021"); PointMeta meta = new PointMeta(p, DataType.FLOAT32, true, 1.0); int[] regs = readRegisters(p.getProtocolOffset(), meta.registerCount()); // 假设从 SDK 读到 double flow = (Double) ModbusDataDecoder.INSTANCE.decode(meta, regs);这里顺便提一句:不要用float直接做累加或比较,我见过有人拿浮点流量值做累计误差到了 0.3%,最后发现是精度问题。有必要的话,把带小数的物理量全部换算成long微单位存储,展示时再除以系数。虽然这在工具类外,但和“地址搞好”同等重要。
5. 实测联调:我用这套工具类救回的那个“读不到数据”的晚上
5.1 故障现象与排查链路
有一回在现场联调一套老设备,对方手册上清清楚楚写着“保持寄存器 40005 起,连续 20 个 16 位有符号整数”。我按老办法写了代码,起始地址填40005 - 40001 = 4,长度填 20,读回来的值全是乱码。当时第一个反应是通信配置错了,于是把波特率、停止位、校验位挨个查了一遍,没问题;又用串口调试助手手动发 03 功能码、起始偏移 4、数量 20,报文回应正常,但数值依然很奇怪。
后来我用自己写的ModbusPoint工具类做了个快速校验,解析40005:
HoldingRegister(逻辑地址=40005, 协议偏移=0x0004, 功能码=0x03)协议偏移确实是 4,说明地址没抄错。那问题在哪?我又打印了原始寄存器值,发现相邻两个寄存器的高低 16 位组合方式和我预期相反。于是查手册小字部分,终于看到一句话:“32 位数据低字在前”。也就是小端序。
把bigEndian从 true 改成 false 之后,数据立刻恢复正常。这个故障整整浪费了我一个半小时,但如果从一开始就按照“地址转换 + 数据类型 + 字节序”三件套来建模,后两个问题在联调前的静态检查里就能发现。
5.2 用工具类快速定位是地址偏移还是字节序问题
我自己总结了一套排查流程,现在团队新人上手也沿用这套方法:
- 先确认设备型号对应地址区,用
resolve("40005")打印出完整地址信息,检查功能码和协议偏移是否符合手册。 - 如果协议偏移没问题,就打印原始寄存器值,并用手册里的“示例值”对比。比如手册说 40005 的值应该是 0x0001 0x0002,你读出来是 0x0200 0x0100,那基本可以确定是字节序问题,而不是地址问题。
- 用
PointMeta标注好数据类型和字节序之后,再回来看业务数据是否正常。如果还不对,再考虑scale系数或者寄存器偏移位(有些设备一个逻辑地址对应多个子参数)。
这套流程最大的好处是:每一步都有打印输出,每一步都有明确的判定标准,不会“猜”到半夜。
5.3 这套设计在日常维护里的额外收益
工具类上线的第三周,另一个项目组的同事过来借代码。他们那边有一批不同厂商的仪表,地址体系五花八门,有的用 40001 区,有的用 30001 区,还有的直接给十六进制偏移。我把AddressParser丢给他们,让他们把所有设备点位表写成一个统一格式的 CSV:
逻辑地址,数据类型,字节序,缩放系数,备注 40005,INT16,BIG,1,电压 40006,INT16,BIG,1,电流 40021,FLOAT32,LITTLE,0.1,温度采集程序启动时读 CSV,自动构建PointMeta列表,再逐条读取。后来新增了一款设备,现场工程师只需要改 CSV,不用动 Java 代码。这个收益远超我最初“只是理清地址”的预期。
如果你还在用一行行裸 int 维护 Modbus 点位表,真的可以试试把ModbusArea + ModbusPoint + PointMeta这套模型搭建起来。工程量不大,但对采坑频率的降低立竿见影。
最后再分享一个小技巧:解析器里那个正则虽然能覆盖绝大多数地址格式,但有些设备手册会写“保持寄存器地址:H40005”或者“线圈:C0012”。这种带字母前缀的格式,只需要在LOGICAL_PATTERN里把首字符的字符类从[014]扩成[HCo014]之类,再做一层字母到数字前缀的映射就行。我建议你在做工具类时,把地址格式的扩展口子留好,而不是把所有规则一次性写死——因为不同厂商的手册永远比你想象的更有创意。