本站为个人非经营性网站,仅用于技术学习与分享,不提供任何商品、服务、报价或收费咨询。 合规信息

外购液位仪表的协议接入:
把第三方仪表当成一个特殊从站

液位测量这件事我没有自己做传感器,而是买了一块某型液位仪表,把它挂到自己的 RS485 总线上。 写通信代码只花了两天,剩下的时间全花在一件事上:搞清楚它到底怎么说话。 它自称 Modbus,但多出一组非标准功能码,写操作还带锁;它一次就能把整片数据读回来, 代价是结构体对齐全靠手动保证。这篇记录写的是我从摸底到写保护设计的整个过程。

RS485 Modbus 变体 结构体映射 第三方设备适配 写保护 已发布
已发布 · 个人学习项目

把外购液位仪表接入自有总线(RS485 / Modbus 变体)

主控侧是 GD32F303VC(Cortex-M4)+ FreeRTOS,总线上除了这块仪表还挂着面板、 读卡器和我自己做的采集小板。仪表这一侧的适配工作单独放在一个模块里, 对外只暴露“一份结构化快照”和几个写接口。做下来我的结论有三条: 第一,第三方仪表的成本不在通信代码,而在把它当成一个不可靠的外部依赖来设计; 第二,协议的文档和实物行为可能不一致,必须用实测报文核对; 第三,零点迁移、量程上下限这类“改了就回不去”的参数,必须有自己的范围保护与二次确认, 不能指望对方替你挡住。

PLATFORM主控侧(Cortex-M4 + FreeRTOS)
STACKModbus RTU 变体 + 结构体映射
TOOLSKeil MDK + 串口助手
STATUS个人学习项目 · 功能已实现

一、为什么要接第三方仪表,而不是自己做传感器

一开始我是想自己做的:一个液位探头加一路 ADC,换算成高度,多简单。 真正动手才发现,我要的其实不是“液位”,而是体积和一组可报警的位置值。 从液位到体积这一步,需要知道罐型、罐体半径与长度、两端封头的形状; 再往上还要做温度补偿、多个位置的上下限报警、正反向判断。 这些已经不是“读一个模拟量”的事,而是仪表行业的专业范围:要结构、要标定, 某些场合还要计量认证。这些我在个人条件下做不到,也不该硬做。

所以这块仪表是外购的。但买回来的不是一个“功能”,而是一份契约: 它的接口是固定的、协议是它自己定义的、行为不保证与文档完全一致。 我的工作因此从“做传感器”变成了“把它的契约翻译进我的系统”, 具体就是三件事:一次读回一份完整的快照、把写操作按风险分级管起来、 以及在它不回话的时候让系统知道该怎么办。

这条链路接进主控之后,主控那一侧看到的只是一份结构化的数据(当前体积、多路位置值、多路温度、 状态字、报警上下限)和几个写接口,完全不用知道对面是哪一家的表、用什么功能码。 这一层隔离是我在这件事上做对的地方:把“不可靠”关在适配层的门里, 业务代码只管读快照。

我一开始的预期是“读写寄存器而已,两天搞定”。两天确实把读通了, 但真正让我改了设计的是后面两件事:一次把某个参数写进去之后,我发现原值已经不在了; 以及按文档解析出来的几个字段,实测值明显不合理。 接第三方设备,难点从来不是写代码。

二、先摸清它的协议:地址、功能码集合、读写方式

面对一块外购仪表,我给自己定了一套固定的摸底顺序,做完这五步才允许动代码:

  1. 把文档当线索,不当事实。先从通信说明里抄出三样东西:从站地址范围、功能码集合、寄存器清单;
  2. 总线上只留它一块表。把其他设备断开,避免自己的轮询报文混进来干扰判断;
  3. 用串口助手逐条发最小报文,把每一条的应答原样存下来,不急着写解析代码;
  4. 逐字段与文档比对,并把结论分成两类:实测确认过的、只在文档里见过的;
  5. 形成一份自己的字段表,从此以后代码只认这张表,不再去翻对方的文档。

第 4 步里有个小技巧我一直在用:文档里写“保留”“备用”的字段,实测时也要读回来看看它到底是什么。 这类字段往往是对方没写进文档的功能,或者是上一个固件版本留下的痕迹; 至少要确认它不会因为我的写操作而改变。

地址:改地址本身是一件危险的事

这块表出厂带一个默认的短地址(本文不列出具体值),也支持通过写事件改地址。 这里有个很容易被忽略的风险:改地址这个动作一旦生效,你就按旧地址找不到它了。 如果新地址写错了、或者写进去之后没保存成功,下一次上电总线上就少了一个从站, 而现场没有任何提示。所以我后来把“改地址”单独归到最高风险的一类里, 写之前要先记住旧地址、写之后要按新地址回读确认、确认不到就按旧地址再找一次。

顺带说一句地址规划:我这条总线上挂了不止一种设备,所以从一开始就维护了一张设备地址表 (哪一类设备用哪一段地址)。这不是为了好看——地址不冲突是总线上唯一不能商量的事, 它不像帧长或超时那样可以事后调。

功能码集合:标准两个,其余是非标准的

它的功能码集合可以分成三类,我把它们的性质列在下面这张表里。 具体数值属于对方的实现约定,本文不列出:

类别性质我因此要注意什么
读保持寄存器 标准 Modbus 功能码,语义与规范一致 可以用通用调试工具直接发;这是我做摸底时的主力命令
写单个寄存器 标准功能码的编号,但语义被改成了“投递一个事件” 不能按“把值写进地址”去理解;必须按对方的事件语义去用,否则会写出莫名其妙的结果
扩展读写 非标准功能码,成对出现:读方向与写方向各一个 通用调试工具不认识它们,需要自己组帧;也说明这些功能只能以对方文档为准
锁 / 解锁 一个专用功能码,后面跟一个子码表示锁或解锁 写操作之前必须先确认锁的状态,否则会出现“命令发出了但没生效”

