在工业分拣系统中,扫描枪的选择直接影响整个系统的效率和稳定性。我参与过多个大型物流分拣项目,发现很多工程师在选型时容易陷入误区——盲目选择市面上最常见的USB扫描枪。其实不同类型的扫描枪各有特点,需要根据具体场景选择。
目前主流的扫描枪分为三类:PS/2接口(已淘汰)、USB接口和RS232串口。USB扫描枪确实使用方便,即插即用,但它有个致命缺陷:必须在前台获得焦点才能工作。这意味着如果系统需要同时处理其他任务,或者需要隐藏扫描界面,USB枪就无法满足需求。而串口扫描枪虽然接线复杂些,但能完美解决这些问题。
去年我们为某汽车配件仓库设计分拣系统时,就遇到了典型的多枪协同需求。仓库有6个分拣工位,每个工位需要独立扫描配件条码,同时后台系统要实时记录哪个工位扫描了哪些配件。如果使用USB枪,要么需要6台电脑独立运行,要么要开发复杂的焦点切换逻辑——这两种方案都不够优雅。最终我们选择了RS232串口枪,通过串口号区分不同工位,一套程序就搞定了所有需求。
在C#中实现串口通信,首先要了解几个关键参数。我常用的SerialPort类封装了这些参数:
csharp复制SerialPort port = new SerialPort(
portName: "COM3", // 串口号
baudRate: 9600, // 波特率
parity: Parity.None, // 校验位
dataBits: 8, // 数据位
stopBits: StopBits.One // 停止位
);
实际项目中,这些参数必须与扫描枪的配置完全一致。有次调试时扫描枪一直没反应,排查半天发现是波特率设成了19200,而枪的实际波特率是9600。建议先用厂商提供的调试工具测试枪的参数,再在代码中配置。
多把串口枪同时工作需要解决两个问题:物理连接和逻辑区分。现在的工控机通常自带1-2个串口,要连接更多设备需要扩展。我们常用两种方案:
在某电商仓库项目中,我们使用了1台工控机连接8把扫描枪。具体配置是:主板自带2个串口 + 2块4口扩展卡。每把枪的串口号在设备管理器中固定分配,避免重启后串口号变化。
下面是一个完整的串口监听示例,包含错误处理和资源释放:
csharp复制public class BarcodeListener : IDisposable
{
private SerialPort _port;
public BarcodeListener(string portName)
{
_port = new SerialPort(portName, 9600);
_port.DataReceived += Port_DataReceived;
_port.ErrorReceived += Port_ErrorReceived;
}
private void Port_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
// 注意:此事件在辅助线程触发
string barcode = _port.ReadExisting().Trim();
Console.WriteLine($"从{_port.PortName}收到条码:{barcode}");
}
private void Port_ErrorReceived(object sender, SerialErrorReceivedEventArgs e)
{
// 处理奇偶校验错误等
LogError($"串口{_port.PortName}错误:{e.EventType}");
}
public void Start() => _port.Open();
public void Stop() => _port.Close();
public void Dispose()
{
_port?.Dispose();
}
}
使用时创建多个监听实例即可:
csharp复制var listener1 = new BarcodeListener("COM3");
var listener2 = new BarcodeListener("COM4");
listener1.Start();
listener2.Start();
工业环境中,串口数据可能因干扰出现异常。我们总结了几种常见问题及解决方案:
改进后的数据接收方法:
csharp复制private void Port_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
try {
string barcode = _port.ReadLine().Trim();
if(!string.IsNullOrEmpty(barcode)) {
// 将数据放入队列处理,避免阻塞串口线程
_queue.Enqueue((_port.PortName, barcode));
}
}
catch(TimeoutException) {
LogWarning($"串口{_port.PortName}读取超时");
}
}
工业现场难免会遇到设备重启或线路松动。传统的SerialPort类在拔掉设备时会抛出异常。我们通过定期检测和自动重连来解决:
csharp复制private Timer _checkTimer;
void StartCheckTimer()
{
_checkTimer = new Timer(state => {
if(!_port.IsOpen && IsPortAvailable(_port.PortName)) {
try {
_port.Open();
LogInfo($"串口{_port.PortName}重新连接成功");
} catch { /* 记录日志 */ }
}
}, null, 0, 5000); // 每5秒检查一次
}
bool IsPortAvailable(string portName)
{
return SerialPort.GetPortNames().Contains(portName);
}
在快节奏的分拣线上,可能因操作失误重复扫描。我们添加了两个保护措施:
csharp复制private Dictionary<string, DateTime> _lastScans = new Dictionary<string, DateTime>();
bool IsValidBarcode(string barcode, string portName)
{
// 基础校验
if(barcode.Length < 8 || barcode.Length > 20) return false;
// 去重检查
if(_lastScans.TryGetValue(barcode, out var lastTime)) {
if((DateTime.Now - lastTime).TotalSeconds < 5) return false;
}
_lastScans[barcode] = DateTime.Now;
return true;
}
在多枪高并发场景下,我发现直接在主线程处理数据会导致界面卡顿。最终的解决方案是:
csharp复制private BlockingCollection<(string, string)> _queue = new BlockingCollection<(string, string)>();
// 启动处理线程
Task.Run(() => {
foreach(var (portName, barcode) in _queue.GetConsumingEnumerable()) {
ProcessBarcode(portName, barcode);
}
});
void ProcessBarcode(string portName, string barcode)
{
// 这里可以访问数据库或调用业务接口
var workstation = GetWorkstationByPort(portName);
SaveToDatabase(workstation, barcode, DateTime.Now);
}
经过多个项目迭代,我总结出一个稳定的分层架构:
核心接口设计:
csharp复制public interface IScannerService
{
event Action<string, string> OnBarcodeScanned;
void StartAll();
void StopAll();
}
public interface IBarcodeRepository
{
void SaveScanRecord(string portName, string barcode);
IEnumerable<ScanRecord> GetTodayRecords();
}
将串口配置放在appsettings.json中,便于现场调整:
json复制{
"ScannerSettings": {
"Ports": [
{
"PortName": "COM3",
"BaudRate": 9600,
"Workstation": "A区1号工位"
},
{
"PortName": "COM4",
"BaudRate": 9600,
"Workstation": "A区2号工位"
}
]
}
}
对应的配置类:
csharp复制public class ScannerConfig
{
public List<PortConfig> Ports { get; set; }
}
public class PortConfig
{
public string PortName { get; set; }
public int BaudRate { get; set; }
public string Workstation { get; set; }
}
当多个串口同时高频工作时,可能会遇到系统资源不足。我们的优化经验:
csharp复制_port.ReceivedBytesThreshold = 1; // 收到1字节就触发事件
_port.ReadBufferSize = 4096; // 4KB接收缓冲区
在更复杂的自动化分拣线上,可能需要与PLC协同工作。我们通过串口或OPC UA实现:
示例代码片段:
csharp复制public class PlcController
{
private SerialPort _plcPort;
public void SendToPlc(string command)
{
byte[] frame = BuildModbusFrame(command);
_plcPort.Write(frame, 0, frame.Length);
}
private byte[] BuildModbusFrame(string cmd)
{
// 实现Modbus RTU帧构建
}
}
这种方案在某汽车零部件分拣项目中实现了每小时2000+次的分拣效率,错误率低于0.1%。