配料灌装、包装复检、收货过磅,凡是涉及重量的工位,最怕的就是"人眼看仪表、手抄进系统":抄错一位数、趁乱多记少记、出了质量问题查无对证。电子秤和地磅几乎都带串口,把重量实时、稳定、可信地读进上位机并不复杂,但有两个坑必须绕开:协议帧要按字节拆对,重量要等真正稳定再抓。这篇以工厂保有量最大的耀华 XK3190 系列仪表为例,给出从接线、解析到抓重、防作弊的完整 C# 方案。
一、先选工作方式:连续发送还是命令应答
| 模式 | 工作方式 | 适合场景 | 注意 |
|---|---|---|---|
| 连续发送 (Continuous) | 仪表上电后主动、持续地向外发当前重量,通常每秒数帧到十几帧 | 单秤直连、灌装/复检等需要实时看重量曲线的工位 | 无站号,一条总线只能挂一台 |
| 命令应答 (On-demand) | 上位机发读重命令,仪表回一帧 | 多台秤 RS485 组网轮询、地磅汽车衡称重软件 | 要实现超时重发,命令帧带站号 |
▲ 仪表参数表里两种模式一般都能切(XK3190 系列通过通讯参数设置)。下文重点拆连续发送——它用得最多,也最容易因为粘包/乱序写出不稳定的解析代码。
二、接线和串口参数:90% 的"通讯不上"出在这里
- 物理层:台式仪表多用 RS232(DB9 或端子 TXD/RXD/GND 三线),上位机没有原生串口就用 USB 转 RS232,选工业级芯片(FT232/隔离型),十几块钱的杂牌转换器在车间接地干扰下会随机丢字节;地磅离操作室超过 15 米,改用仪表的 RS485 口或加 RS232/485 中继;
- 接线记住交叉:仪表 TXD 接转换头 RXD、RXD 接 TXD、GND 必须接。只接两根数据线不接地,电位差会让帧里随机冒 0x00/0xFF;
- 串口参数:XK3190-A9 连续发送常见默认
2400,8,N,1,部分新机为 9600,8 个数据位、无校验、1 个停止位。上位机参数必须和仪表设置逐字一致,现象对照:波特率错=全是乱码;数据位/停止位错=能收到字节但帧永远对不上; - 设备管理器确认 COM 号,USB 转串口重新插拔后 COM 号可能变,程序里让用户可选并记忆。
三、XK3190-A9 连续帧:12 个字节逐个认
连续发送模式下,仪表把当前显示值打成固定长度的 ASCII 帧不断送出。以 XK3190-A9 常见格式为例,一帧 12 字节:
▲ 逐字节含义(固件版本不同会有细微差异,落地时以随表通讯协议手册为准,但帧头判定和拼包方法通用):
| 字节 | 取值 | 含义 |
|---|---|---|
| [0] 帧头 | '=' (0x3D) | 重量稳定:仪表内部稳定判定通过 |
| '~' (0x7E) | 重量变化中/不稳定 | |
| [1] w | '0'~'5' | 状态字,按位组合(ASCII 数值 = 字符码 - 0x30):bit0:1=净重,0=毛重;bit1:1=零位;bit2:1=超载/溢出 |
| [2] n | '0'~'7' | 扩展状态字(部分固件用于欠压、峰值/保持等指示,多数场景保留不用) |
| [3-8] 重量 | " 12.50" | 6 个 ASCII 字符,前导空格补位,小数点位置随仪表分度/量程设置,直接 decimal 解析 |
| [10-11] | 0x0D 0x0A | 帧结束回车换行,可靠的切帧标记 |
'3'=0b011 表示"净重 + 零位"。解析时把字符转成整数后用 (w & 0b001)!=0 判净重、(w & 0b010)!=0 判零位、(w & 0b100)!=0 判超载,不要拿固定字符做相等比较。四、C# 串口解析:DataReceived 里拼包,永远不要按事件假设一帧
新手写法是 ReadExisting() 然后 Split——2400 波特率下,一次 DataReceived 可能只有半帧,也可能一次塞来一帧半,Split 出来的东西必然时好时坏。正确做法是维护一个接收缓冲区,按"找帧头→等够长度→验帧尾"状态机切帧:
public class ScaleFrame { public bool Stable; public bool IsNet; public bool AtZero; public bool Overload; public decimal Weight; public DateTime Ts; } public class Xk3190Reader : IDisposable { private readonly SerialPort _port; private readonly List<byte> _buf = new(); private const int FrameLen = 12; public event Action<ScaleFrame> FrameReceived; public Xk3190Reader(string com, int baud = 2400) { _port = new SerialPort(com, baud, Parity.None, 8, StopBits.One) { ReadBufferSize = 4096, ReceivedBytesThreshold = 1 }; _port.DataReceived += OnData; } private void OnData(object s, SerialDataReceivedEventArgs e) { var n = _port.BytesToRead; var tmp = new byte[n]; _port.Read(tmp, 0, n); lock (_buf) { _buf.AddRange(tmp); ParseBuffer(); } } private void ParseBuffer() { while (_buf.Count >= FrameLen) { // ① 找帧头 = (0x3D) 或 ~ (0x7E),之前的残字节全部丢弃 int head = _buf.IndexOfAny(0x3D, 0x7E); if (head < 0) { _buf.Clear(); return; } if (head > 0) _buf.RemoveRange(0, head); if (_buf.Count < FrameLen) return; // 半帧,等下次 // ② 验帧尾 0x0D 0x0A,不对说明帧头是数据里的巧合字节,前进一位继续找 if (_buf[10] != 0x0D || _buf[11] != 0x0A) { _buf.RemoveAt(0); continue; } var raw = _buf.GetRange(0, FrameLen); _buf.RemoveRange(0, FrameLen); var frame = Decode(raw); if (frame != null) FrameReceived?.Invoke(frame); } } private static ScaleFrame Decode(List<byte> b) { var ascii = Encoding.ASCII; if (!decimal.TryParse(ascii.GetString(b.ToArray(), 3, 6).Trim(), NumberStyles.Any, CultureInfo.InvariantCulture, out var w)) return null; int status = b[1] - 0x30; return new ScaleFrame { Stable = b[0] == 0x3D, IsNet = (status & 0b001) != 0, AtZero = (status & 0b010) != 0, Overload = (status & 0b100) != 0, Weight = w, Ts = DateTime.Now }; } }
五、抓重:仪表说"稳定"还不够,三个条件同时满足才采信
仪表的稳定判定只在"重量变化小于分度值并维持若干秒"时置位,车间现场有振动、有气流、有人碰台面,单帧 = 就抓值会抓到假象。工程上抓重要三个条件做 AND:
连续 N 帧帧头都是 =(不是 ~),单帧稳定不采信,防临界抖动。
最近 N 帧极差 ≤ 允许误差(如 1 个分度),且持续 300~500ms,挡住低频晃动。
PLC 到位信号有效(桶已就位/输送已停),且已过抓重延时,防止空抓、早抓。
// 抓重判定器:喂入连续帧,满足三条件时取窗口中值(比取末值更抗干扰) public class WeightGrabber { private readonly Queue<decimal> _window = new(); private readonly int _needFrames; private readonly decimal _tolerance; private DateTime _lastFrameTime = DateTime.Now; public decimal? TryGrab(ScaleFrame f, bool plcInPosition) { _lastFrameTime = f.Ts; if (!plcInPosition || f.Overload || f.AtZero) return null; _window.Enqueue(f.Weight); if (_window.Count > _needFrames) _window.Dequeue(); if (_window.Count < _needFrames || !f.Stable) return null; var vals = _window.OrderBy(x => x).ToList(); if (vals[^1] - vals[0] > _tolerance) return null; // 极差超差,继续观察 return vals[vals.Count / 2]; // 中值作为本次抓重结果 } } // needFrames 按发送频率定:约10帧/秒时取5帧;tolerance 取1个实际分度 // (如30kg秤分度10g,tolerance=0.01kg)。用标准砝码实测后固化参数。
六、净重、去皮与灌装落差补偿
- 两种净重获取方式:一是容器放上后发仪表去皮命令(清零皮重,之后显示净重,状态字 bit0 置净重);二是上位机自己存皮重——先抓一次空桶毛重 T,装料后抓总重 G,净重 = G − T。连桶批次固定时后者更灵活,桶重超差还能顺带检出"错用桶";
- 自动清零窗口要窄:空秤时若零漂在 ±0.25 分度内允许一键清零,超出就报警提示人工检查台面——无限制自动清零会慢慢吃掉真实偏差;
- 灌装两段加料 + 落差补偿:快加料到目标 95% 切慢加料,到达"目标量 − 落差提前量"即停阀。落差(阀门关闭后还在空中落进桶里的料)每次记录,提前量用最近若干批次的实测落差做滑动平均自学习,批次温差、料黏度变化都能跟住;
- 超差自动复检:净重不在目标 ±允差带内时,上位机不写合格记录,驱动复检辊道/报警,杜绝"差几斤也放行"。
七、异常与防作弊:把"不可能"变成可告警
| 异常 | 识别方法 | 处置 |
|---|---|---|
| 通讯断开 | Stopwatch 记录最后一帧时间,连续发送正常间隔约 50~150ms,800ms 无帧即判断线 | 界面置灰、声光报警,PLC 联锁停止灌装,不允许用断线前缓存值继续生产 |
| 超载/溢出 | 状态字 bit2,或重量字段为 'o.L' 类错误码 | 立即停止加料并报警,记录超载事件 |
| 零位漂移 | 空秤窗口长期偏离零点超过阈值 | 提示重新标定,禁止自动清零掩盖 |
| 重量突变/遥控作弊 | 抓重同时记录重量变化曲线;同一物料批次重量统计突变、非加料时段重量异常跳变 | 留曲线证据并报警;地磅场景加防遥控检测设备,软件只做发现与留证 |
| 乱码帧增多 | 解析失败计数/总帧数 比例升高 | 查接地、转换器、波特率;比例写进诊断界面,比等用户投诉快 |
八、多台秤组网:连续模式不行就切命令模式
连续发送没有站号,多台秤的数据在同一条总线上无法区分。三种组网方式按现场选:
- 每秤一个串口:4/8 口多串口卡或多个 USB 转串口,简单可靠,适合秤少(2~4 台)且距离近;
- 串口服务器:多口 RS232/485 转网关,上位机用 TCP 连每个端口,每个端口还是连续发送模式,适合车间分散布点;
- RS485 总线 + 命令应答:仪表设不同站号挂一条 485 总线(末端加 120Ω 终端电阻),上位机按站号轮询读重,每站超时重发 1 次后标记离线,适合地磅群、多工位称重。轮询周期 = 站数 × 单帧往返,站多了实时性下降要提前算账。
九、验收方法:用砝码说话
- 用标准砝码(满量程 20%、50%、80% 三点各)连续自动抓重 100 次,比对系统记录值与砝码值,误差必须稳定在 1 个分度内,最大值/最小值差反映抓重逻辑的抖动;
- 模拟干扰:轻敲台面、风扇直吹、人员走过,验证抓重不误触发、曲线被记录;
- 拔串口线 3 秒/30 秒各一次,验证 800ms 内报警、恢复后自动续采、断线期间业务被联锁;
- 灌装场景连做 50 桶,统计落差自学习后净重均值与标准差,标准差应收敛到允差带的 1/3 以内。
电子秤对接看似简单,做好的标准是重量来源唯一(系统记录即仪表真实值)、抓重时机可信(三条件联锁)、异常都有出口(断线/超载/漂移/突变)、数据可回溯(带曲线和帧日志)。做到这四点,称重工位就从质量纠纷的高发区,变成整条追溯链上最扎实的数据入口。
