把外购液位仪表接入自有总线(RS485 / Modbus 变体)
主控侧是 GD32F303VC(Cortex-M4)+ FreeRTOS,总线上除了这块仪表还挂着面板、 读卡器和我自己做的采集小板。仪表这一侧的适配工作单独放在一个模块里, 对外只暴露“一份结构化快照”和几个写接口。做下来我的结论有三条: 第一,第三方仪表的成本不在通信代码,而在把它当成一个不可靠的外部依赖来设计; 第二,协议的文档和实物行为可能不一致,必须用实测报文核对; 第三,零点迁移、量程上下限这类“改了就回不去”的参数,必须有自己的范围保护与二次确认, 不能指望对方替你挡住。
一、为什么要接第三方仪表,而不是自己做传感器
一开始我是想自己做的:一个液位探头加一路 ADC,换算成高度,多简单。 真正动手才发现,我要的其实不是“液位”,而是体积和一组可报警的位置值。 从液位到体积这一步,需要知道罐型、罐体半径与长度、两端封头的形状; 再往上还要做温度补偿、多个位置的上下限报警、正反向判断。 这些已经不是“读一个模拟量”的事,而是仪表行业的专业范围:要结构、要标定, 某些场合还要计量认证。这些我在个人条件下做不到,也不该硬做。
所以这块仪表是外购的。但买回来的不是一个“功能”,而是一份契约: 它的接口是固定的、协议是它自己定义的、行为不保证与文档完全一致。 我的工作因此从“做传感器”变成了“把它的契约翻译进我的系统”, 具体就是三件事:一次读回一份完整的快照、把写操作按风险分级管起来、 以及在它不回话的时候让系统知道该怎么办。
这条链路接进主控之后,主控那一侧看到的只是一份结构化的数据(当前体积、多路位置值、多路温度、 状态字、报警上下限)和几个写接口,完全不用知道对面是哪一家的表、用什么功能码。 这一层隔离是我在这件事上做对的地方:把“不可靠”关在适配层的门里, 业务代码只管读快照。
我一开始的预期是“读写寄存器而已,两天搞定”。两天确实把读通了, 但真正让我改了设计的是后面两件事:一次把某个参数写进去之后,我发现原值已经不在了; 以及按文档解析出来的几个字段,实测值明显不合理。 接第三方设备,难点从来不是写代码。
二、先摸清它的协议:地址、功能码集合、读写方式
面对一块外购仪表,我给自己定了一套固定的摸底顺序,做完这五步才允许动代码:
- 把文档当线索,不当事实。先从通信说明里抄出三样东西:从站地址范围、功能码集合、寄存器清单;
- 总线上只留它一块表。把其他设备断开,避免自己的轮询报文混进来干扰判断;
- 用串口助手逐条发最小报文,把每一条的应答原样存下来,不急着写解析代码;
- 逐字段与文档比对,并把结论分成两类:实测确认过的、只在文档里见过的;
- 形成一份自己的字段表,从此以后代码只认这张表,不再去翻对方的文档。
第 4 步里有个小技巧我一直在用:文档里写“保留”“备用”的字段,实测时也要读回来看看它到底是什么。 这类字段往往是对方没写进文档的功能,或者是上一个固件版本留下的痕迹; 至少要确认它不会因为我的写操作而改变。
地址:改地址本身是一件危险的事
这块表出厂带一个默认的短地址(本文不列出具体值),也支持通过写事件改地址。 这里有个很容易被忽略的风险:改地址这个动作一旦生效,你就按旧地址找不到它了。 如果新地址写错了、或者写进去之后没保存成功,下一次上电总线上就少了一个从站, 而现场没有任何提示。所以我后来把“改地址”单独归到最高风险的一类里, 写之前要先记住旧地址、写之后要按新地址回读确认、确认不到就按旧地址再找一次。
顺带说一句地址规划:我这条总线上挂了不止一种设备,所以从一开始就维护了一张设备地址表 (哪一类设备用哪一段地址)。这不是为了好看——地址不冲突是总线上唯一不能商量的事, 它不像帧长或超时那样可以事后调。
功能码集合:标准两个,其余是非标准的
它的功能码集合可以分成三类,我把它们的性质列在下面这张表里。 具体数值属于对方的实现约定,本文不列出:
| 类别 | 性质 | 我因此要注意什么 |
|---|---|---|
| 读保持寄存器 | 标准 Modbus 功能码,语义与规范一致 | 可以用通用调试工具直接发;这是我做摸底时的主力命令 |
| 写单个寄存器 | 标准功能码的编号,但语义被改成了“投递一个事件” | 不能按“把值写进地址”去理解;必须按对方的事件语义去用,否则会写出莫名其妙的结果 |
| 扩展读写 | 非标准功能码,成对出现:读方向与写方向各一个 | 通用调试工具不认识它们,需要自己组帧;也说明这些功能只能以对方文档为准 |
| 锁 / 解锁 | 一个专用功能码,后面跟一个子码表示锁或解锁 | 写操作之前必须先确认锁的状态,否则会出现“命令发出了但没生效” |
摸底时我注意到一个细节:它的扩展功能码沿用了 Modbus 里“最高位置 1 表示同一功能的另一路”的编号习惯, 读和写是成对的。这个规律本身没有写进文档,是我把功能码集合列出来之后看出来的。 发现这个规律的好处很实际:遇到一个没见过的功能码,我能先猜出它大概属于哪一组、 是读还是写,再去实测验证;坏处是猜出来的东西必须实测确认, 不能因为“编号规律对得上”就当事实用。
三、它不是标准 Modbus:多出来的功能码与“锁 / 解锁”机制
标准 Modbus 的模型是“数据在地址上”:功能码说明我要做什么操作, 地址说明这个数据在哪。这颗表保留了这个外壳,但把写这一侧改掉了: 写命令的地址字段放的其实是“事件类型”,数据字段放的是这件事的参数。 同样是“写单个寄存器”的报文形状,语义却从“放数据”变成了“下命令”。
| 对比项 | 标准 Modbus 的写法 | 这块表的写法 |
|---|---|---|
| 写操作的语义 | 把某个值放进某个寄存器,可反复写、可读回 | 投递一个事件:地址表示事件类别,数据表示事件参数 |
| 可逆性 | 通常可逆:写错了再写回去 | 部分不可逆:有的参数一写下去,原值就没了 |
| 权限 | 规范里没有“锁”的概念 | 有独立的锁 / 解锁命令,锁上之后写事件会被拒绝 |
| 报文形状 | 标准帧结构 | 读命令沿用标准帧结构,扩展命令自己一套编号 |
写事件的类别我整理在下面这张表里。这里只写“改的是什么”,不写对方的事件编号—— 编号是对方的实现约定,而且它会随着固件版本变:
| 写事件类别 | 改的是什么 | 风险等级 | 我的处理 |
|---|---|---|---|
| 通信地址 | 从站地址本身 | 最高 | 二次确认 + 写前留旧值 + 写后按新地址回读;失败则按旧地址找回 |
| 正反方向 | 位置值的增减方向 | 高 | 二次确认;写后回读并与预期值比对 |
| 零点迁移 | 把当前测量值当作新的零点基准 | 最高(原零点被覆盖) | 先本地判断迁移量是否离谱,再二次确认;写前记录原零点 |
| 各位置上下限 | 多个位置的报警上限与下限 | 高 | 本地先校验“上限必须大于下限、必须落在量程内”,再逐项写并回读 |
| 恢复出厂设置 | 把标定值与几何参数一起抹掉 | 最高(不可在现场恢复) | 本地直接禁用这条路径;确实要用时按“重新标定”流程走,先备份全部可读参数 |
锁 / 解锁为什么必要
锁这个机制在标准 Modbus 里没有,但它解决的是一个真实问题: 写命令只要发出去就会生效,而发命令的一方可能是错的。 现场有人乱按、上位机有 bug、调试时手滑,都可能往总线上扔一条写事件。 锁是仪表侧的最后一道门:锁上之后,写事件被直接拒绝,参数保持在锁定时的状态。
我的封装方式是把“解锁 → 写 → 再锁上”做成一个函数,让它们成对出现, 而不是把解锁和加锁留给调用者自己记。原因很简单: 靠记性维持的状态一定会漏——漏了解锁,写命令悄悄失效; 漏了加锁,保护就形同虚设。这两种错都不会报错,只会让人对系统产生错误的信任。
四、用结构体映射一次读回一整片数据
这块表最省事的地方是:一条读命令就能把整片数据全部取回来。 版本号、多个位置值、多路温度、通信地址、满量程、零点、速度、状态字、用户迁移零点、 多个位置的上下限报警、正反标志、体积、罐型与罐体几何参数,全都在这一片里。
| 字段类别 | 它是什么 | 我要它做什么 |
|---|---|---|
| 版本号 | 仪表固件的版本标识 | 换表或对方升级固件后,第一件事就是看它变了没有 |
| 位置值(多路) | 各测量位置当前的液位高度 | 对外显示与联锁判断的主数据 |
| 温度(多路) | 仪表测到的各处温度 | 体积换算的输入,也是判断探头是否正常的旁证 |
| 通信地址 | 当前的从站地址 | 确认眼前这块表就是我地址表里登记的那一块 |
| 满量程 | 该表能测的最大值 | 做范围校验:任何写进去的限值都不能超出它 |
| 零点 | 当前使用的零点基准 | 与零点迁移事件配合,写之前先把它读出来存档 |
| 速度 | 测量响应速度档位 | 判断读数跳动是工艺波动还是档位太灵敏 |
| 状态字 | 仪表自身的状态位集合 | 区分“读数为 0”与“仪表根本没在测”这两种完全不同的情况 |
| 用户迁移零点 | 用户自己设的迁移量 | 排查“读数整体偏了”时,先看这里有没有被改过 |
| 位置上下限 | 多个位置的报警上限与下限 | 直接决定仪表的报警输出,属于我要写、但必须小心写的那一类 |
| 正反标志 | 位置值的增减方向 | 方向反了会让“越上限”变成“越下限”,必须与现场一致 |
| 体积 | 由位置值与罐体几何换算出的体积 | 这是我最想要的值,也是我不自己做传感器的原因 |
| 罐型与几何参数 | 罐体类型、半径、长度、封头尺寸 | 体积换算的依据;一旦被恢复出厂抹掉,必须实测罐体才能填回 |
一次读回一整片有两个明显的好处。第一是数据一致性: 这些字段属于同一时刻的快照,不会出现“版本是新的、几何参数还是旧的”这种撕裂。 第二是省通信:一条命令顶十几条,总线占用和超时管理的复杂度都降下来了。
做法很直接:按对方文档给出的字段顺序定义一个紧凑对齐的结构体, 把回帧的数据段整块拷进去,然后按字段名取值。省掉了逐字段解析的代码, 但也把两件事同时推给了我:对齐和字节序。
/* 通用写法示意:按对方文档给出的字段顺序,用紧凑对齐的结构体接住整片数据 */
#pragma pack(push, 2) /* 粒度按字段宽度定(示例按 16 位字段),不能用默认值 */
typedef struct {
u16 version; /* 字段顺序照抄文档,这里只列前几项 */
u16 level[POS_NUM];
u16 temp[CH_NUM];
u16 full_scale;
u16 zero_offset;
/* …中间字段按文档顺序排列,一项都不能漏、不能插… */
u16 tank_shape;
} dev_snapshot_t;
#pragma pack(pop)
/* 先核对长度:布局和文档不一致就干脆别解析 */
/* static_assert(sizeof(dev_snapshot_t) == PAYLOAD_LEN, "layout mismatch"); */
memcpy(&snap, payload, sizeof(snap)); /* 一次搬完,后面逐字段取用 */三个会静默出错的地方
-
默认对齐会插填充字节。编译器为了让成员落在自然边界上,
会在两个字段之间插入空隙,于是
sizeof(结构体)比数据长度大—— 拷贝本身没错,但从此以后每个字段都对不上位置。 所以必须用#pragma pack把填充关掉,并且长度核对要跟着加上。 - 字段宽度写错一样错位。文档写的是 16 位,我顺手写成 32 位, 这一个字段就把后面全部顶偏。所以宽度只能用文档给定的那一种,不能“反正够大”。
- 字节序。Modbus 的寄存器是大端,而 MCU 是小端; 整片原样拷进结构体,每个多字节字段的字节序都是反的。 数值不会离谱到一眼看出,只会变成另一个“看起来也像数据”的数。 我的处理是把字节序转换收敛到取值的地方,而不是散落在各个使用点—— 散着写,迟早有一处忘了转。
这三个坑的共同点是:它们都不会报错。结构体大小不对、宽度不对、字节序不对, 编译全都通过,运行也不崩,只是数据错。所以我的做法是加一道“长度核对”的闸门: 结构体长度和回帧数据长度不一致时,直接拒绝解析并上报故障, 而不是“能解多少解多少”。宁可显示“读不到”,也不要显示一个错的数。
如果让我重做一版,我会更倾向于不落结构体: 用一个字节数组接住数据,再配一组按偏移取值的访问器。 结构体读起来漂亮,但它把“布局”这件事交给了编译器; 而偏移表虽然啰嗦,却是显式的、可以对着文档逐行核对的,加字段也不会影响旧字段。
五、写操作的风险:零点迁移与量程上下限这类“改了就回不去”的参数
读这一侧做扎实之后,写这一侧才是真正需要小心的地方。 这块表上有一类参数,我把它叫“改了就回不去”:写下去的那一刻, 原值就被覆盖了,而我没有办法从总线上把原值找回来。
- 零点迁移:把当前测量值当作新的零点基准。写错之后,所有后续读数都建立在一个错误的基准上, 而且原来的零点已经被覆盖——现场表现是“液位一直偏高或偏低某个固定值”,越看越像探头坏了。
- 各位置的上下限:写反了(上限低于下限),报警要么永远不触发,要么一直触发。 后者更糟:一直报警会让人把报警当背景噪声,真的越限时反而没人看。
- 恢复出厂设置:把标定值与罐体几何参数一起抹掉。位置上下限我还能重新算, 但罐型、半径、长度、封头尺寸这些必须实测罐体才能填回去—— 现场没有人带着卷尺的时候,这块表就只能当摆设了。
- 通信地址:前面说过,改错了就从总线上消失。它属于“能回得去,但要有退路”的那一类。
这几件事让我给自己定了一套写操作的规矩。它不是从书上抄的, 是上面这些坑一条条换来的,我把它当成这块仪表适配层的设计经验:
- 把“不可逆”做成参数的一个属性,写进参数表里,而不是靠记性。 每条参数除了地址与取值范围,还带一个“可逆 / 不可逆”的标志; 写路径查这个标志决定要不要走确认流程。
- 不可逆的写必须先二次确认:先进入“待确认”状态, 收到明确的确认命令才真正下发;或者要求同一个值连写两次,两次一致才生效。
- 范围保护做在本地,不要指望对方替你挡。 写上下限之前自己先算:上限是否大于下限、是否落在满量程内、与当前位置值的关系是否合理; 写零点迁移之前先判断“这次迁移量是不是大得离谱”。越界的请求在本地就拒绝, 根本不发到总线上——总线上没有“撤销”。
- 写之前先读回旧值并留档。哪怕只是一条日志, 它也是事后唯一能说明“被改成了什么、原来是什么”的线索。
- 写操作不做自动重发,写后必须回读确认。原因见下一节:读是幂等的,写不是。
/* 通用写法示意:“改了就回不去”的参数,先过三道闸再做写动作 */
static int param_guard(u16 id, s32 new_val)
{
if (!range_ok(id, new_val)) return -1; /* 1. 范围闸:越界直接拒 */
if (!level_enough(id, cur_level)) return -2; /* 2. 权限闸:等级不够不给写 */
if (id_is_irreversible(id) && !confirm_pending(id))
return -3; /* 3. 确认闸:不可逆参数要二次确认 */
return 0; /* 三道都过,才允许组帧下发 */
}六、异常与超时:把第三方仪表当成一个不可靠从站来对待
第三方仪表是个黑盒:它可能不回话、回半帧、回异常码、也可能回得完全正确但内容是旧值。 我的态度很简单:它不回话不是异常,是常态; 适配层的责任是让上层永远拿不到“看起来正常的错误数据”。
| 情形 | 我观察到的表现 | 处理方式 |
|---|---|---|
| 完全不回话(掉线、掉电、地址不对) | 等到超时也没有任何字节 | 有限次重试;到上限就判链路故障并上报,同时把缓存数据作废,不再对外提供 |
| 回帧不完整或长度不对 | 收到了字节,但帧长与长度字段对不上 | 整帧丢弃,按超时处理;不要试图“用手上这些字节凑一凑” |
| 回异常码 | 对方明确告诉我这条命令它不接受 | 分成“值得重试”(设备忙、暂时不可用)与“不值得重试”(不支持的功能、参数非法)两类; 后者直接报错,不要反复发同一条命令 |
| 读回了数据,但其实是旧值 | 数值看着合理,只是没跟上现场变化 | 给缓存加快照时间与有效位;超过有效期就不给上层,避免把旧值当现值用 |
| 写命令超时 | 不知道对方到底执行了没有 | 不自动重发;按“可能已生效”处理,立即回读该参数确认实际状态 |
为什么“读可以重试,写只能回读”
这是我在这一节里最想说清楚的一条。读是幂等的:读一次和读三次,仪表的状态不变, 重试没有副作用。写不是幂等的:一条“零点迁移”重发两次, 很可能意味着以一个新的基准又迁移了一次;一条“改地址”重发两次, 第二次发到的可能已经不是你原来那块表了。
所以写命令的超时处理必须是回读确认而不是重发: 超时之后先假设“它可能已经生效了”,然后按可能的新状态去读一次, 用读回来的实际值决定下一步。宁可多花一条读命令,也不要让一条写命令在总线上出现两次。
/* 通用写法示意:把第三方设备当成“随时可能不回话”的从站 */
send_request();
if (wait_reply(TIMEOUT_MS) != OK) { /* 1. 单次超时,先别急着再发 */
retry++;
if (retry >= RETRY_MAX) {
mark_link_down(); /* 2. 到上限就判链路故障 */
invalidate_cache(); /* 3. 缓存立刻作废,不能继续用旧值 */
}
}
/* 写命令超时后不走这条路:改成“回读该参数、按实际值判断” */RS485 半双工:方向切换的那点延时不能省
这块表挂在半双工 485 上,方向脚要自己控制。代码里的注释写得很直白: 发送完毕之后,切换回接收必须先延时。我当时用的值是发送前切到发送方向延时 1 ms、 发送后切回接收方向延时 2 ms。
为什么不能一发送完就立刻切回来?两个原因叠在一起: “发送完成”这个标志通常只表示数据已经交给移位寄存器, 最后一个字节可能还在线上;而收发器本身的方向切换也要时间。 切早了,对方的回帧头几个字节会被自己吃掉——现场表现是“偶尔校验错、偶尔整帧丢”, 而且是概率性的,很难复现。这类问题的账我算过很多次, 《串口帧同步的四种方案》里 RS485 半双工那一节写得比我这里更细。
最后一点是心跳:我让主控周期性地把整片数据读回来,用这条读命令的成败当作链路健康指标, 连续失败就上报故障。这条读命令本来就要发,所以它当心跳是零成本的—— 和我在显示板上用的思路一样:已经有一条周期性必然发生的报文,就不要再发明心跳。
七、问题与解决
下面这几条都是我在接这块表的过程中真实撞到的,按“现象 → 原因 → 办法”记下来。
| 现象 | 原因 | 我的办法 |
|---|---|---|
| 按文档解析出来,某几个字段的值明显不合理(地址或状态字对不上) | 手上这份文档与实物固件版本不一致,字段有增减或含义改了 | 用实测报文逐字节核对,把字段标成“已实测确认”和“仅文档”两类; 不一致时先怀疑自己的偏移与字节序,再怀疑文档版本 |
| 结构体中间加了一个字段之后,后面所有字段都变成离谱的值 | 结构体布局变了,但没有任何长度核对,错位被静默吃下去 | 加长度核对(结构体长度与回帧数据长度比对),不一致直接拒绝解析并报故障 |
| 结构体长度比回帧数据长度多出几个字节 | 默认对齐在成员之间插了填充字节 | 用 #pragma pack 关掉填充;字段宽度严格按文档,不随手放大 |
| 偶尔整帧丢、偶尔校验错,概率性出现 | 半双工方向切换太早,对方回帧的头几个字节被自己吃掉 | 发送完成后再延时再切回接收;把我实际用的延时值写进注释,别让它变成一个“没人知道为什么”的魔数 |
| 调试时手滑发出了写事件,参数被改 | 读和写共用同一个发送入口,中间没有任何闸门 | 把写路径单独封装并加范围、权限、确认三道闸;调试阶段先用只读模式,把写函数关掉 |
| 写命令发出去了,但对方没执行 | 仪表处于锁定状态,写事件被拒绝;而我以为它是开着的 | 把“解锁 → 写 → 再锁上”包成一个函数成对出现;写后必须回读确认,不靠返回值想当然 |
| 上层拿到的是几分钟前的快照,却当成当前值在用 | 缓存只存了值,没有存“这个值是什么时候读到的” | 给缓存加时间戳与有效位,超过有效期就不对外提供,并把这个状态暴露给上层 |
这一路做下来,我最大的收获不是学会了某个功能码,而是接受了一个事实: 第三方设备的文档与实物行为可能不一致,必须以实测报文为准。 我的做法是把每一份“文档结论”都当成待验证的假设,先在总线上抓一遍原始字节, 确认过之后才写进自己的字段表;没确认过的,就在表里标注清楚。 这件事看起来很笨,但它让我后来排查问题时有一个可靠的基准: 我的字段表是我实测过的,出问题就一定在别处。
八、局限与还没做完的部分
这块表的接入是能用的,但下面每一条都是我知道不够好、暂时还没动的地方。
| 问题 | 现状 | 我打算怎么改 |
|---|---|---|
| 只按“一块表”写的 | 一份缓存、一个地址、一套超时计数;如果一条总线上挂多块同类仪表,现在这套结构撑不住 | 把缓存、超时、锁状态都收进一个“设备实例”结构里,按实例管理,调用方式不变 |
| 仪表侧的标定参数没有备份 | 几何参数与零点只存在仪表里;它一恢复出厂,我只能拿尺子重新量罐体 | 把全部可读参数定期抄回主控存档,并留一份“上次已知良好”的副本 |
| 写成功的判定只靠回读比对 | 回读一致就认为写成功,没有更强的确认手段 | 对不可逆参数再多做一步:写后隔一小段时间再读一次,确认不是瞬时值 |
| 字段偏移硬编码在结构体里 | 加字段只能改结构体、重编译;文档一换版就得重新核对一遍代码 | 改成一张字段表(偏移 / 宽度 / 含义 / 读写属性 / 是否不可逆)驱动解析, 让“哪些参数不能乱写”也成为表里的一列;思路和参数表设计那篇里写的代价与坑是同一套 |
| 缓存的时效只有标志位 | 有“有效 / 无效”,但没有“多久之前读到的” | 加时间戳,让上层能自己决定“这个数据还能不能用” |
| 没有做异常注入测试 | 拔线、断电、错误波特率、错误地址这些情况我都是碰上了才处理 | 做一轮有针对性的破坏性测试,把每种情形下适配层的行为记成一张表,再长时间跑一遍 |
| 没有做对方固件版本的兼容层 | 对方固件一变、字段长度一变,我这边的长度核对会直接拒绝解析 | “拒绝解析”这个行为我要保留,但需要补一条明确的提示路径:告诉用户去核对文档,而不是只报一个错误码 |
还有一条不算“没做完”,而是我对这类工作的看法: 接第三方设备,代码量从来不是主要成本。 真正花时间的是摸底、实测核对、以及把“它可能不听话”这件事设计进系统里。 我在这块表上写的通信代码可能只有几百行, 但围绕它做的范围保护、二次确认、缓存时效和超时策略,比通信代码本身多得多。 这些代码平时看不出来在干什么,只在出事的那一次才有用——而它们存在的意义,就是那一次。
参考资料与说明
- Modbus 应用协议规范(Modbus Application Protocol Specification),用于确认标准功能码、异常码与报文结构;本文这块仪表只沿用了其中一部分,其余为厂商自定义。
- Modbus over Serial Line Specification and Implementation Guide,用于确认串行链路上的帧间隔与半双工方向控制要求。
- 某型液位仪表随机通信说明与寄存器表(厂商公开随机文档),本文只引用其字段类别,不复制其表格数值。
- TIA/EIA-485 串行通信标准相关公开资料,用于确认收发器方向切换与总线建立的时序约束。
- GD32F303 系列用户手册与 Arm Cortex-M4 技术参考手册,用于确认结构体对齐、字节序与串口配置。
- FreeRTOS 官方文档,用于确认任务与软件定时器在周期读与超时管理中的用法。
- 文中涉及的芯片型号与标准名称仅用于说明技术方案,与相关厂商无隶属或授权关系; 第三方仪表的品牌与型号未在本文出现,统一以“某型液位仪表”指代。
- 本文不列出从站地址、寄存器数量、功能码与子码取值、事件类型编号,以及量程与罐体几何参数等具体数值。
- 示例代码为按个人理解重写的最小片段,不代表任何产品或交付代码。
- 出于对个人项目实现细节的保护,本文只公开方法与思路; 涉及核心实现的部分以步骤与字段说明代替代码,示例代码为按个人理解重写的通用片段。