基于winform的串口调试助手
目录
一、串口助手界面设计
1.1 串口配置
1.2 接收配置
1.3 发送配置
1.4 接收窗口和发送窗口
1.5 状态显示窗口
1.6 串口通讯控件
二、程序编写
2.1 端口号自动识别并显示在端口号下拉框
功能说明:
2.2 波特率下拉框显示
2.3 数据位下拉框显示
2.4 校验位下拉框显示
2.5 停止位下拉框显示
2.6 停止位列表数字与serialport1使用的枚举类型对应
2.7 选择的串口名称、波特率、数据位、校验位、停止位参数赋值给serialport1对应的变量
2.8 编写串口打开按钮的点击事件
2.9 串口数据发送程序编写
2.10 串口数据接收程序编写
一、串口助手界面设计
1.1 串口配置
采用groupbox控件,其中包括了串口配置、波特率、数据位、校验位、停止位、RTS、DTR和打开串口控件。
① 串口配置、波特率、数据位、校验位、停止位:combobox控件和对应文字描述的label控件
② RTS、DTR:checkbox控件
③ 打开串口:最熟悉的button控件 哈哈哈
1.2 接收配置
同样采用groupbox控件,支持清空数据、暂停接收、保存数据等功能;可选择十六进制显示,便于查看原始数据格式;可指定保存路径,便于数据。
① 自动清空、16进制:checkbox控件
② 手动清空、暂停、保存数据和选择路径:button控件
③ 文件保存路径:textbox控件
1.3 发送配置
同样采用 GroupBox 控件,支持手动输入数据发送、文件发送以及自动循环发送功能;可选择十六进制格式发送,适用于不同通信协议的调试需求;可设置发送时间间隔,满足周期性发送场景。
① 自动发送、16进制:CheckBox
控件
② 手动发送、清除数据、发送文件和选择路径:Button
控件
③ 文件路径选择及发送周期设定:TextBox
控件
1.4 接收窗口和发送窗口
接收窗口和发送窗口采用的是richtextbox控件
1.5 状态显示窗口
状态接收采用的是statustrip控件,主要包括了状态、接收数量、发送数量和清空计数功能。
1.6 串口通讯控件
使用serialport控件进行串口通讯,本次编写的串口软件主要是依托于serialport控件及其对应的类,本次串口软件中serialport空间名字名为serialport1,后续程序中都是使用该名称进行编写程序。
二、程序编写
2.1 端口号自动识别并显示在端口号下拉框
private void save_btn_Load(object sender, EventArgs e)
{
// 将电脑上的串口资源加载到 port_cbb(端口选择下拉框)中
this.port_cbb.Items.AddRange(SerialPort.GetPortNames());
// 初始化发送和接收区域的提示文字
this.sent_rtb.Text = "我是发送区域";
this.receive_rtb.Text = "我是接收区域";
}
其中的serialport是system.IO.ports命名空间下的serialport类的一个实例对象,在串口软件页面加载事件中进行创建的实例对象。
功能说明:
-
🔍 自动识别串口:通过
SerialPort.GetPortNames()
获取当前计算机所有可用串口号,并添加到ComboBox
控件中,供用户选择,提升了使用的便捷性。 -
✍️ 区域提示信息:在发送区(
sent_rtb
)和接收区(receive_rtb
)初始化时分别添加默认提示文字,让用户一目了然地了解每个区域的功能。
2.2 波特率下拉框显示
直接在波特率下拉框控件中的item属性中设置常用的波特率数值:19200、9600、4800、2400,程序运行以后直接可以根据通讯要求进行选择对应波特率。
2.3 数据位下拉框显示
同上述②,直接在控件的item属性中设置常用的数据长度:5、6、7、8
2.4 校验位下拉框显示
同上述③,直接控件的item属性设置常用的校验位类型:Odd、Even、None、Mark、Space
2.5 停止位下拉框显示
同上述④,直接在控件的item属性中设置常用的停止位类型:1、2、1.5
注意事项:当数据为6、7、8时,停止位只能配置成1或者2;当数据位为5时,停止位只能设置成1或者1.5位。另外,serialport1中使用的停止位是枚举类型,需要将停止位列表中的数字于serialport1中使用的停止位枚举类型进行一一对应。
2.6 停止位列表数字与serialport1使用的枚举类型对应
停止位下拉列表中的数字实际是字符串,serialport1的停止位设置使用“serialport.stopbits”枚举类型进行设置,将字符串和stopbits枚举中的选项进行对应,具体如下所示。
switch (stopbit_cbb.SelectedItem.ToString())
{
case "1.5":
this.serialPort1.StopBits = StopBits.OnePointFive;
break;
case "1":
this.serialPort1.StopBits = StopBits.One;
break;
case "2":
this.serialPort1.StopBits = StopBits.Two;
break;
}
2.7 选择的串口名称、波特率、数据位、校验位、停止位参数赋值给serialport1对应的变量
this.serialPort1.PortName = this.port_cbb.SelectedItem.ToString();
this.serialPort1.BaudRate = Convert.ToInt32(baud_cbb.SelectedItem);
this.serialPort1.DataBits = Convert.ToInt32(databit_cbb.SelectedItem);
this.serialPort1.Parity = (Parity)Enum.Parse(typeof(Parity), checkbit_cbb.Text.ToString());
serialPort1.Encoding = Encoding.UTF8;
其中,serialport.encoding是编码格式,本次的串口助手中使用的utf-8,用于后续串口数据的发送和接收;由于控件中item参数默认的数据类型是字符串,需要将字符串转换成serialport需要的数据,涉及字符串转数字,字符串转枚举。校验位参数是枚举类型,其在serialport1中可设置的值如下所示:
2.8 编写串口打开按钮的点击事件
基本流程如下所示
开始
|
v
[点击“打开串口”按钮]
|
v
{openport_btnisopen == false?}
| 是
v
[配置串口参数]
- 串口号(PortName)
- 波特率(BaudRate)
- 数据位(DataBits)
- 校验位(Parity)
- 停止位(StopBits,根据选择设置)
- 编码格式(UTF-8)
|
v
[打开串口 serialPort1.Open()]
|
v
{串口是否成功打开? serialPort1.IsOpen == true}
| 是
v
[按钮背景设为绿色]
[按钮文字设为“关闭串口”]
[设置 openport_btnisopen = true]
|
v
结束
{openport_btnisopen == false?}
| 否
v
[关闭串口 serialPort1.Close()]
[按钮背景设为灰色]
[按钮文字设为“打开串口”]
[设置 openport_btnisopen = false]
|
v
结束
注意:其中的openport_btnisopen时一个全局变量,在串口软件界面初次加载的时候赋值为false。
打开串口按钮事件具体代码如下所示:
private void openport_btn_Click(object sender, EventArgs e)
{
try
{
if (openport_btnisopen==false)
{
//串口名称、波特率、数据位、校验位、停止位参数设置
this.serialPort1.PortName = this.port_cbb.SelectedItem.ToString();
this.serialPort1.BaudRate = Convert.ToInt32(baud_cbb.SelectedItem);
this.serialPort1.DataBits = Convert.ToInt32(databit_cbb.SelectedItem);
this.serialPort1.Parity = (Parity)Enum.Parse(typeof(Parity), checkbit_cbb.Text.ToString());
serialPort1.Encoding = Encoding.UTF8;
//当数据位为6、7、8位时,停止位只能配置成1或2位;同样当数据位为5位时,停止位只能为1或1.5位
switch (stopbit_cbb.SelectedItem.ToString())
{
case "1.5":
this.serialPort1.StopBits = StopBits.OnePointFive;
break;
case "1":
this.serialPort1.StopBits = StopBits.One;
break;
case "2":
this.serialPort1.StopBits = StopBits.Two;
break;
}
//打开端口
this.serialPort1.Open();
if (this.serialPort1.IsOpen ==true)
{
this.openport_btn.BackColor = Color.Green;
this.openport_btn.Text = "关闭串口";
}
openport_btnisopen = true;
}
else
{
this.serialPort1.Close();
this.openport_btn.BackColor = Color.Gainsboro;
this.openport_btn.Text = "打开串口";
openport_btnisopen = false;
}
//关闭端口
}
catch (Exception xc)
{
}
}
2.9 串口数据发送程序编写
在发送数据之前,需要判断发送框内的数据是否为空和串口通讯是否打开这两个条件。如果条件满足,使用“encoding.Getbytes”函数将发送框内的数据换成字节数据,使用serialport.write函数将转换得到的字节数据发送给对方。
注意:本次串口助手软件中没有补充不满足条件的执行代码,小伙伴可以根据自己的要求进行补充完善。
private void hand_send_btn_Click(object sender, EventArgs e)
{
if (sent_rtb.Text !=null && this.serialPort1.IsOpen)
{
Encoding Chinese = System.Text.Encoding.UTF8;//定义一个可以进行中文编码的变量
byte[] writeBytes = Chinese.GetBytes(sent_rtb.Text);
this.serialPort1.Write(writeBytes,0, writeBytes.Length);
//this.serialPort1.Write(writeBytes,0, writeBytes.Length);
}
}
2.10 串口数据接收程序编写
这个是B站上up主编写的数据接收程序,使用的编码格式是GB2312,首先在串口数据接收事件中声明了个字节数组,字节数组长度是串口缓存区中可以读取字节的数目,使用串口的read函数,将串口缓存区中的可读字节数据读取并存到字节数组中。同时将字节数组数据存储到全局的列表recivebuffer中,使用invoke将读取的数据在UI界面里的窗口显示区域进行显示(涉及到UI线程和非UI线程的问题,有兴趣的小伙伴可以查点资料看看)。
2.10.1 VSPD软件
这里介绍一个虚拟串口软件Virtual Serial Port Driver,它是 Eltima Software 出品的一款 虚拟串口工具,可以在系统中创建成对的虚拟串口(如 COM3 <-> COM4),广泛用于串口通信调试、设备模拟和串口软件开发中。
具体原理如下:
1.串口通讯调试
✅ 开发者可用它来模拟串口设备之间的通信,实现无需真实硬件的调试环境。
-
例如:开发一款串口通信的软件,但没有真实的串口设备,就可以用 COM3 <-> COM4 虚拟一对串口,一端运行你的软件,另一端用串口助手发送测试数据。
2.串口数据转发、转接协议调试
✅ 可用于协议桥接、数据透传、协议中间件调试。
-
比如:
-
A 程序向 COM5 发数据;
-
B 程序从 COM6 接收;
-
COM5 和 COM6 通过 VSPD 连接;
-
就可以验证 A 和 B 之间串口协议是否兼容。
-
具体使用教程可以参考博客:
VSPD工具 - Virtual Serial Port Driver v6.9 激活教程&使用教程 - Citrusliu - 博客园
具体下载链接(网上很多资源):
https://gitcode.com/open-source-toolkit/29e06/?utm_source=tools_gitcode&index=top&type=cardhttps://gitcode.com/open-source-toolkit/29e06/?utm_source=tools_gitcode&index=top&type=card
2.10.2 串口通讯实验
根据上述截图中的串口数据接收代码,自己进行了一定的改写,使得程序能够满足自己串口软件的使用。具体程序如下所示:
private void serialPort1_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
//如果暂停按钮被按下则停止接收数据
if (receiveisopen == false)
{
return;
}
if (serialPort1.BytesToRead==0)
{
return;
}
byte[] z = new byte[this.serialPort1.BytesToRead];
this.serialPort1.Read(z,0,z.Length);
datahuanchun.AddRange(z);
this.Invoke(new EventHandler(delegate
{
if (hex_receive_ckb.Checked == true)
{
//编写数据采用16进制进行显示
}
else
{
//编写数据采用字符串进行显示
if (serialPort1.BytesToRead == 0)
{
//判断串口缓存区没有可以在读的字节数据
string xx = Encoding.UTF8.GetString(datahuanchun.ToArray());
receive_rtb.AppendText(xx);
//接收完成后清空datahaunchun
datahuanchun.Clear();
}
}
}));
具体实验结果如下图所示:
这个问题如果你用单步调试可能查不出问题,因为单步调试执行一步,中间存在间隔时间,串口传输的线程可以在间隔时间内将全部数据发送给你,当你解码的时候,数据往往是一段完整的数据。
注意事项:在使用串口方式进行数据收发过程中,需要知道串口数据并不是一次性全部发送,它可能是分多批次地给你发送。这个结果就可能导致中文转码时发生乱码问题,为什么这么说呢,听我细细道来:
(1)单个字母和数字对应一个字节
(2)一个中文(UTF8编码)对应三个字节;一个中文(GB2312编码)对应两个字节
举个例子:
举个例子:
假设你串口接收 "你" 字(UTF-8编码为 E4 BD A0
):
-
第一次读取:收到
E4 BD
(2个字节,还差1个字节) -
第二次读取:收到
A0
程序连续执行时(正常运行):
-
程序执行速度快,串口缓冲区数据还没完全到齐你就开始处理。
-
比如 UTF-8 中文“你”应为
E4 BD A0
,但你先只收到E4 BD
,立刻尝试解码就会报错或乱码。
2.10.3 中文乱码解决方法
以下是程序编写流程图:需要用到lock锁
开始
|
v
[进入 serialPort1_DataReceived 事件]
|
v
{是否按下“暂停接收”按钮? (receiveisopen == false)}
| 是
v
[直接 return,结束接收]
|
v
结束
|
否
v
{串口接收缓存是否为空? (BytesToRead == 0)}
| 是
v
[直接 return,结束接收]
|
否
v
[定义接收字节数组 z,并读取数据到 z]
|
v
[将读取数据加入 datahuanchun 缓存(加锁 lock)]
|
v
[Invoke 主线程处理接收显示逻辑]
|
v
{是否勾选“16进制显示”? (hex_receive_ckb.Checked == true)}
| 是
v
[此处未写实现逻辑,可扩展为 hex 格式显示]
|
否
v
[进入文本显示分支(加锁 datahuanchun)]
|
v
[UTF8 解码缓存数据 → charBuffer]
|
v
[将解码结果追加到接收显示区 receive_rtb]
|
v
[清空 datahuanchun 缓存]
|
v
[更新接收数据统计 senddata_num + data长度]
|
v
[更新状态栏 toolStripStatusLabel4]
|
v
结束
等后面串口软件写好后,再上传下一部分内容