ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C#上位机串口读取梅特勒电子天平:MT-SICS协议解析与稳定读数实战

C#上位机串口读取梅特勒电子天平:MT-SICS协议解析与稳定读数实战 1. 需求拆解与方案设计思路1.1 为什么要把天平接到电脑上做过实验室工位、配料车间或者质检线的人都知道梅特勒电子天平本身读数非常准但它默认的用法是人站过去、看屏幕、手动抄到纸质记录单上。这个流程在样品量少的时候没什么问题一旦一天要称几百次抄错、抄漏、漏记时间戳几乎是必然的后面再想追溯数据就更麻烦。把梅特勒电子天平通过串口接到上位机本质上就是把「人眼读屏 手写记录」换成「程序读字节 自动入库」精度还是天平原本的精度但人工环节被彻底去掉。我做的这套方案核心就是几件事C# 上位机通过串口读取称重数据、解析成结构化的重量值、判定读数是否稳定、再把结果落到数据库或者界面。适合正在做称重采集上位机的开发者也适合手头只有一台天平、想先跑通最小可运行 Demo 的新手。只要你会写基本的 C# 控制台或者 WinForms 程序剩下的就是对串口和梅特勒自有协议的理解了这块我会拆得很细。1.2 硬件链路与软件链路的整体架构整套系统的物理链路其实很简单梅特勒天平通常是 RS-232 接口的 DB9 母头→ 串口线或 USB 转串口模块 → PC 的 COM 口 → C# 程序。软件侧我习惯拆成三层这个分层不是照搬教科书而是踩过坑之后觉得最省事的分法。最底下是连接层只负责打开端口、收发原始字节、处理断线重连对外暴露「收到一帧文本」事件中间是解析层把一帧 ASCII 文本翻译成{是否稳定, 数值, 单位, 状态码, 时间戳}这样的结构体最上面是业务层负责稳定判重、去皮、超差报警、写库、上传 MQTT、刷新界面。分三层最大的好处是换一台不同型号的天平往往只需要改解析层的单位表和状态字符连接层和业务层基本不动。这种分层看起来多写了一点代码但在现场很值。我见过不少项目把SerialPort和业务逻辑全塞进一个按钮的Click事件里后期加个自动去皮就要满文件找ReadLine改一次崩一次。1.3 关键选型串口直连还是走网关在动手之前有个选型问题必须先想清楚是让 PC 直接读串口还是买一个协议网关把天平转成以太网或者 OPC UA。我的一般建议是——单一工位、单台天平、需要低延迟实时读数的场景直接串口直连如果是全厂几十台天平要汇总到一个 MES那用网关或者边缘盒子会省很多布线麻烦代价是成本高、中间多一跳延迟。对于绝大多数和我遇到的类似的场景串口直连是性价比最高的。它的延迟可以做到几十毫秒级别代码量小出了问题也容易定位串口调试助手一挂上去就知道是硬件问题还是代码问题。本方案后面所有内容都基于串口直连展开。2. 梅特勒天平通信协议要点把 MT-SICS 吃透2.1 拉模式和推模式先选对工作方式梅特勒Mettler-Toledo的电子天平大多支持自家的 MT-SICS 指令集这是理解整个项目最核心的一块。它有两种典型工作方式一种是拉模式也就是上位机主动发指令比如发S让天平返回一个当前稳定重量天平收到后回一帧另一种是推模式天平按固定间隔自己往外吐数据上位机只管接。这两种模式适用场景完全不同。拉模式适合「需要一个确定时刻的确定值」比如按下打印按钮、扫码枪扫完之后立刻取重推模式适合「连续监控」的看板场景比如观察一个反应过程中的重量变化曲线。我的实际做法是默认用推模式跑实时界面需要精确取数的瞬间再切回拉模式补一条S指令两种模式共存靠协议的响应格式来区分。需要注意的是切换工作模式往往要在天平本体的菜单里改设置常见路径是Setup → Communication一类的菜单或者用 SICS 指令切换。切换之后波特率、输出格式可能一起变改完要重新验证一遍别想当然。2.2 应答帧逐字符拆解MT-SICS 的标准重量应答帧是一串定长 ASCII长度通常在 15 到 18 个字符之间含结束符。我拿一帧真实数据来拆S S 123.4567 g按位置看第 1 到第 2 个字符是状态字示例里的S S表示「静态稳定值」第 3 个字符是一个空格第 4 到第 13 个字符是重量数值字段通常是右对齐、左边补空格宽度大约 10 个字符第 14 个字符又是空格第 15 到第 16 个字符是单位或者有些型号只占 1 个字符最后跟一个回车换行。状态字里第一个字符通常是SStable稳定或者DDynamic动态第二个字符区分更多含义S表示静态稳定、D表示动态变化、还有表示超载、欠载、无效、出错的状态字母。这里有个非常容易被忽略的点稳定不代表可以直接入库动态读数如果被当成稳定值写进数据库后面追溯出来的结果就是错的。所以解析的时候一定要把稳定性判定做实不能只看有没有数字。2.3 常用指令速查与响应约定下面是日常开发最常打交道的几条 MT-SICS 指令我做了个速查表实际调试的时候对着这张表发指令能省掉大量翻手册的时间。指令作用典型响应复位清空天平内部待处理命令 A表示接受S请求一个稳定重量值S S 100.00 gSI立即请求重量不管稳不稳定S I 100.00 gSIR请求连续立即输出天平开始持续推送连续多帧T执行去皮TareT S 0.00 gZ清零ZeroZ A或Z ID在显示屏上显示一段文字用于提示操作员I0至I4查询天平型号、序列号等信息I0 A ME204响应里出现A表示命令已接受并执行出现I表示天平理解命令但当前无法执行比如正在动态测量中就去皮。这两种应答很容易和重量帧混在一起所以解析层第一步就要判断「这是命令应答还是称重数据」判据一般是长度和第三个字符是不是空格这个细节后面代码里会写。提示给天平发指令时命令后面通常要补回车换行再发送。有些型号对结束符很敏感只发回车可能没反应一定要按手册写全。3. 硬件连接与串口参数实操3.1 接口类型与线序确认梅特勒天平常见的通信接口有几种一种是标准 RS-232 的 DB9 母座一种是圆形的迷你 DIN 接口很多分析天平用这个还有部分型号直接给 USB 口但内部还是模拟串口。DB9 的情况下如果是一根直连线接 PC 的 DB9 公头一般能直接通如果接 USB 转串口就要弄清楚转接头的 TX、RX 有没有交叉。我碰到过一次很典型的坑线是随手找的「串口延长线」插上去能识别、能打开端口但就是收不到任何数据。折腾了两个小时之后换了一根确定是直连的线立刻就通了。原因就是那根延长线内部已经做了一次交叉再加转接头的交叉变成了「交叉再交叉」实际又变回直连和天平期望的对不上。所以线序问题永远是第一排查项不是第二排查项。如果天平的接口是那种小圆口一定要用厂家配套的通信线或者按手册自己焊不能凭感觉接。涉及硬件的部分我建议先在天平的菜单里打印一份通信配置页很多型号支持直接打印当前通信设置比翻菜单截图靠谱。3.2 USB 转串口芯片的选型与驱动踩坑现在 PC 基本没有原生串口了USB 转串口是绕不开的一环。市面上常见的芯片就是那几类CH340 系列、CP2102 系列、FT232 系列。从稳定性上讲我个人偏向 FT232 或者 CP2102长时间连续跑几天不容易掉线CH340 便宜量大做一次性调试完全够用但在有些老版本的系统和虚拟机里驱动会打架。驱动问题几乎每个人都会遇到一次设备管理器里出现一个带黄色感叹号的未知设备或者端口号在COM1和COM20之间来回跳。前者的处理办法是装对应芯片的官方驱动装完一定要重启一次再插后者是因为 Windows 给 USB 设备分配端口号时不会回收拔插次数多了端口号越用越大程序里如果写死了COM3就会打开失败。我的做法是程序里永远不写死端口名启动时用SerialPort.GetPortNames()把可用端口列到下拉框里让操作员选选完把端口名存到配置文件。这样换电脑、换转接头都不用改代码。3.3 串口参数与天平菜单的一致性串口参数必须和天平本体的设置完全一致这句话听起来像废话但实际项目中「不一致」导致收不到数据的比例非常高。常用参数是9600 波特率、8 数据位、无校验、1 位停止位、无流控这也是绝大多数梅特勒天平的出厂默认。但注意有些型号默认波特率是 4800有些在切到特定输出模式后会自动改成 19200。参数对不上的典型表现是能打开串口能发命令但收到的全是乱码或者根本没有响应。判断方法很直接——把串口调试助手挂上去选同样的参数看能不能看到可读的 ASCII 文本。看得到可读文本就说明参数对了剩下是代码问题看不到就回头调参数。流控这一项也要留意。有些天平支持硬件流控RTS/CTS如果你在代码里开了Handshake.RequestToSend但天平并没有真的接那两根线串口会一直等待允许发送信号表现为「发出去的指令没有回应」。所以默认设置成Handshake.None是最省心的。4. C# 代码落地从打开串口到稳定读数4.1 连接层封装先把读写和重连做扎实下面这段是我常用的连接层骨架。它做的事情是打开端口、起一个后台读循环、把收到的每个字节拼成完整帧往外抛。我刻意没用DataReceived事件原因后面单独讲。using System; using System.IO.Ports; using System.Text; using System.Threading; using System.Threading.Tasks; public sealed class MettlerSerialClient : IDisposable { private readonly SerialPort _port; private CancellationTokenSource? _cts; private Task? _readLoop; public event Actionstring? FrameReceived; public event Actionstring? Log; public bool IsOpen _port.IsOpen; public MettlerSerialClient(string portName, int baudRate 9600) { _port new SerialPort(portName, baudRate, Parity.None, 8, StopBits.One) { Handshake Handshake.None, ReadTimeout SerialPort.InfiniteTimeout, WriteTimeout 1000, Encoding Encoding.ASCII, DtrEnable true, RtsEnable true }; } public void Open() { if (_port.IsOpen) return; _port.Open(); _port.DiscardInBuffer(); _port.DiscardOutBuffer(); _cts new CancellationTokenSource(); _readLoop Task.Run(() ReadLoopAsync(_cts.Token)); } private async Task ReadLoopAsync(CancellationToken token) { var buffer new byte[256]; var sb new StringBuilder(64); while (!token.IsCancellationRequested) { int read; try { read await _port.BaseStream .ReadAsync(buffer.AsMemory(0, buffer.Length), token) .ConfigureAwait(false); } catch (OperationCanceledException) { break; } catch (Exception ex) { Log?.Invoke($读取异常准备重连{ex.Message}); break; } if (read 0) continue; for (int i 0; i read; i) { char c (char)buffer[i]; if (c \r) continue; if (c \n) { var frame sb.ToString(); sb.Clear(); if (frame.Length 0) FrameReceived?.Invoke(frame); } else { sb.Append(c); if (sb.Length 512) sb.Clear(); // 防止异常数据撑爆缓冲 } } } } public async Task SendAsync(string command) { if (!_port.IsOpen) throw new InvalidOperationException(串口未打开); var bytes Encoding.ASCII.GetBytes(command \r\n); await _port.BaseStream.WriteAsync(bytes).ConfigureAwait(false); } public void Dispose() { try { _cts?.Cancel(); } catch { } try { if (_port.IsOpen) _port.Close(); } catch { } _port.Dispose(); } }这段代码有几个地方是我故意这么写的。第一接收是异步循环而不是事件驱动第二用StringBuilder自己做分帧第三遇到异常直接跳出循环交给上层重连而不是在循环里死撑。4.2 为什么我不用 DataReceived 事件SerialPort.DataReceived是很多人上手串口通信时写的第一个东西它看起来很方便有数据就触发事件事件里ReadExisting一下。但它在生产环境中会带来几个麻烦。一是触发时机不保证。事件不代表「收到了一帧完整数据」它只代表「内部缓冲区里有字节了」。连续输出模式下天平在持续吐数据一次事件里可能读到半帧也可能读到三帧半。如果你按「一次事件 一帧」来写代码一定会丢数据或者解析出错。二是线程模型别扭。事件在后台线程池线程上执行直接更新界面控件会抛跨线程异常得各种Invoke代码很快就乱。三是关闭时的死锁风险。在DataReceived里面调用Close()有概率卡住这在程序退出的时候特别明显。所以我的做法统一是用BaseStream.ReadAsync起一个后台任务自己维护一个StringBuilder按换行符切帧。这样帧边界是确定的线程也干净收到完
返回列表