摸底时我注意到一个细节:它的扩展功能码沿用了 Modbus 里“最高位置 1 表示同一功能的另一路”的编号习惯, 读和写是成对的。这个规律本身没有写进文档,是我把功能码集合列出来之后看出来的。 发现这个规律的好处很实际:遇到一个没见过的功能码,我能先猜出它大概属于哪一组、 是读还是写,再去实测验证;坏处是猜出来的东西必须实测确认, 不能因为“编号规律对得上”就当事实用。

主控经 RS485 连接外购液位仪表,用锁形图标标出“写操作需要先解锁”,并标注“改了就回不去”的参数类别
图 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));   /* 一次搬完,后面逐字段取用 */

三个会静默出错的地方

  1. 默认对齐会插填充字节。编译器为了让成员落在自然边界上, 会在两个字段之间插入空隙,于是 sizeof(结构体) 比数据长度大—— 拷贝本身没错,但从此以后每个字段都对不上位置。 所以必须用 #pragma pack 把填充关掉,并且长度核对要跟着加上。
  2. 字段宽度写错一样错位。文档写的是 16 位,我顺手写成 32 位, 这一个字段就把后面全部顶偏。所以宽度只能用文档给定的那一种,不能“反正够大”。
  3. 字节序。Modbus 的寄存器是大端,而 MCU 是小端; 整片原样拷进结构体,每个多字节字段的字节序都是反的。 数值不会离谱到一眼看出,只会变成另一个“看起来也像数据”的数。 我的处理是把字节序转换收敛到取值的地方,而不是散落在各个使用点—— 散着写,迟早有一处忘了转。

这三个坑的共同点是:它们都不会报错。结构体大小不对、宽度不对、字节序不对, 编译全都通过,运行也不崩,只是数据错。所以我的做法是加一道“长度核对”的闸门: 结构体长度和回帧数据长度不一致时,直接拒绝解析并上报故障, 而不是“能解多少解多少”。宁可显示“读不到”,也不要显示一个错的数。

如果让我重做一版,我会更倾向于不落结构体: 用一个字节数组接住数据,再配一组按偏移取值的访问器。 结构体读起来漂亮,但它把“布局”这件事交给了编译器; 而偏移表虽然啰嗦,却是显式的、可以对着文档逐行核对的,加字段也不会影响旧字段。

左侧一条读命令,右侧是一整片寄存器,用箭头把连续数据块映射到结构体的各个字段上
图 2 · 一次读取的数据映射图

五、写操作的风险:零点迁移与量程上下限这类“改了就回不去”的参数

读这一侧做扎实之后,写这一侧才是真正需要小心的地方。 这块表上有一类参数,我把它叫“改了就回不去”:写下去的那一刻, 原值就被覆盖了,而我没有办法从总线上把原值找回来。

  • 零点迁移:把当前测量值当作新的零点基准。写错之后,所有后续读数都建立在一个错误的基准上, 而且原来的零点已经被覆盖——现场表现是“液位一直偏高或偏低某个固定值”,越看越像探头坏了。
  • 各位置的上下限:写反了(上限低于下限),报警要么永远不触发,要么一直触发。 后者更糟:一直报警会让人把报警当背景噪声,真的越限时反而没人看。
  • 恢复出厂设置:把标定值与罐体几何参数一起抹掉。位置上下限我还能重新算, 但罐型、半径、长度、封头尺寸这些必须实测罐体才能填回去—— 现场没有人带着卷尺的时候,这块表就只能当摆设了。
  • 通信地址:前面说过,改错了就从总线上消失。它属于“能回得去,但要有退路”的那一类。

这几件事让我给自己定了一套写操作的规矩。它不是从书上抄的, 是上面这些坑一条条换来的,我把它当成这块仪表适配层的设计经验:

  1. 把“不可逆”做成参数的一个属性,写进参数表里,而不是靠记性。 每条参数除了地址与取值范围,还带一个“可逆 / 不可逆”的标志; 写路径查这个标志决定要不要走确认流程。
  2. 不可逆的写必须先二次确认:先进入“待确认”状态, 收到明确的确认命令才真正下发;或者要求同一个值连写两次,两次一致才生效。
  3. 范围保护做在本地,不要指望对方替你挡。 写上下限之前自己先算:上限是否大于下限、是否落在满量程内、与当前位置值的关系是否合理; 写零点迁移之前先判断“这次迁移量是不是大得离谱”。越界的请求在本地就拒绝, 根本不发到总线上——总线上没有“撤销”。
  4. 写之前先读回旧值并留档。哪怕只是一条日志, 它也是事后唯一能说明“被改成了什么、原来是什么”的线索。
  5. 写操作不做自动重发,写后必须回读确认。原因见下一节:读是幂等的,写不是。
/* 通用写法示意:“改了就回不去”的参数,先过三道闸再做写动作 */
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 官方文档,用于确认任务与软件定时器在周期读与超时管理中的用法。
  • 文中涉及的芯片型号与标准名称仅用于说明技术方案,与相关厂商无隶属或授权关系; 第三方仪表的品牌与型号未在本文出现,统一以“某型液位仪表”指代。
  • 本文不列出从站地址、寄存器数量、功能码与子码取值、事件类型编号,以及量程与罐体几何参数等具体数值。
  • 示例代码为按个人理解重写的最小片段,不代表任何产品或交付代码。
  • 出于对个人项目实现细节的保护,本文只公开方法与思路; 涉及核心实现的部分以步骤与字段说明代替代码,示例代码为按个人理解重写的通用片段。