简介:这是一份基于VB.NET原生SerialPort组件实现串口助手的完整源码示例,面向需要绕开AXCom控件、亲自掌握串口通信与委托线程机制的VB.NET开发者。资源共37个文件,压缩包仅181KB,包含6个vb源码、已编译exe、调试pdb,以及config、xml、resx等配置与界面资源文件,覆盖窗体设计、串口参数配置、数据收发和线程安全更新UI的完整流程。目前已有519人参与学习。作者放弃网上常见的AXCom控件,改用最新SerialPort类,重点展示波特率、数据位、停止位、奇偶校验等核心参数的设置方式,并理清异步收发时委托与线程的关系,亲测可运行。整个工程结构清晰,压缩包轻量,既可当作串口调试工具的开发模板,也能为VB.NET通信编程提供参考,直接编译即可体验。 搞嵌入式这些年,谁电脑里没装过两三个串口调试工具。从最早的串口调试助手、SSCOM,到后来的XCOM、友善串口助手,装了删、删了装。说实话,这些工具都能用,但用久了总觉得差点意思——界面布局不合自己习惯,发送区不支持我想要的快捷方式,想按项目定制个协议格式还得去求人改软件。后来我干脆花了一个周末,用VB.NET给自己写了个串口助手,这一用就是好几年。
这篇文章我就把这个过程完整摊开讲。从串口通信那几个必须搞明白的参数,到VB.NET里SerialPort组件的实际用法,再到接收显示、发送控制、波形绘制这些核心功能怎么一步步写出来,最后是实战中一定会踩的坑。不管你是刚接触上位机的嵌入式新人,还是想把手头工具换得更顺手的工程师,这篇文章应该都能给你点实在的参考。
1. 为什么自己动手写串口助手
1.1 现成工具能用,但总有让你不舒服的地方
市面上的串口调试助手确实不少,功能也大同小异。但实际用起来,每个人遇到的需求都不一样。比如我经常要调试Modbus RTU协议,一条报文七八个字节,每次都要手打那段十六进制字符串,或者从文本文件里复制过来,非常累。还有些工具不支持定时发送,我调试传感器轮询的时候就得手动一条条发。
更麻烦的是,有些项目需要把串口收到的数据按特定格式解析出来,甚至直接绘图显示。通用工具大部分要么不做这功能,要么做得很简陋。毕竟人家做的是"通用"产品,不可能为了你一个项目去定制功能。
这其实就是自己写工具最大的价值——你可以完全按自己的工作方式来设计它。
1.2 技术选型:为什么是VB.NET
可能有人会说,写串口助手用C#不行吗?用Python不也行吗?当然都可以。我选VB.NET不是因为它比别的语言强,而是因为它在Windows平台上做这种小工具确实方便。
.NET框架里自带了System.IO.Ports命名空间,里面封装好了SerialPort类,打开串口、读写数据都是现成的方法,不需要你去调底层API。Visual Studio社区版免费,拖拽控件做界面,半小时就能把界面骨架搭起来。这对于工控、测试领域的工程师来说,学习成本很低,很多老工程师多多少少都会点VB的语法,迁移到VB.NET很自然。
你要问C#和VB.NET哪个好,我个人觉得在串口助手这种场景下没本质区别,因为核心调用的都是同一个SerialPort类。选VB.NET纯粹是熟悉和顺手的问题。
2. 串口通信的那些参数,你得真搞懂
2.1 波特率、数据位、停止位、校验位到底在说什么
串口通信的原理其实很朴素:一根发送线TX,一根接收线RX,发送方按照约定的节奏一位一位地把电平信号送出去,接收方用同样的节奏去采样。关键在于"约定的节奏"和"约定的格式",这就是我们常说的波特率、数据位、停止位、校验位。
波特率是每秒传输多少个bit,单位bps。常见的有9600、115200,工业设备里4800、19200也有不少。波特率不是越高越好,线缆长了、环境干扰大了,高速率下就容易出错。而且通信双方必须严格一致,差一点都不行。
数据位通常是8位,老一点设备有7位的情况。停止位有1位、1.5位、2位三种,正常情况下选1位。校验位有五种:None无校验、Odd奇校验、Even偶校验、Mark恒为1、Space恒为0,绝大多数场景用None或Even。
这几个参数是串联成一帧的完整格式,实际调试时两边设不一致,表现就是要么收一堆乱码,要么干脆没数据。比如你设了偶校验,对方没校验,那接收端就会不停地报错,数据根本进不到你的接收缓冲区。
2.2 硬件层面:TTL、RS232、RS485的区别
做上位机经常要跟硬件连,绕不开这三种电平标准。
TTL电平是单片机直接输出的,0V表示低电平,3.3V或5V表示高电平。这种电平只能板内短距离通信,最多几十厘米。
RS232是负逻辑,-3V到-15V表示1,+3V到+15V表示0,传输距离能到15米左右。电脑老的DB9串口就是RS232电平。现在笔记本没有串口了,需要用USB转串口线转出来。
RS485是差分信号,用A、B两根线之间的电压差来传数据,抗干扰能力强,最远能到1200米,工业现场用得最多。但它有个特点——半双工,同一时刻只能发或者只能收。
对你写的串口助手来说,这些电平标准其实不影响代码——不管外面是TTL还是RS232还是RS485,转换完之后到USB串口芯片那里,你接收到的都是一样的字节流。但如果你在调试时发现收不到数据,得能想到是不是电平不匹配、接线反了的问题。
2.3 USB转串口芯片那点事
现在调试都是USB转串口,常见芯片就那几款:CH340、FTDI FT232、CP2102、PL2303。CH340是国产的,性价比高,模块几块钱一个,缺点是驱动偶尔有兼容性问题。FTDI稳定性好但价格贵,市面上假芯片多。CP2102中规中矩。
Win10以上系统一般能自动装驱动,但CH340偶尔会掉链子,识别成了未知设备。我的经验是直接去芯片官网下驱动手动装,稳定得多。那些驱动精灵、驱动大师什么的,装完经常给你来个全家桶,没必要。
在写串口助手的时候,建议加一个图标或提示,显示当前用的什么芯片,方便排查问题。
3. 核心功能逐行实现
3.1 创建项目与界面布局设计
用Visual Studio新建一个Windows Forms应用(.NET Framework),这里要注意,如果你装的是新版VS,创建项目时选"Windows窗体应用(.NET Framework)",跑起来最省心。项目名我就叫它MySerialTool。
界面布局按自己习惯来,参考我自己的工具,大概是下面这个结构:
- 顶部区域:串口选择、波特率选择、数据位、校验位、停止位下拉框,以及打开串口按钮
- 接收区:一个多行文本框,勾选"HEX显示"可以切换十六进制,勾选"时间戳"可以给每条数据加时间
- 发送区:一个多行文本框,支持按HEX或ASCII发送,旁边是发送按钮
- 附加功能:定时发送勾选框加间隔输入框,清空接收区按钮、保存日志按钮
界面代码我就不贴了,拖控件谁都会。重点说逻辑代码。
3.2 枚举串口与打开串口
打开界面时,需要先把系统里现存的串口号枚举出来填到下拉框里。SerialPort类提供了现成的方法:
Imports System.IO.Ports Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load GetPorts() End Sub Private Sub GetPorts() Dim ports As String() = SerialPort.GetPortNames() cmbPort.Items.Clear() For Each p As String In ports cmbPort.Items.Add(p) Next If cmbPort.Items.Count > 0 Then cmbPort.SelectedIndex = 0 End If End Sub打开串口的代码是这样的:
Private sp As New SerialPort() Private Sub btnOpen_Click(sender As Object, e As EventArgs) Handles btnOpen.Click If Not sp.IsOpen Then Try sp.PortName = cmbPort.SelectedItem.ToString() sp.BaudRate = Convert.ToInt32(cmbBaud.SelectedItem) sp.DataBits = 8 sp.Parity = Parity.None sp.StopBits = StopBits.One sp.Handshake = Handshake.None sp.DtrEnable = False sp.RtsEnable = False sp.Open() btnOpen.Text = "关闭串口" Catch ex As Exception MessageBox.Show("打开串口失败:" & ex.Message) End Try Else sp.Close() btnOpen.Text = "打开串口" End If End Sub这里有两个容易踩的坑。第一个:串口在打开状态下,去修改PortName、BaudRate这些属性会直接抛异常。所以如果你要做参数实时修改,得先关串口再改参数再重新打开。第二个:很多USB转串口板子在打开串口的瞬间会把DTR或RTS引脚拉低,如果你的单片机电路正好用这两个信号控制复位和BOOT,会导致设备一打开串口就重启。所以我这里直接设成了False,后面的排查部分还会讲。
3.3 数据接收与跨线程更新UI
接收数据有两种常见方式。一是开个线程循环读,二是用SerialPort自带的DataReceived事件。后者更省事。事件触发时操作系统在后台线程调用,所以直接在事件里操作界面控件会抛跨线程异常。
我的写法是事件里面把数据读进缓冲区,再用Me.Invoke回到UI线程更新界面:
Private Sub sp_DataReceived(sender As Object, e As SerialDataReceivedEventArgs) Handles sp.DataReceived Dim bytesToRead As Integer = sp.BytesToRead Dim buffer(bytesToRead - 1) As Byte sp.Read(buffer, 0, bytesToRead) Me.Invoke(Sub() AppendReceiveData(buffer) End Sub) End Sub Private Sub AppendReceiveData(data() As Byte) Dim text As String = "" If chkHexDisplay.Checked Then text = BytesToHex(data) Else text = Encoding.Default.GetString(data) End If If chkTimestamp.Checked Then txtReceive.AppendText("[" & DateTime.Now.ToString("HH:mm:ss.fff") & "] " & text & vbCrLf) Else txtReceive.AppendText(text) End If End Sub这里有几个细节值得说。ReceivedBytesThreshold属性默认是1,也就是说缓冲区每进来1个字节就会触发一次DataReceived。如果数据量很大,这个事件会被频繁触发,性能损失明显。所以我一般把它改成大一点的数,比如1024,攒够一批再通知。
还有一个就是要一次把缓冲区的数据全部读走,不要只读一部分。用BytesToRead拿到当前有多少字节,然后一次性Read出来。要是每次只读固定长度,数据稍多就会留在缓冲区,造成延迟甚至丢失。
3.4 数据发送与HEX处理
发送数据就简单很多:
Private Sub btnSend_Click(sender As Object, e As EventArgs) Handles btnSend.Click If Not sp.IsOpen Then MessageBox.Show("串口未打开") Return End If Try If chkHexSend.Checked Then Dim data() As Byte = HexToBytes(txtSend.Text) sp.Write(data, 0, data.Length) Else Dim data() As Byte = Encoding.Default.GetBytes(txtSend.Text) sp.Write(data, 0, data.Length) End If Catch ex As Exception MessageBox.Show("发送失败:" & ex.Message) End Try End SubHEX字符串转字节数组的函数,注意考虑用户可能输入带空格或者不带空格的情况:
Private Function HexToBytes(hexStr As String) As Byte() hexStr = hexStr.Replace(" ", "").Replace("-", "").Trim() If hexStr.Length Mod 2 <> 0 Then Throw New Exception("HEX字符串长度不是偶数") End If Dim bytes((hexStr.Length \ 2) - 1) As Byte For i As Integer = 0 To hexStr.Length - 1 Step 2 bytes(i \ 2) = Convert.ToByte(hexStr.Substring(i, 2), 16) Next Return bytes End Function发送时有一个很隐蔽的坑:编码问题。如果你用ASCII方式发送中文字符串,有些设备用的是GB2312或者GBK编码,而你的程序在中文Windows系统上用Encoding.Default通常能对上。但换了一台英文系统的电脑,Encoding.Default就变成ASCII了,发出去的中文就乱了。最严谨的做法是显式指定编码:
Dim data() As Byte = System.Text.Encoding.GetEncoding("GB2312").GetBytes(txtSend.Text)同理,接收的时候如果知道对方的中文编码是GB2312,也用同样的方式解码。
3.5 定时发送实现
定时发送用WinForms的Timer控件最方便。界面放一个Timer,Tick事件里调用发送逻辑,间隔通过Interval属性设置,单位是毫秒。
Private Sub chkAutoSend_CheckedChanged(sender As Object, e As EventArgs) Handles chkAutoSend.CheckedChanged If chkAutoSend.Checked Then TimerSend.Interval = Convert.ToInt32(txtInterval.Text) TimerSend.Start() Else TimerSend.Stop() End If End Sub Private Sub TimerSend_Tick(sender As Object, e As EventArgs) Handles TimerSend.Tick btnSend.PerformClick() End Sub定时发送调试Modbus轮询特别有用。我之前调试一个温湿度传感器,上位机要每隔100ms读一次寄存器,手动发送根本来不及,用定时发送就完全自动化了。
4. 进阶功能:让工具更顺手
4.1 接收区波形绘制
做PID调参的时候,光看ASCII数字还是不够直观,把收到的数据画成波形图就方便多了。.NET自带Chart控件,不用装第三方库。
简单做法是设定一个协议规则,比如设备每帧发两个字节代表一个数值,或者发一行"val=123"这样的文本。上位机收到后解析出数值,加到Chart里:
Imports System.Windows.Forms.DataVisualization.Charting Private Sub AddWavePoint(value As Double) If Chart1.Series("Wave").Points.Count > 500 Then Chart1.Series("Wave").Points.RemoveAt(0) End If Chart1.Series("Wave").Points.AddY(value) End Sub只保留最近500个点,防止数据越来越多导致绘图变卡。这个功能虽然代码量不大,但在调PID的时候简直就是神器,关键参数的变化趋势一眼就能看出来。
4.2 日志保存与加载
调试中经常要把串口数据存下来,方便事后分析。在接收数据的时候,顺手把原始数据写到日志文件里就行。文件名可以带上日期时间:
Private Sub AppendReceiveData(data() As Byte) ' ...显示逻辑省略... If chkLog.Checked Then Dim line As String = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff") & " " & text File.AppendAllText("log_" & DateTime.Now.ToString("yyyyMMdd") & ".txt", line & vbCrLf) End If End Sub这里要注意日志文件的打开顺序和异常处理,最好在程序启动时就检查目录可写。另外如果是长时间连续接收,建议每次写入时先打个日志大小,避免单个文件无限膨胀。
4.3 常用命令快捷面板
把调试时反复发送的固定报文做成按钮,点击就发送。这个是纯体力活,但确实能大幅提升效率。比如Modbus报文、查询命令、设置命令这些,放一行按钮,有时候比敲键盘快得多。
5. 实战问题与排查速查
5.1 乱码问题排查
乱码在实践中遇到最多,原因也最多。最常见的是波特率不一致,两边差一位小数都不行。其次是校验位和数据位设置不一致。再就是编码问题,设备按UTF-8发字符串,你这边按GB2312解码,中文就会变"锟斤拷"。
排查方法是先用最简单的模式验证:9600波特率、8数据位、无校验、1停止位,发一串英文和数字看看是否正常。如果正常,再逐步调整参数对比。
5.2 数据丢失
数据丢失有两个常见原因。第一,接收事件触发了但你没把缓冲区数据全部读完,新的数据把旧数据覆盖了。第二,在高波特率下,比如921600,接收线程处理太慢,数据积压导致缓冲区溢出。
解决办法是:
- 把SerialPort.BufferSize调大,比如设置成8192或者更大
- 在DataReceived里一次性把BytesToRead读干净
- 不要在该事件里做耗时的UI操作,分析、存文件放到工作线程去做
5.3 串口被占用导致打不开
USB转串口如果拔插频繁,经常会出现"COM口被占用"的情况。一种可能是其他调试工具还开着占用了串口,另一种可能是拔插太快,系统没来得及释放端口。
遇到打不开的情况,去设备管理器看看端口是哪个COM号,看看是不是有黄色感叹号。如果是,卸载设备重新扫描,或者换个USB口插。代码层面,打开串口一定要try catch,给出明确的错误提示,不然用户根本不知道出了什么事。
5.4 单片机一打开串口就复位
这个问题我做STM32调试时遇到过,当时差点以为是硬件问题。后来发现是CH340模块的DTR和RTS信号接到了MCU的复位电路上。很多开发板的自动下载电路就是靠这两个信号来切换BOOT状态和复位,但平时调试时如果上位机打开串口时把DTR/RTS拉低了,就会导致开发板复位。
解决办法很简单,打开串口时把DtrEnable和RtsEnable都设为False。如果你需要控制这两个信号,也可以在界面上做成复选框,手动控制电平状态。
5.5 接收区UI卡顿
数据量大的时候,TextBox内容越来越多,UI就会越来越卡。这跟WinForms控件本身的性能有关。我的做法是限制接收区最大长度,超过一定数量就截断前面的内容:
If txtReceive.TextLength > 100000 Then txtReceive.Text = txtReceive.Text.Substring(txtReceive.TextLength - 50000) End If另外接收数据时尽量用AppendText而不是直接对Text做拼接。AppendText内部做了优化,效率比txtReceive.Text &= "xxx"高不少,同时光标还能自动滚动到末尾。
写到这里,这套串口助手的基本功能就成型了。从最基础的串口打开关闭,到接收区显示、HEX转换、定时发送,再到波形绘制、日志保存,每一步都是我在实际项目中验证过、踩过坑才完善的。说实话,自己写串口助手这个过程,比单纯下载一个工具来用有价值得多。它不仅让我对串口通信协议有了更深入的理解,也让我在遇到各种通信问题时能更快定位到是硬件问题、协议问题还是上位机代码问题。后面如果你也想动手写一个,建议从最简版本开始,先把打开串口和数据收发跑通,再按自己的实际需求一步一步加功能,用着不顺手的就改,最后你会收获一个完全符合自己习惯的工具。
本文还有配套的精品资源,点击获取