ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

C#对接扫码枪:USB键盘模式与串口通信全攻略

C#对接扫码枪:USB键盘模式与串口通信全攻略 简介C#扫码枪完整WinForms工程示例基于.NET Framework架构面向需要为Windows桌面应用接入扫码枪的C#开发者解决USB与串口两类常见连接问题适用于库存管理、生产扫码录入等场景。压缩包内共29个文件以12个C#源文件为核心配套窗体资源、配置文件、可执行程序与调试文件压缩包仅67KB结构清晰便于阅读和改造。目前已有7450人下载学习适合从入门到进阶的C#开发者参考。串口部分使用SerialPort类配置波特率与数据位通过DataReceived事件实时接收扫码数据USB部分演示设备枚举和Windows底层API调用并引入钩子扫描、自定义事件参数完整串起从硬件触发到界面显示的链路。开发者可将输送数据的事件封装、双窗体测试项目作为参考模板快速集成到自有项目中也可作为学习C#硬件交互的实用范例。 第一次接到扫码枪对接需求的时候我花了一上午在找 Honeywell 的官方 SDK甚至差点用 USBPcap 去抓包看 USB 协议。后来被旁边的老工程师一句话点醒扫码枪在电脑眼里要么是一把键盘要么是一个串口设备仅此而已。这句话直接改变了我的实现路径。C# 对接扫码枪不管设备是 USB 口还是 DB9 串口核心问题就两个数据从哪个管道进来一条完整条码从哪里结束。搞懂这两件事代码怎么写都顺。这篇文章我把这些年用 C# 写扫码枪接收程序的经验整理出来重点覆盖 USB 模拟键盘和串口两种接入方式包括代码实现、串口参数、驱动坑和现场故障排查。内容面向正在做上位机扫码录入、仓库盘点、产线追溯系统的开发者尤其适合那种第一次接触扫码枪、对着设备管理器发懵的新手。我会把硬件原理、代码细节和踩坑经历掺在一起讲尽量让你看完就能直接上手。1. 先搞懂扫码枪的两个“身份”USB键盘还是串口设备1.1 为什么说USB扫码枪本质是一把键盘大部分 USB 接口的扫码枪默认工作模式叫“USB 键盘仿真”行业内也叫 Keyboard Wedge 模式。扫码枪把条码解码之后不是像你想的那样通过 USB 自定义协议把数据“发给”应用而是模拟键盘按键把条码内容一个字符一个字符地敲进电脑。所以在操作系统看来它就是一把外接键盘。这解释了为什么扫码枪插上电脑之后你随便开一个记事本对准条码扣一下扳机条码字符会像有人在打字一样蹦出来最后还会自动换行。最后一个换行就是扫码枪在“按回车”。对于 WinForms、WPF 程序来说你要做的不是在 USB 协议层面拦截数据而是接收这些键盘动作组合成完整的条码字符串。另一种是串口扫码枪。它解码之后直接把数据通过串口逐字节发送出去通常走 RS232 电平也有把 TTL 电平直接引到主板上给嵌入式设备用的模组。电脑端必须有一个 COM 口来接收这个 COM 口可能是主机自带的 DB9 串口也可能是 USB 转串口芯片虚拟出来的。1.2 拿到一台扫码枪怎样判断是哪种工作模式判断方法很简单把扫码枪插到电脑上打开设备管理器看“键盘”和“端口 (COM 和 LPT)”两个分类。如果“键盘”下面多出来一个 HID Keyboard Device这台枪现在就工作在 USB 键盘仿真模式。如果“端口”下面多出来一个 COM 口设备名通常长这样USB-SERIAL CH340 (COM4)、USB Serial Port (COM4)它工作在串口模式。还有一种情况设备管理器里出现“HID-compliant device”或者带黄色感叹号的“USB Serial”说明它可能处于配置模式或者驱动没装上。很多工业扫码枪两种模式都支持出厂默认只是选了其中一种。Honeywell 的不少型号默认是 USB 键盘模式插上就能用但如果你希望程序能主动控制扫码枪、读取扫描状态或者让多个扫码枪同时接在一台电脑上还能区分彼此就需要用配置码把它切换成“USB 串口”模式。切换的方法在第 5 章单独讲。1.3 USB和串口到底选哪种我的选择原则可以浓缩成一句话能用 USB 键盘模式解决的就不要上串口程序需要感知扫描过程或者有多把扫码枪接在同一台机器上的优先用串口。USB 键盘模式最大的好处是零驱动、零配置代码也极其简单。缺点是它和用户的键盘输入混在一起程序在后台运行时很难区分哪个字符是扫码枪发的、哪个字符是用户敲的。另一个硬伤是一台电脑接两把以上 USB 键盘模式扫码枪时它们会同时往系统里发键盘事件你根本分不清数据来自哪把枪。串口模式的好处是数据管道独立哪个 COM 口对应哪把枪清清楚楚程序还能反向给扫码枪发命令比如控制激光开关、切换码制。缺点是要处理驱动、串口参数、数据分包这些问题。而且从实际效果看USB 转串口芯片的虚拟串口在数据实时性上不如原生串口数据偶尔会被切成两段代码里必须做拼接处理。2. USB接入方式把扫码枪当作键盘来接收条码2.1 最简单的表单实现TextBox聚焦加回车判断如果你的程序界面里有个输入框扫码员扫码前把鼠标点进输入框那用 TextBox 的 KeyPress 事件就够了。扫码枪默认会在条码末尾发一个回车键这个回车就是完整数据的结束标志private void txtBarcode_KeyPress(object sender, KeyPressEventArgs e) { if (e.KeyChar (char)13) { e.Handled true; string barcode txtBarcode.Text.Trim(); if (barcode.Length 0) { HandleBarcode(barcode); } txtBarcode.Clear(); } }逻辑非常直白检测到回车就把输入框里的内容当作一条条码处理然后清空输入框等下一条。这里有两个细节值得注意。第一为什么用 KeyPress 而不是 KeyDown。因为 KeyPress 处理的是字符回车在 KeyDown 里拿到的虚拟键码 Keys.Enter 代表不了一个完整的字符输入而 KeyPress 能拿到 (char)13语义更准确。第二必须设置 e.Handled true否则这个回车还会继续触发按钮点击或者让焦点跳走界面逻辑容易乱。这个方案的缺点也很明显扫码员必须手动点击输入框。真实工厂里工人手里可能抱着箱子眼睛还要看货物和单子让他扫码前专门去点一下输入框效率低不说还容易漏扫。如果你的应用场景是固定工位录入可以接受这个交互那它就是最稳的方案。我一般会在窗体的 Activated 事件里把焦点强制设回 txtBarcode防止扫码员不小心点到别处导致条码被输进了错误的地方。2.2 后台运行的程序需要低级键盘钩子很多项目场景里根本没有输入框程序就是一个后台监控窗口扫码枪随时可能扫进来焦点也可能在任何控件上。这时候要用低级键盘钩子WH_KEYBOARD_LL在系统层面拦截所有键盘输入把扫码枪发出的字符按顺序拼起来遇到回车就算一条完成。using System.Runtime.InteropServices; using System.Text; public class KeyboardScanner : IDisposable { private const int WH_KEYBOARD_LL 13; private const int WM_KEYDOWN 0x0100; private readonly HookProc _proc; private IntPtr _hookId IntPtr.Zero; private readonly StringBuilder _buffer new StringBuilder(); public event EventHandlerstring BarcodeScanned; private delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam); [DllImport(user32.dll, SetLastError true)] private static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, uint dwThreadId); [DllImport(user32.dll)] private static extern bool UnhookWindowsHookEx(IntPtr hhk); [DllImport(user32.dll)] private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam); [DllImport(kernel32.dll, CharSet CharSet.Auto, SetLastError true)] private static extern IntPtr GetModuleHandle(string lpModuleName); public void Start() { _proc HookCallback; using (Process curProcess Process.GetCurrentProcess()) using (ProcessModule curModule curProcess.MainModule) { _hookId SetWindowsHookEx(WH_KEYBOARD_LL, _proc, GetModuleHandle(curModule.ModuleName), 0); } } private IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam) { if (nCode 0 wParam (IntPtr)WM_KEYDOWN) { int vk Marshal.ReadInt32(lParam); if (vk (int)Keys.Enter) { string barcode _buffer.ToString(); if (barcode.Length 0) { BarcodeScanned?.Invoke(this, barcode); } _buffer.Clear(); } else if (vk (int)Keys.D0 vk (int)Keys.D9) { _buffer.Append((char)(0 vk - (int)Keys.D0)); } else if (vk (int)Keys.NumPad0 vk (int)Keys.NumPad9) { _buffer.Append((char)(0 vk - (int)Keys.NumPad0)); } else if (vk (int)Keys.A vk (int)Keys.Z) { _buffer.Append((char)(A vk - (int)Keys.A)); } // 条码里常见的短横线、点号等根据实际按键补充 else if (vk (int)Keys.OemMinus) { _buffer.Append(-); } else if (vk (int)Keys.OemPeriod) { _buffer.Append(.); } } return CallNextHookEx(_hookId, nCode, wParam, lParam); } public void Stop() { if (_hookId ! IntPtr.Zero) { UnhookWindowsHookEx(_hookId); _hookId IntPtr.Zero; } } public void Dispose() { Stop(); } }使用方式也简单窗口加载时 new 一个 KeyboardScanner订阅 BarcodeScanned 事件Start() 开始监听窗口关闭时 Stop() 或 Dispose()。需要注意低级键盘钩子必须跑在带有消息循环的线程上WinForms 主窗体里直接调用没问题但如果你在控制台程序里写必须启动 Application.Run() 之类的消息循环否则钩子回调永远不会触发。2.3 USB方式实战中常踩的三个坑第一个坑是中文输入法。用户把输入法切到中文状态下扫码枪输出的数字和字母一旦进入有输入焦点的控件就可能被输入法拦截甚至变成全角字符导致条码内容完全不对。低级键盘钩子方案不受输入法影响这是它的一个额外优势但如果你用 TextBox 方案最好在状态栏上做个提示或者用代码临时切换到英文输入法。第二个坑是扫码枪不带回车后缀。大部分扫码枪出厂默认带 CR但有些枪或者某些配置下不带条码内容发完就结束了程序端永远等不到回车这个结束信号。解决方法是扫说明书里的“加回车后缀/Data Suffix”配置码或者在代码里做超时判断收到字符后 100 毫秒内没有新数据就认为一条条码结束。代码逻辑不难推荐两种都做以回车为主、超时兜底。第三个坑是全局钩子把用户正常键盘输入也当成了条码。这是低级键盘钩子方案绕不开的问题。我的做法是给应用设计一个“扫码模式”切换开关扫码时开启钩子平时关闭或者利用扫码枪的输入速度做过滤一次扫码中字符间隔通常不会超过 30 毫秒人工打字不可能这么快超过间隔就清空缓冲区重新开始。后者的体验更好但代码复杂度会上去看项目需求决定。3. 串口接入方式SerialPort读取与数据分包处理3.1 开发前先用串口调试助手验证硬件链路串口方式的第一步不是写代码而是确认硬件链路本身没问题。把扫码枪接上电脑装好驱动后打开串口调试助手选择对应的 COM 口波特率如果不知道就先用 9600然后对准条码扫一下。调试助手里如果能正常看到条码内容和回车换行再开始写程序。这一步能帮你排除掉至少一半的问题。很多人代码写了一大堆最后发现是波特率不对、接线接反、或者扫码枪根本没切到串口模式。用串口调试助手验证过后代码里只要照着调试助手的参数填就行后面的问题几乎都集中在数据处理上了。3.2 SerialPort初始化和DataReceived事件绝大多数扫码枪出厂串口参数是 9600、8、N、1也就是波特率 96008 位数据无校验1 位停止位。也有部分工业枪是 115200 或者 19200一切以说明书为准。初始化代码如下SerialPort port new SerialPort(COM4, 9600, Parity.None, 8, StopBits.One); port.Encoding Encoding.UTF8; port.NewLine \r; port.ReceivedBytesThreshold 1; port.DataReceived Port_DataReceived; port.ErrorReceived Port_ErrorReceived; port.Open();Encoding 值得单独说。普通一维条码和大部分二维码是纯 ASCII 字符用默认编码问题不大但如果项目里扫的是带中文内容的二维码比如某些导入导出场景一定要根据扫码枪设置的编码来选常见的是 UTF-8 或 GBK。选错编码读出来的中文就是乱码而且只看数据很难看出来是编码问题还是传输问题。DataReceived 事件在后台线程触发不能在里面直接操作 UI 控件。Update 界面的正确姿势是用 BeginInvoke 把处理逻辑扔回 UI 线程private void Port_DataReceived(object sender, SerialDataReceivedEventArgs e) { string data port.ReadExisting(); if (string.IsNullOrEmpty(data)) return; BeginInvoke(new Action(() { ProcessData(data); })); }有人会问用 Invoke 还是 BeginInvoke。我的建议是 BeginInvoke它不阻塞 DataReceived 线程扫码枪数据量不算大异步调用积压的可能性很低而且不容易因为 UI 卡死导致串口缓冲区的数据越积越多。3.3 数据分包的本质原因与缓冲区拼接方案串口方案里最经典的坑是数据分包。同样的代码在一台机器上跑得好好的换到另一台用 USB 转串口芯片的电脑上条码忽然被切成两截甚至三截接收。原因是 DataReceived 事件并不代表一条完整数据到达它只表示串口缓冲区里有字节可读。虚拟串口芯片的驱动缓冲策略、系统线程调度、USB 传输的时延都会把一次扫码输出拆成多个批次。所以不要一进事件就把 ReadExisting 的内容直接当作完整条码而是把数据追加到缓冲区再用回车或换行符做分割private readonly StringBuilder buffer new StringBuilder(); private void Port_DataReceived(object sender, SerialDataReceivedEventArgs e) { string chunk port.ReadExisting(); buffer.Append(chunk); string cached buffer.ToString(); int idx; while ((idx cached.IndexOf(\r)) 0 || (idx cached.IndexOf(\n)) 0) { string barcode cached.Substring(0, idx).Trim(); if (barcode.Length 0) { BeginInvoke(new Action(() ProcessData(barcode))); } cached cached.Substring(idx 1); } buffer.Clear(); buffer.Append(cached); }这里的 while 循环是关键。有的扫码枪发 \r\n有的只发 \r有的只发 \n这个写法三种情况都能正确分割。而且多余的去处也处理干净如果这次收到的数据里没有结束符说明只来了一半剩下的残片留在缓冲区里等下一批数据到达时一起拼出来。很多扫码枪接收程序的偶发性丢码、乱码根源就是没做这层拼接。如果你需要更底层的字节级控制可以改用 port.Read(byte[], int, int) 读原始字节再按指定编码转成字符串。但实际项目中 ReadExisting 加缓冲区拼接完全够用除非你在做特殊编码协议否则没必要把字节数组那套逻辑引入进来。3.4 串口异常、设备拔插和自动重连串口程序运行时最怕两种状况扫码枪被拔掉、串口被其他程序占用。前者会触发 IOException后者会抛 UnauthorizedAccessException。实现的时候可以在事件里统一捕获private void Port_ErrorReceived(object sender, SerialDataReceivedEventArgs e) { // 通知UI线程串口异常准备重连 }另外建议做一个简单的自动重连机制用定时器每隔一段时间检查 port.IsOpen如果发现端口断开但 COM 号仍然存在就尝试重新 Open如果 COM 号都没了就提示用户检查扫码枪接线。我习惯在界面上放一个串口状态指示灯绿色表示正常红色表示断开。扫码员不需要知道什么是 COM 口但红点亮起来的时候他会知道该喊 IT 了。这个小设计在产线上非常实用比在日志文件里记异常有用得多。4. 驱动与设备识别CH340、FTDI等硬件基础必须掌握4.1 扫码枪里的USB转串口芯片和驱动问题串口扫码枪如果走 USB 口里面通常装了一颗 USB 转串口芯片常见的有 CH340、FT232R、FT231X、PL2303、CP2102。芯片负责把 USB 信号翻译成串口信号于是电脑设备管理器里会出现一个 COM 口名字多半是 USB-SERIAL CH340 (COM4) 这样的格式。驱动问题随之而来。Win10 和 Win11 一般会自动安装 CH340 或 FTDI 的驱动但也会翻车插上去设备管理器里出现一个带黄色感叹号的“USB Serial”设备。解决办法是去芯片厂商官方下载驱动手动安装。CH340 搜“CH340 串口驱动”就有FTDI 的 FT232R 和 FT231X 用的是同一套 VCP 驱动不要因为芯片名字不熟悉就慌。PL2303 是个历史遗留坑老版本芯片在 Win10 之后的系统上驱动兼容性很差经常出现装好驱动但设备不可用的状况。如果你手头的老扫码枪是 PL2303 方案建议直接换新枪或者用支持新系统的 CH340/FTDI 转换线别跟一个过时芯片死磕。4.2 用代码枚举串口并识别哪个是扫码枪程序里获取可用串口最简单的方法是 SerialPort.GetPortNames()但它只能返回 COM3、COM5 这样的名字。如果电脑上同时接了扫码枪、打印机、PLC 等多个串口设备用户根本分不清该选哪个。想拿到设备描述可以用 System.Management 查询系统设备信息using System.Management; var searcher new ManagementObjectSearcher( SELECT Name FROM Win32_PnPEntity WHERE Name LIKE %(COM%); foreach (ManagementObject obj in searcher.Get()) { string name obj[Name]?.ToString(); // name 形如 USB-SERIAL CH340 (COM4) // 用正则从括号里提取 COM4 即可 }注意System.Management 在 .NET Framework 里默认可用在 .NET 5/6/8 项目里需要从 NuGet 安装 System.Management 包。这个功能做成一个“刷新串口”按钮在程序启动和扫码枪重新插拔后各调用一次用户界面下拉框里就能直接显示“USB-SERIAL CH340 (COM4)”这样的可读信息而不会只裸奔一个 COM4。4.3 硬件接线和芯片选型里的两个提醒第一个提醒是电平问题。原生 DB9 串口输出的是 RS232 电平电压范围通常到 ±12V扫码枪开发板或模组上引出的可能是 TTL 电平3.3V 或 5V。把 TTL 直接接到 RS232 接口上轻则读不到数据重则烧芯片。如果用的是 USB 转 TTL 模块接扫码枪要确认模块和扫码枪都是 TTL 电平并且 TX、RX 交叉连接。交叉连接这个细节很多第一次接线的人会栽在上面。第二个提醒是 USB 转串口芯片的性能差异真实存在。CH340 在低速场景下挺好用但在高频连续扫码时偶尔会丢字节FTDI 的芯片在工业场景里明显更稳。如果项目要求快速连续扫一堆条码比如整箱扫描录入尽量选 FTDI 方案的扫码枪或者 USB 转串口模块别在这种长期稳定性的环节上省成本。5. Honeywell等工业扫码枪的配置与故障排查5.1 “灯亮但扫不出来”的完整排查链路“扫码枪灯亮但扫不出来”是现场报修率最高的问题而且每次的根因可能都不一样。我一般按下面这个顺序排查基本能覆盖绝大多数情况判断是否进入了配置模式。扫描枪如果之前被人扫过配置码可能停留在配置模式下这时它不响应正常扫码。处理办法是扫一下“退出配置”或“恢复出厂设置”配置码。确认触发方式。很多工业枪支持手动、连续、自动感应三种触发方式。如果被切成自动感应手动按扳机可能就不出激光或者激光一直在闪但无法锁定条码。确认条码码制是否开启。有些枪默认只解码 Code128、EAN/UPC如果目标条码是 DataMatrix、PDF417 或者其他不常见码制需要在配置项里手动打开。检查条码本身的质量。镜面反光、标签褶皱、条码破损、遮挡都会导致解码失败。这个很多人想不到拿手机闪光灯照一下条码表面常常就发现问题了。回看第 1 章的身份判断方法。如果枪现在工作在 USB 键盘模式而程序在等串口数据那当然永远扫不出来。我自己遇到过一回挺有代表性的。客户报修说一把 Honeywell 枪昨天还好好的今天插上就扫不了。我到现场一看灯是亮的但扫码完全没有反应。后来翻说明书对了一遍配置码发现是有人不小心扫到了“启用自动感应模式”那一项扳机触发被禁用了。扫回“手动触发模式”配置码问题当场解决。碰上这种“昨天还能用今天不行”的案例十有八九是配置码被误扫先恢复出厂设置再根据需求重新配置是最快的解。5.2 用配置码和配置二维码调整输出格式与模式工业扫码枪的输出行为基本都可以通过扫配置码来调整。以 Honeywell 为例说明书里有专门一节讲 Data Suffix里面有“添加 CR (Enter) 后缀”的配置码扫一下之后每次扫码输出完条码内容会自动追加一个回车程序端就能用回车来判断数据结束。还有切换 USB 模式的配置码可以让枪在 USB 键盘模式和 USB 串口模式之间互相切换。有些新型号直接扫二维码格式的配置码原理一样形式从一维条码换成了二维码而已。这里最关键的教训是不同型号的配置码不通用同一品牌不同系列的枪配置码有时都完全不同。务必找到对应型号的说明书。我现在的项目部署包里一定会放一份常用型号的说明书 PDF手机里也会存一份现场调试的时候直接翻不用到处找人要。给交付工程师一个建议扫码枪的配置码是“恢复出厂设置”优先。现场无论之前被人改过什么先扫一个恢复出厂设置再从零按需求配。这比自己猜测是哪个配置项被改过要快得多。恢复出厂之后把回车后缀、模式切换、需要的码制这几项配置好问题基本就终结了。5.3 扩展场景扫码枪对Excel数据匹配比对的实现思路最后讲一个和扫码枪结合最常见的衍生需求扫一批条码和一张 Excel 清单做比对判断哪些存在、哪些不存在、哪些重复。仓库盘点、来料检验、发货复核都会碰到。在 C# 里比较推荐的做法是程序启动时把 Excel 数据读进 DataTable然后按条码列建一个 Dictionary扫描时直接用字典查找private Dictionarystring, string barcodeMap new Dictionarystring, string(StringComparer.OrdinalIgnoreCase); public void LoadExcelData(DataTable table, string barcodeColumn) { barcodeMap.Clear(); foreach (DataRow row in table.Rows) { string code row[barcodeColumn].ToString().Trim(); if (!barcodeMap.ContainsKey(code)) { barcodeMap[code] code; } } } public bool Exists(string barcode) { return barcodeMap.ContainsKey(barcode.Trim()); }用 Dictionary 做查找几万条数据也能毫秒级返回结果。每次扫码进来直接判断是否在清单里然后播放不同声音反馈。想处理重复扫描也很简单把 Dictionary 的值换成计数器扫到第二次时提示重复即可。说回扫码枪本身我最后再分享一个坚持了很多年的小习惯扫码程序一定要做成功和失败的声音反馈。扫码员在仓库里不可能一直盯着屏幕声音是最高效的反馈通道。哪怕界面做得再简陋扫码成功“滴”一声、失败“嘟嘟”两声现场的误扫率立刻就能降下来。这个细节是我在产线项目里吃过亏之后总结出来的写在这里供你参考。本文还有配套的精品资源点击获取
返回列表