简介:这份资源面向使用 WinForm 开发串口称重采集功能的 .NET 开发者,聚焦耀华 3190-A12+E 电子秤通过 RS232 串口读取重量的实现。代码区分两种工作模式:连续发送模式对应方案 2,指令模式对应方案 1,默认指令模式下可完成清零与读数操作,并提示先用超级终端验证电子秤能否正常向电脑发送数据,波特率 9600、其余参数默认即可,其他品牌电子秤需参照说明书调整。压缩包共 41 个文件,约 102KB,以 cs 源码、resx 与 resources 资源文件、config 配置、txt 说明及编译产物为主,构成可直接运行的完整 WinForm 工程。已有 2144 人学习下载,适合需要快速打通串口通信与称重采集链路的开发者参考复用。
1. 车间称重台前的那根串口线,到底怎么把重量读进 WinForm
去年帮一个做配料的小厂改称重工位,操作员每天要把电子秤上的数字手抄进 Excel,一天两百多笔,抄错一次整批配方就废。他们的秤是耀华 3190-A12+E,背后一个 RS232 接口,一根串口线连到工位机。需求说白了就一句话:WinForm 窗体上实时显示重量,点一下按钮把重量和批次号写进数据库。听起来像课后作业,真上手才发现坑不少——串口参数对不上收不到数、收上来的是一串十六进制看不懂、UI 线程被阻塞直接假死。这篇就把这个「winform 读取串口电子秤重量」的完整落地过程拆开讲,从 SerialPort 配置、耀华协议帧解析,到界面刷新和二次开发扩展,代码都能直接抄。适合手里有串口秤、想做个称重上位机的开发者,新手照着能跑通,熟手能直接拿去改。
2. 技术选型与工程骨架:WinForm + SerialPort 为什么够用
2.1 为什么不上第三方串口库
这个场景的技术栈其实很朴素:.NET Framework 4.x(或 .NET 6+ 的 Windows 桌面运行时)、WinForm、System.IO.Ports.SerialPort。有人会问要不要上什么串口通信框架,我的血泪经验是——读取电子秤这种单向、低频(每秒几帧到十几帧)、帧长固定的场景,SerialPort 自带的能力完全够,多引一个库只是多一层要排查的黑匣子。SerialPort 的 DataReceived 事件在后台线程触发,把字节读进缓冲区,主线程只负责解析和刷 UI,职责清晰。
选 WinForm 而不是 WPF,理由也很实际:工位机配置低,WinForm 启动快、控件少、部署就是一个 exe 加几个 dll,现场维护的人看得懂。真要上 WPF 也不是不行,但称重界面就几个 Label 和一个 DataGridView,没必要。
工程骨架我一般这么分:
ScaleReader/ ├── Forms/ │ └── MainForm.cs // 界面与事件绑定 ├── Core/ │ ├── ScalePort.cs // 串口封装:打开/关闭/收字节 │ ├── FrameParser.cs // 协议解析:字节流 -> 重量 │ └── WeightRecord.cs // 数据模型 ├── Config/ │ └── scale.config.json // 串口参数、协议类型 └── Program.cs把串口封装和协议解析从窗体里拆出来,是为了后面换秤型、换协议时不用动界面代码。这个拆分看着小题大做,但等你接到第二台不同品牌的秤就懂了。
2.2 串口参数怎么定:波特率、数据位、校验位
耀华 3190-A12+E 这类计价秤的串口输出,常见出厂配置是波特率 9600、8 位数据位、1 位停止位、无校验(8-N-1),连续发送模式。但注意,不同批次、不同设置菜单里可能被改成 4800 或 19200,校验位也可能被设成偶校验。所以参数不能写死,必须做成可配置,现场对不上就改配置重开,而不是重新编译。
// Core/ScalePort.cs public class ScalePort : IDisposable { private readonly SerialPort _port; public event Action<byte[]>? DataReceived; public ScalePort(string portName, int baudRate = 9600, Parity parity = Parity.None, int dataBits = 8, StopBits stopBits = StopBits.One) { _port = new SerialPort(portName, baudRate, parity, dataBits, stopBits) { ReadTimeout = 500, WriteTimeout = 500, // 关键:收到多少读多少,不要等缓冲区满 ReceivedBytesThreshold = 1 }; _port.DataReceived += OnDataReceived; } private void OnDataReceived(object sender, SerialDataReceivedEventArgs e) { try { int n = _port.BytesToRead; if (n <= 0) return; var buf = new byte[n]; _port.Read(buf, 0, n); DataReceived?.Invoke(buf); // 抛给解析层,别在这里做业务 } catch (Exception ex) { // 串口被拔、驱动异常都会走到这 Debug.WriteLine($"串口读取异常: {ex.Message}"); } } public void Open() => _port.Open(); public void Close() { if (_port.IsOpen) _port.Close(); } public void Dispose() { _port?.Dispose(); } }逐段说下关键点。ReceivedBytesThreshold = 1是让事件尽快触发,默认值在某些情况下会攒一批才通知,称重场景要的是实时。DataReceived里只做「读字节 + 抛事件」,绝不做解析和 UI 更新,因为它在后台线程,直接碰控件会抛跨线程异常。ReadTimeout设 500ms 是防止读操作卡死,虽然这里用的是BytesToRead判断,但保留超时是习惯。异常捕获不能省,现场工人拔线、换 USB 转串口,都会让读操作抛异常,不接住整个程序就崩。
2.3 配置外置:让现场能改参数
串口名(COM3 还是 COM5)、波特率这些,装到不同机器上都不一样。我一般用一个简单的 JSON 配置,启动时读,界面上再给个下拉框允许临时改。
// Config/scale.config.json { "portName": "COM3", "baudRate": 9600, "parity": "None", "dataBits": 8, "stopBits": "One", "protocol": "YaoHua" }读取用System.Text.Json或Newtonsoft.Json都行,反序列化成一个ScaleConfig类。这里有个小技巧:Parity、StopBits这些枚举,配置里存字符串,读的时候Enum.Parse,比存数字可读性好得多,现场维护的人改配置也不容易改错。协议类型也放配置里,是为了后面接第二种秤时,解析器能按类型分发,而不是改代码。
3. 协议解析核心:把一串十六进制变成能看的重量
3.1 耀华连续发送帧长什么样
耀华 3190 系列在连续发送模式下,常见是每帧固定若干字节,帧头有标识,中间是重量数值的 ASCII 或 BCD 编码,末尾带校验。不同型号、不同输出格式设置下帧结构会有差异,所以第一步永远是:拿串口助手把原始字节抓下来,看清楚再写解析。我一般让现场把秤调到连续发送,用串口工具抓几十帧,对比空秤、放 500g、放 2kg 三种状态,帧里哪几位在变,一眼就看出来了。
假设抓到的帧是这种结构(这是这类秤常见的形态,具体以实际抓包为准):帧头 0x02,接着若干位重量数据(ASCII 数字),然后单位标识,最后 0x03 结束。解析的核心就是「找帧头、取数据段、校验、转数值」。
// Core/FrameParser.cs public class FrameParser { private readonly List<byte> _buffer = new(); // 输入一段原始字节,输出解析出的重量(kg),解析不出返回 null public decimal? Feed(byte[] chunk) { _buffer.AddRange(chunk); // 帧头 0x02,帧尾 0x03,中间是数据 int start = _buffer.IndexOf(0x02); int end = _buffer.IndexOf(0x03); if (start < 0 || end < 0 || end <= start) { // 数据不完整,保留缓冲等下一批;缓冲过长则丢弃防内存涨 if (_buffer.Count > 256) _buffer.Clear(); return null; } // 取出完整帧,移除已消费部分 var frame = _buffer.GetRange(start, end - start + 1).ToArray(); _buffer.RemoveRange(0, end + 1); return ParseFrame(frame); } private decimal? ParseFrame(byte[] frame) { // 去掉帧头帧尾,取中间 ASCII 数字 var payload = frame.Skip(1).Take(frame.Length - 2).ToArray(); var text = Encoding.ASCII.GetString(payload); // 只保留数字和小数点,过滤单位字母、符号 var digits = new string(text.Where(c => char.IsDigit(c) || c == '.').ToArray()); if (decimal.TryParse(digits, out var value)) return value; return null; } }逻辑说明:Feed方法设计成「喂多少处理多少」,因为串口一次Read拿到的字节数不保证正好是一帧,可能半帧,也可能一帧半。用缓冲区累积,每次找帧头帧尾,凑齐一帧就解析并移除,剩下的留着等下次。这是串口解析最稳的写法,比「假设每次读到的就是一帧」可靠得多,后者在高速或系统繁忙时必翻车。
参数说明:0x02/0x03是这类秤常见的 STX/ETX 帧界定符,如果你的秤抓包发现是别的(比如 0x3D 开头),改这两个常量即可。ParseFrame里过滤非数字字符,是因为帧里常混着单位(kg/g)和状态位,直接decimal.TryParse会失败。如果你的秤输出的是 BCD 编码而非 ASCII,那就要按位取半字节换算,逻辑不同,得先确认编码方式。
3.2 校验位要不要算
有些型号的帧末尾带校验和(比如前面所有字节异或),有些没有。抓包时如果发现最后一字节随数据变化且不符合数值规律,大概率是校验。校验该算就算,算不过就丢帧,这是防止读到半帧错值的后悔药。异或校验的写法:
private bool CheckXor(byte[] frame) { // 假设最后一字节是校验,前面所有字节异或 byte sum = 0; for (int i = 0; i < frame.Length - 1; i++) sum ^= frame[i]; return sum == frame[^1]; }如果抓包确认没有校验位,这段就别加,加了反而把所有帧都判成错。判断依据只有一个:抓包对比。
3.3 把解析结果接到界面上
解析出来的重量要显示到 Label,还要处理「稳定值」的问题——秤在晃动时数值一直在跳,业务上通常要等数值稳定几百毫秒才采信。我一般加一个简单的稳定判定:连续 N 次解析结果差值小于阈值,才认为稳定。
// MainForm.cs 片段 private decimal? _lastStable; private int _stableCount; private void OnScaleData(byte[] chunk) { var weight = _parser.Feed(chunk); if (weight is null) return; // 跨线程更新 UI BeginInvoke(new Action(() => { lblWeight.Text = weight.Value.ToString("0.000") + " kg"; if (_lastStable.HasValue && Math.Abs(weight.Value - _lastStable.Value) < 0.002m) _stableCount++; else _stableCount = 0; _lastStable = weight.Value; btnConfirm.Enabled = _stableCount >= 5; // 稳定才允许确认 })); }BeginInvoke是 WinForm 跨线程更新控件的标准做法,别用Invoke死等,也别关掉跨线程检查(CheckForIllegalCrossThreadCalls = false)图省事,那是埋雷。稳定判定阈值 0.002kg 和次数 5 是经验值,秤的精度和刷新率不同要调,精度 1g 的秤阈值可以更小。
4. 避坑与排查:现场最容易翻车的五件事
4.1 打开串口报「拒绝访问」
现象:_port.Open()抛UnauthorizedAccessException,提示端口被占用。原因:COM 口是独占资源,串口助手没关、上一个进程没退干净、或者驱动虚拟出的口被别的软件占着。解决:先确认串口助手已关闭,任务管理器里看有没有残留进程,必要时拔插 USB 转串口让系统重新枚举。代码里Open要包 try-catch,失败给明确提示而不是崩掉。
4.2 能打开但一个字节都收不到
现象:串口打开成功,DataReceived从不触发。原因八成是参数不匹配——波特率、校验位和秤的设置不一致,或者秤根本没设成连续发送模式。解决:用串口助手用同样参数试,助手能收到说明是代码问题,收不到就是秤的设置或接线问题。RS232 还要注意 TX/RX 是否交叉、是否需要交叉线,接线错了数据方向就是反的。
4.3 收到的全是乱码或问号
现象:字节能收到,转成字符串是乱码。原因:编码假设错了,秤发的可能是 BCD 或二进制而非 ASCII,或者波特率偏差导致位错。解决:先按十六进制看原始字节,确认帧结构和编码方式,再决定用 ASCII 还是按位解析。波特率偏差通常表现为偶发错帧,而不是全乱。
4.4 界面卡死、按钮点不动
现象:称重时窗体无响应。原因:在DataReceived里直接操作控件,或者解析逻辑太重阻塞了后台线程,又或者用了Invoke同步等待。解决:后台线程只读字节,解析轻量化,UI 更新一律BeginInvoke异步投递。解析里别做数据库写入这种耗时操作,要写就丢到队列里由独立线程处理。
4.5 数值偶尔跳一下又恢复
现象:重量显示偶尔出现一个明显错误的值。原因:半帧被当成完整帧解析,或者校验没做。解决:确认缓冲区累积逻辑正确(3.1 的写法就是为此),有校验位的一定要校验,校验不过丢帧。另外 USB 转串口在电磁干扰大的车间容易丢字节,能上带隔离的转换器就上。
5. 二次开发:从「能读」到「能用在产线」
5.1 换秤型时改哪里
这套结构的好处是换秤只动FrameParser。新秤的帧结构不同,就新写一个解析类实现同一个接口,配置里protocol字段决定实例化哪个。我一般定义一个IFrameParser接口,Feed(byte[])返回decimal?,耀华一个实现,别的品牌各一个实现,窗体代码一行不改。这是当初拆层的回报。
5.2 加数据落库和批次管理
称重确认后要写库,别在 UI 线程里同步写。用一个BlockingCollection<WeightRecord>做生产者消费者:UI 线程把记录丢进队列,后台线程批量写数据库。批量写比逐条写快一个数量级,产线高峰期不会卡。表结构至少要有批次号、重量、时间戳、操作员,方便追溯。
// 后台写库线程 private void DbWorker() { var batch = new List<WeightRecord>(); foreach (var rec in _queue.GetConsumingEnumerable()) { batch.Add(rec); if (batch.Count >= 20 || _queue.Count == 0) { SaveBatch(batch); // 事务批量插入 batch.Clear(); } } }5.3 验证改造是否成功的三个动作
改完别只看界面显示对了就完事。第一,空秤归零、放标准砝码、取下,看数值是否线性准确;第二,快速连续放取,看稳定判定和按钮使能是否跟得上;第三,拔掉串口线再插回,看程序是否自动恢复还是需要重启——现场拔线是常态,能自动重连的版本才敢交付。我吃过一次亏,交付当天工人拔了下串口,程序直接白屏,从那以后每次交付前都强制走一遍拔插测试,重连逻辑也成了标配。希望这些能帮到你,少走我踩过的那些弯路。
本文还有配套的精品资源,点击获取