ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用VB.NET从零开发自定义串口调试助手:原理、实现与避坑指南

用VB.NET从零开发自定义串口调试助手:原理、实现与避坑指南 简介VB.NET编写的串口助手源码包适合正在学习VB.NET串口通信或需要自制串口调试工具的开发者。相比网上常见的AXcom控件方案作者选用SerialPort类实现并处理了委托与线程的关系代码风格简洁亲测可用兼具参考性与实用性。资源共37个文件包括6个vb源码文件、5个config配置文件、4个exe可执行程序、2个txt说明文档以及pdb调试符号、resx资源文件、sln工程文件等压缩包整体仅181KB结构清晰便于直接打开和对比学习。已有519人学习。通过这份资源读者可以完整查看窗体设计、SerialPort配置与数据收发逻辑理解跨线程更新UI的委托写法还可以直接运行exe体验功能。包内还保留了App.config、My Project等工程配置适合用Visual Studio打开后边看边改快速搭建属于自己的串口助手也可作为课程设计或工具开发的起步模板。 从大学第一次调单片机开始我就对串口助手这类工具有一种又爱又恨的感觉。爱的是它们能让我直接看到设备吐出来的数据恨的是市面上那些串口调试助手总是差那么一点意思——要么界面老气横秋要么收发数据时偶发卡顿想加个自动保存日志和CRC校验功能还得付费。后来用VB.NET自己动手写了一个串口助手从串口参数配置、十六进制收发到数据缓存、自动发送、日志记录全部都按自己的使用习惯来。这个项目做下来不仅解决了我调试设备的实际痛点还把我对.NET中SerialPort控件、多线程委托、编码处理的理解往上拉了一大截。这篇文章我就把整个开发过程和踩过的坑完整记录下来想自己写一个串口助手的朋友可以直接照着做也可以在这个基础上继续扩展成更适合自己的调试工具。1. 为什么放着现成的串口调试工具一大堆我还是要自己写一个1.1 现成工具的问题功能过剩或者功能缺失市面上能下载到的串口调试助手非常多从SSCOM、XCOM到各种友善版常兴版基本功能都差不多——选择串口号、配置波特率、打开串口、发送接收数据、十六进制显示。但这些工具用久了你就会发现几个问题免费版往往夹带弹窗广告装到工控电脑上非常碍眼万一误点还会装上一堆全家桶。有些工具功能做得很全但界面密密麻麻全是按钮和输入框找自己要用的那个功能还得上下摸索半天。更关键的是现成工具很难针对你自己的项目做定制。比如我经常测试带CRC16校验的Modbus协议每次都要先在外部计算器里算出校验码再拼到发送帧里步骤繁琐还容易出错。我就想把这个校验功能直接集成到串口助手的发送区里这在大多数现成工具里根本做不到。还有一点很现实做嵌入式或单片机开发时采样数据往往是高频、连续、带时间戳的现成工具要么没有时间戳要么导出格式乱七八糟。这些定制需求才是推动我自己动手的核心动力。1.2 自己写的好处代码可控、定制自由、顺便练技术自己开发串口助手最大的好处就是工具长在我手上。一开始我只需要一个能收发串口数据的小工具但随着项目深入我逐渐在它上面加了这些功能自定义快捷发送区把常用的AT指令、Modbus帧保存成预设按钮点击即发送。接收数据显示时间戳方便定位数据流中某条协议报文出现的时刻。自动保存完整日志崩溃或关掉之后也不怕丢失现场数据。集成CRC校验计算发Modbus帧时自动把校验字节追加到报文尾部。发送间隔可配置用来测试设备在大流量冲击下是否会丢帧。这些需求如果靠现成工具可能需要同时开好几个软件配合。但自己写一个就是一个窗体的事。而且用VB.NET做WinForms开发拖控件、写事件处理器说实话门槛真不高很适合作为.NET桌面开发入门到进阶的实战项目。2. 串口通信到底在做什么速率、数据位、校验位背后的原理好多新手在界面上把波特率、数据位、校验位、停止位一顿乱填然后发现设备不上数就开始怀疑代码有问题。其实串口参数的本质是收发双方的通信协议两边设置不一致物理层就直接对不上了。2.1 串口参数的逻辑9600、8、N、1 到底代表什么我们在配置串口时经常看到9600, 8, N, 1这串数字它分别代表波特率、数据位、校验位、停止位。波特率每秒传输的比特数。9600代表每秒钟传输9600个bit实际去掉起始位、停止位、校验位后数据吞吐量大约是960字节/秒。如果设备端是115200你用9600去打开收到的基本就是乱码。数据位一个字节数据用几位来表示常见的是8位也有一些老旧设备用7位。8位数据位时可以完整表示0~255的字节值。校验位用于检测传输过程中是否发生比特错误常见的包括None无校验、Odd奇校验、Even偶校验。现在大多数场景直接用None因为校验位只能查错不能纠错如果数据完整性要求高一般会在协议层用CRC校验而不是串口校验位。停止位每个字节传输结束后的停止信号长度常见的是1位或2位。我用一个生活化类比来理解串口通信就像两个人隔着一条马路用对讲机说话。波特率决定了两个人说话的语速必须一致数据位是每句话要说几个字校验位就像在每句话后面加一个重复一遍关键数字的机制停止位则是我说完了换你的提示音。任何一项设置不一致对话就进行不下去。在VB.NET中这些参数在SerialPort控件上都是直接暴露的With SerialPort1 .PortName COM3 .BaudRate 115200 .DataBits 8 .Parity Parity.None .StopBits StopBits.One .Handshake Handshake.None .ReadTimeout 500 .WriteTimeout 500 End With2.2 数据的两种形态ASCII字符与十六进制字节串口传输的数据本质上是一串字节Byte每个字节是0~255之间的数值。但在调试界面里这些字节有不同展现方式ASCII显示把每个字节当成一个字符显示。比如设备返回0x31、0x32、0x33ASCII模式下显示成123。十六进制显示把每个字节显示成两位十六进制数比如0x31 0x32 0x33显示成31 32 33。对于只想阅读设备状态文本的场景比如GPS模块输出的NMEA语句直接用ASCII模式就很直观。但对于Modbus、自定义二进制协议这种面向字节流的通信必须用十六进制模式才能看清每一帧的原始数据。我在界面里做了一个RadioButton切换接收和发送的显示格式。发送时以字节数组为基准如果是十六进制模式就把用户输入的AA BB CC字符串解析成对应字节如果是ASCII模式就直接把字符串按编码转成字节。接收时则反过来。理解了一切皆字节这条主线后面代码就不容易写乱。3. 界面设计一个顺手好用的串口助手布局才是灵魂先别急着写串口代码工具类软件最重要的是好用两个字。我第一版界面随手乱摆控件用起来总觉得别扭。后来重构了一版才明白界面布局直接影响调试效率。3.1 核心区域划分连接区、接收区、发送区我把窗体分成三个核心区域分界线很清楚顶部连接区串口号下拉框、波特率下拉框、打开/关闭串口按钮、刷新串口按钮。中间接收区带滚动条的TextBox或者RichTextBox显示所有接收到的数据旁边放清空按钮和显示格式选项。底部发送区发送内容输入框、发送按钮、定时发送勾选框和间隔设置以及快捷发送按钮区。顶部连接区要紧凑不要占用太多垂直空间因为接收区才是调试时主要盯着的区域。接收区我用TextBox设置了Multiline、ScrollBars为Vertical、ReadOnly为True同时把Font设置为等宽字体比如Consolas这样十六进制数据显示出来每一列都对齐肉眼对比数据时舒服很多。发送区和接收区之间用SplitContainer分隔方便用户根据当前数据量随时调整上下区域的高度。这个细节我强烈建议加上因为当协议数据帧长度差别很大时固定比例布局会很别扭。3.2 用ComboBox做动态串口枚举的几个细节串口列表不能写死在界面上否则插拔USB转串口设备后下拉框就是哑的。我用SerialPort.GetPortNames()获取当前系统里的串口列表在窗体加载时和点击刷新时调用。这里有个小坑GetPortNames()返回的是COM1、COM3这样的字符串格式但顺序是随机的。我按数字部分排了个序不然COM10会排在COM2前面看起来乱糟糟的。Private Sub RefreshPortList() Dim ports As String() SerialPort.GetPortNames() Array.Sort(ports, Function(a, b) Dim na As Integer Integer.Parse(a.Replace(COM, )) Dim nb As Integer Integer.Parse(b.Replace(COM, )) Return na.CompareTo(nb) End Function) cboPort.Items.Clear() cboPort.Items.AddRange(ports) If ports.Length 0 Then cboPort.SelectedIndex 0 End If lblComState.Text 串口未打开 End Sub设备插上但列表里没出现新串口绝大多数情况是USB转串口驱动没装好。在设备管理器里看不到COM口时优先处理驱动而不是改代码。4. 核心实现SerialPort控件的打开、接收与发送界面准备好了下面就是整个项目的核心串口通信逻辑。4.1 串口初始化与开关控制我直接在窗体上拖了一个SerialPort控件省得手动实例化传参数。打开串口时先检查是否已经打开避免重复Open抛异常Private Sub btnOpen_Click(sender As Object, e As EventArgs) Handles btnOpen.Click If SerialPort1.IsOpen Then SerialPort1.Close() btnOpen.Text 打开串口 btnSend.Enabled False lblComState.Text 串口已关闭 Return End If Try SerialPort1.PortName cboPort.Text SerialPort1.BaudRate CInt(cboBaud.Text) SerialPort1.Parity CType(cboParity.SelectedIndex, Parity) SerialPort1.StopBits CType(cboStopBits.SelectedIndex, StopBits) SerialPort1.DataBits CInt(cboDataBits.Text) SerialPort1.Open() SerialPort1.DiscardInBuffer() SerialPort1.DiscardOutBuffer() btnOpen.Text 关闭串口 btnSend.Enabled True lblComState.Text 串口已打开: cboPort.Text | cboBaud.Text Catch ex As Exception MessageBox.Show(打开串口失败 ex.Message, 错误, MessageBoxButtons.OK, MessageBoxIcon.Warning) End Try End Sub这里有个易忽略的小地方ComboBox校验位和停止位的下拉项要小心和枚举对应。我用SelectedIndex直接转换枚举所以下拉项的顺序必须与Parity枚举、StopBits枚举一致否则打开串口时参数会是错的。4.2 DataReceived事件与跨线程更新UI的Invoke问题这是串口开发里最核心、最容易翻车的部分。SerialPort的DataReceived事件是在后台辅助线程触发的不是UI线程。如果你在事件里直接写txtReceive.Text some data程序不会报错但UI可能闪烁、卡顿甚至崩溃因为TextBox并不允许后台线程直接操作。正确做法是用Invoke或BeginInvoke把UI更新操作封装到UI线程上执行。我习惯先判断IsHandleCreated防止窗体关闭时Invoke引发ObjectDisposedExceptionPrivate Sub SerialPort1_DataReceived(sender As Object, e As SerialDataReceivedEventArgs) Handles SerialPort1.DataReceived Dim bytesAvailable As Integer SerialPort1.BytesToRead If bytesAvailable 0 Then Return Dim buffer(serialPort1.BytesToRead - 1) As Byte SerialPort1.Read(buffer, 0, buffer.Length) If Me.IsHandleCreated Then Me.BeginInvoke(Sub() ProcessReceivedBytes(buffer) End Sub) End If End Sub读取串口数据用Read(byte[], offset, count)方法一次把当前缓冲区的所有字节读出来而不是用ReadLine或者ReadExisting。这样做的原因后面第五节会细说先把代码骨架搭对。4.3 发送数据的编码处理与十六进制发送发送逻辑相对简单但编码处理要想清楚。我的发送函数分两种情况如果用户选择ASCII发送直接把发送框里的字符串按Encoding.GetEncoding(GBK)转成字节数组。为什么用GBK而不用UTF-8因为很多设备尤其是国产工业设备和GPS模块默认用GBK或ASCIIUTF-8编码的中文字符发过去会变成乱码。如果是纯英文数字用ASCII就够了。如果用户选择Hex发送把输入框里的AA BB CC字符串解析成字节数组。注意要去掉空格并且每两个字符一组非法字符直接提示用户。Private Function ParseHexString(hexStr As String) As Byte() Dim clean hexStr.Replace( , ).Replace(-, ) If clean.Length Mod 2 0 Then Throw New Exception(十六进制字符串长度必须为偶数) End If Dim result(CInt(clean.Length / 2) - 1) As Byte For i As Integer 0 To result.Length - 1 result(i) Convert.ToByte(clean.Substring(i * 2, 2), 16) Next Return result End Function Private Sub SendBytes(data As Byte()) If Not SerialPort1.IsOpen Then MessageBox.Show(串口未打开) Return End If SerialPort1.Write(data, 0, data.Length) End Sub发Modbus帧时我习惯在快捷发送按钮里直接写死整帧数据或者把CRC计算也做成一个公共函数点发送前自动计算追加调试效率高很多。5. 串口调试最大的坑丢数据、粘包与乱码开发过程中最让我头疼的不是串口打不开而是数据接收不稳定。在这里把排查思路完整记录下来。5.1 为什么DataReceived里不能直接做ReadLine我第一次写接收代码时图省事直接在DataReceived里调用ReadLine()结果发现数据时有时无而且经常报操作超时异常。原因是ReadLine()会一直读到换行符为止。如果设备发送的数据不是以换行符结尾或者一帧数据还没发完就触发了DataReceivedReadLine就会占用线程等待超时期间新到的数据又会触发新的DataReceived最终线程越积越多接收就乱了。此外ReadLine()默认认为换行符是LF\n遇到CRLF结尾的帧还能读出来但如果设备只用CR那就是一场灾难。我把DataReceived里的逻辑改成先读进字节数组再自己处理帧边界问题就解决了。SerialPort.ReadLine和ReadExisting只适合数据量小、格式固定的简单文本协议规规矩矩按字节流处理才是通用方案。5.2 缓冲区读取与字节缓存从源头解决粘包串口数据是流式的没有天然的消息边界。设备可能一次发来3个字节也可能一次发来300个字节通信驱动会在合适的时间触发DataReceived事件但这和协议层的一帧没有必然联系。如果设备一帧是AAB1C2F4但事件触发时你只读到了AA B1 C2下一轮才读到F4直接显示就会把一帧拆成两半这就是最常见的粘包/半包问题。我的处理方案是引入一个全局的接收缓存集合把每次从串口读到的字节先追加到缓存里然后尝试从缓存中解析出完整的帧并处理处理完的字节就从缓存中移除。针对自己定义的协议比如帧头长度数据校验可以写一个专门的FindFrame函数如果只是通用调试工具就把缓存里的字节全部追加到接收显示区让用户自己判断帧边界。在通用工具里我主要保证一点接收区显示的内容和实际收到的字节完全一致不丢数据。具体做法就是把DataReceived里读到的buffer原封不动追加到TextBox中Dim text As String Encoding.ASCII.GetString(buffer) AppendReceiveText(text)AppendReceiveText里用TextBox.AppendText而不是直接textBox.Text text因为AppendText会自动滚动到底部频繁插入时效率也高得多。5.3 乱码问题编码不匹配的排查思路乱码几乎是每个做串口调试的人都会碰到的问题。乱码的原因不外乎这两个发送端的字符编码和接收端显示的编码不一致。设备输出UTF-8字节流但你的助手按ASCII解析那中文当然就是一堆锟斤拷。波特率错误导致收到大量0x7F或0x00之类的异常字节。排查思路我按这个顺序走先用示波器或者逻辑分析仪确认波特率是否匹配。没有示波器的话可以用串口助手收发纯数字或英文测速如果能正确显示12345说明波特率大概率没问题。确认设备文档里指定的编码格式。常见的有ASCII、GB2312/GBK、UTF-8。VB.NET里编码获取方式Encoding.GetEncoding(GBK)、Encoding.UTF8。如果设备手册说支持UTF-8但显示还是乱检查是不是发送时VB.NET把字符串转成了UTF-16你得显式按字节数组发送不能直接把String塞给Write方法。这里再补充一个工控场景常见问题USB转串口芯片兼容性。同一块设备用FT232芯片的线没问题换成CH340就可能出现丢字节或乱码。这不是代码问题换一条线或者重装驱动就能解决。我被这个坑害过整整一个下午排查了代码、协议、编码之后才发现是转接线的问题。6. 从能用到好用自动发送、日志记录与其他扩展基础版串口助手能用了之后我开始往里加真正提升效率的功能。6.1 定时自动发送的三种实现方式定时发送就是每隔固定时间自动发送一次数据用来测试设备在长时间压力下的稳定性。实现思路有三种Timer控件最简单在窗体上拖一个Timer设置IntervalTick事件里调用发送函数。适合发送间隔在几十毫秒以上的场景精度足够。线程循环Slepp在后台线程里做循环间隔时间用Thread.Sleep控制。发送间隔可以做到几毫秒但注意帧间隔不准且UI无法直接通知线程停止要用标志位控制。Stopwatch忙等最高精度但不推荐在WinForms里用CPU占用高而且和定时器线程抢占调度会引入不确定性。我用的方案是第一和第二种结合。普通场景用Timer.SetInterval实现做压力测试时切到后台线程模式用ManualResetEvent来通知开始和停止Private _autoSendThread As Thread Private _autoSendCancel As New ManualResetEvent(False) Private Sub StartAutoSend() _autoSendCancel.Reset() _autoSendThread New Thread(AddressOf AutoSendLoop) _autoSendThread.IsBackground True _autoSendThread.Start() End Sub Private Sub AutoSendLoop() Dim interval CInt(txtAutoInterval.Text) Dim frame Encoding.ASCII.GetBytes(txtSend.Text vbCrLf) While Not _autoSendCancel.WaitOne(interval) If SerialPort1.IsOpen Then SerialPort1.Write(frame, 0, frame.Length) End If End While End Sub Private Sub StopAutoSend() _autoSendCancel.Set() End Sub用ManualResetEvent.WaitOne(interval)代替Thread.Sleep的好处是停止线程时不用打断睡眠响应非常快。6.2 日志保存与时间戳的经验日志保存是个加了就回不去的功能。我用一个独立函数处理接收数据每次追加到界面显示的同时写入一个StringBuilder当接收数据量达到一定阈值比如每500行就刷进日志文件。时间戳设计要注意一点不要每收到一个字节就打一次时间戳而应该以行的维度打时间戳。比如收到一帧完整数据后在该帧开头加一行[2025-01-15 14:22:03.125] AA BB CC DD EE FF 12 34如果一帧数据非常长可以在数据里插入换行但不加时间戳。日志文件用UTF-8带BOM保存这样用记事本打开中文不会乱。清理日志文件也要考虑我设置日志文件超过10MB就自动切换新文件旧文件按时间命名保留避免一个文件无限膨胀到几百MB导致打开卡死。6.3 一个更有野心的方向串口波形显示这个功能我放在最后分享是因为它最能体现自己写工具的乐趣。很多现成串口助手没有示波器功能但如果你调试的是传感器数据比如温度、电压、加速度一条条看十六进制数据效率太低。此时如果能在界面上直接画出曲线设备状态一目了然。实现思路不复杂把接收到的数据帧解析成数值塞进一个List(Of Double)或环形缓冲区用GDI或者简单的绘图控件比如Chart控件把曲线画出来。我用的方案是.NET自带的Chart控件把X轴设置成时间或采样序号Y轴按传感器量程设置。数据解析这一步是自定义的因为不同设备协议不同 假设设备一帧是 T:25.6;C:30.1解析温度和电流并追加到Chart Dim parts line.Split(;c) For Each part In parts If part.StartsWith(T:) Then Dim temp As Double Double.Parse(part.Substring(2)) ChartTemp.Series(温度).Points.AddY(temp) ElseIf part.StartsWith(C:) Then Dim cur As Double Double.Parse(part.Substring(2)) ChartCur.Series(电流).Points.AddY(cur) End If Next当然这个功能要和具体的设备协议绑定不像通用收发那么通用。但正因为是自研工具我才可以随时针对当前测试项目改解析逻辑。这也正是我一开始选择自己写串口助手的原因。最后再分享一个个人经验VB.NET做串口工具项目不需要追求一步到位。先把收发功能做稳再逐步加入自己真正需要的定制功能整个过程远比下载一个现成工具更有价值。工具越好用开发越顺手这个正循环只有自己写过的人才能体会到。本文还有配套的精品资源点击获取
返回列表