配料灌装、包装复检、收货过磅,凡是涉及重量的工位,最怕的就是"人眼看仪表、手抄进系统":抄错一位数、趁乱多记少记、出了质量问题查无对证。电子秤和地磅几乎都带串口,把重量实时、稳定、可信地读进上位机并不复杂,但有两个坑必须绕开:协议帧要按字节拆对,重量要等真正稳定再抓。这篇以工厂保有量最大的耀华 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] 帧头兼稳定标志
w[1] 状态字1
n[2] 状态字2
zz[3-8] 6位重量ASCII
sp[9] 分隔
CR[10] 0x0D
LF[11] 0x0A

▲ 逐字节含义(固件版本不同会有细微差异,落地时以随表通讯协议手册为准,但帧头判定和拼包方法通用):

字节取值含义
[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帧结束回车换行,可靠的切帧标记
状态字读位不读值。w 的取值是按位叠加的,例如 '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
        };
    }
}
这套状态机对所有"帧头+定长+帧尾"仪表通用。换成柯力、托利多(如 MT 连续输出)或其他品牌,只是帧头、长度、重量偏移不同,解析骨架不用动。拿任何新仪表,第一件事是用串口助手抓 5 分钟原始字节流存日志,对照手册把偏移量标定准确再写代码。

五、抓重:仪表说"稳定"还不够,三个条件同时满足才采信

仪表的稳定判定只在"重量变化小于分度值并维持若干秒"时置位,车间现场有振动、有气流、有人碰台面,单帧 = 就抓值会抓到假象。工程上抓重要三个条件做 AND:

条件1:仪表稳定

连续 N 帧帧头都是 =(不是 ~),单帧稳定不采信,防临界抖动。

条件2:软件静止窗口

最近 N 帧极差 ≤ 允许误差(如 1 个分度),且持续 300~500ms,挡住低频晃动。

条件3:工艺联锁

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' 类错误码立即停止加料并报警,记录超载事件
零位漂移空秤窗口长期偏离零点超过阈值提示重新标定,禁止自动清零掩盖
重量突变/遥控作弊抓重同时记录重量变化曲线;同一物料批次重量统计突变、非加料时段重量异常跳变留曲线证据并报警;地磅场景加防遥控检测设备,软件只做发现与留证
乱码帧增多解析失败计数/总帧数 比例升高查接地、转换器、波特率;比例写进诊断界面,比等用户投诉快
断线时用旧值显示是危险设计。有些程序为了界面"不跳",超时后保持最后一个重量——操作员会以为秤还是好的,继续过磅。超时后必须明确显示"通讯异常"并冻结业务动作。

八、多台秤组网:连续模式不行就切命令模式

连续发送没有站号,多台秤的数据在同一条总线上无法区分。三种组网方式按现场选:

  1. 每秤一个串口:4/8 口多串口卡或多个 USB 转串口,简单可靠,适合秤少(2~4 台)且距离近;
  2. 串口服务器:多口 RS232/485 转网关,上位机用 TCP 连每个端口,每个端口还是连续发送模式,适合车间分散布点;
  3. RS485 总线 + 命令应答:仪表设不同站号挂一条 485 总线(末端加 120Ω 终端电阻),上位机按站号轮询读重,每站超时重发 1 次后标记离线,适合地磅群、多工位称重。轮询周期 = 站数 × 单帧往返,站多了实时性下降要提前算账。

九、验收方法:用砝码说话

  1. 用标准砝码(满量程 20%、50%、80% 三点各)连续自动抓重 100 次,比对系统记录值与砝码值,误差必须稳定在 1 个分度内,最大值/最小值差反映抓重逻辑的抖动;
  2. 模拟干扰:轻敲台面、风扇直吹、人员走过,验证抓重不误触发、曲线被记录;
  3. 拔串口线 3 秒/30 秒各一次,验证 800ms 内报警、恢复后自动续采、断线期间业务被联锁;
  4. 灌装场景连做 50 桶,统计落差自学习后净重均值与标准差,标准差应收敛到允差带的 1/3 以内。

电子秤对接看似简单,做好的标准是重量来源唯一(系统记录即仪表真实值)、抓重时机可信(三条件联锁)、异常都有出口(断线/超载/漂移/突变)、数据可回溯(带曲线和帧日志)。做到这四点,称重工位就从质量纠纷的高发区,变成整条追溯链上最扎实的数据入口。