WinForm 串口通信(SerialPort)基础完整版笔记|GBK编码|收发数据|串口开关+WinForm 串口通信进阶笔记(解决数据不全、粘包分包、帧尾校验)
WinForm 串口通信(SerialPort)基础完整版笔记|GBK编码|收发数据|串口开关
一、项目简介
本案例为 WinForm 原生 SerialPort 串口通信基础版本,是物联网、下位机通信、单片机对接必考代码。
实现功能:
-
自定义输入串口号(COM1、COM3等)
-
串口打开、关闭功能
-
文本数据串口发送
-
监听串口接收事件,实时接收下位机数据
-
采用 GBK 中文编码,解决中文乱码问题
-
跨线程更新UI(Invoke 解决线程报错)
二、完整引用命名空间
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
// 【核心必备】串口通信专属命名空间
using System.IO.Ports;
注意:SerialPort、Parity、StopBits、SerialDataReceivedEventArgs 全部依赖该命名空间,必须引用。
三、全局变量定义
public partial class Form1 : Form
{
// 定义全局GBK编码对象,统一收发编码,解决中文乱码
public Encoding gbk = Encoding.GetEncoding("GBK");
// 临时接收数据缓存(备用)
public string temp = "";
知识点
-
Windows串口默认编码非GBK,接收中文必定乱码,手动指定 GBK 为工业标准
-
全局定义只实例化一次,避免重复创建对象卡顿
四、窗体构造方法(串口参数初始化)
原代码存在 代码未启用、事件未绑定 问题,下面为规范完整版注释 + 补全配置
public Form1()
{
InitializeComponent();
// ========== 串口基础参数配置(工业标准默认参数) ==========
// 1.波特率:最常用 9600
serialPort1.BaudRate = 9600;
// 2.奇偶校验:无校验
serialPort1.Parity = Parity.None;
// 3.数据位:标准8位
serialPort1.DataBits = 8;
// 4.停止位:1位停止位
serialPort1.StopBits = StopBits.One;
// 5.设置收发编码为GBK,支持中文
serialPort1.Encoding = gbk;
// 6.绑定数据接收事件(重点!不绑定无法接收数据)
serialPort1.DataReceived += serialPort1_DataReceived;
}
串口四大核心参数(考试必背)
-
波特率 BaudRate:通信速率,设备双方必须一致(9600/115200最常用)
-
数据位 DataBits:默认8位,传输有效数据
-
停止位 StopBits:标识一帧数据结束
-
奇偶校验 Parity:数据纠错,默认None无校验
五、功能1:打开串口按钮(Button1)
private void button1_Click(object sender, EventArgs e)
{
// 从文本框获取用户输入的串口号 COM1/COM3/COM6
serialPort1.PortName = textBox1.Text;
// 判断:串口未打开则开启
if (!serialPort1.IsOpen)
{
serialPort1.Open();
MessageBox.Show("串口打开成功");
}
}
核心逻辑
-
IsOpen:串口状态判断属性 -
Open():开启串口通道 -
串口号由用户手动输入,灵活适配不同设备
六、功能2:发送数据按钮(Button2)
private void button2_Click(object sender, EventArgs e)
{
// 校验:串口未打开禁止发送
if (!serialPort1.IsOpen)
{
MessageBox.Show("串口还未打开");
return;
}
// 发送数据 + 自动换行符 \r\n
// 大部分单片机、下位机需要回车换行作为结束符
serialPort1.Write(textBox2.Text + "\r\n");
}
考点
-
发送前必须判断串口状态,否则报错
-
\r\n换行符:模拟串口帧结束标识,适配绝大多数设备 -
Write():字符串直接发送方法
七、功能3:关闭串口按钮(Button3)
private void button3_Click(object sender, EventArgs e)
{
// 关闭串口,释放占用端口资源
serialPort1.Close();
}
重要:串口必须手动 Close,否则端口被程序占用,重启电脑才能恢复。
八、核心功能:串口数据接收事件(重点难点)
/// <summary>
/// 串口数据接收事件:后台线程触发,不能直接操作UI
/// </summary>
private void serialPort1_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
// 安全校验:串口为空/未打开 直接返回
if (!serialPort1.IsOpen || serialPort1 == null)
{
return;
}
try
{
// 1. 获取缓冲区未读取的字节长度
int length = serialPort1.BytesToRead;
if (length <= 0) return;
// 2. 根据长度创建字节数组
byte[] buffer = new byte[length];
// 3. 读取缓冲区数据到字节数组
serialPort1.Read(buffer, 0, length);
// 4. GBK解码,转字符串(解决中文乱码)
string temp = gbk.GetString(buffer);
// 5. 跨线程更新UI【必考】
// 接收事件是子线程,不能直接修改文本框,必须Invoke回到主线程
Invoke(new Action(() =>
{
richTextBox1.AppendText(temp + Environment.NewLine);
}));
}
catch (Exception E)
{
throw new Exception(E.Message);
}
}
逐行核心原理(必考大题)
-
DataReceived 事件:串口缓冲区有数据自动触发,运行在独立子线程
-
BytesToRead:获取当前缓存区滞留数据字节数
-
byte[] 缓冲区接收:串口通信底层全部是字节传输
-
GBK解码:字节转中文字符串
-
Invoke 跨线程更新UI:子线程禁止操作控件,必须切回主线程渲染文本
九、代码BUG修复(原代码问题汇总)
原代码存在问题
-
串口参数配置代码被注释,参数未生效
-
接收事件未绑定,无法接收数据
-
缺少 System.IO.Ports 命名空间会报错
-
异常直接抛出,会导致程序崩溃
本笔记已全部修复,可直接运行
十、期末必考核心知识点总结
1. 串口通信五要素
串口号、波特率、数据位、停止位、校验位 设备两端必须完全一致 才能通信
2. 乱码解决核心
Windows默认UTF8,硬件设备大多GBK,手动指定 Encoding.GBK 完美解决中文乱码
3. 跨线程UI报错原因
DataReceived 是后台子线程,WinForm控件仅主线程可操作,必须使用 Invoke 回调
4. 收发本质
串口底层传输永远是 字节数组 byte[],所有字符串都是编码转换得到
十一、标准运行流程
-
输入串口号(COMx)
-
点击打开串口
-
输入发送内容,点击发送
-
下位机返回数据自动接收、显示在富文本框
-
关闭串口释放资源
WinForm 串口通信进阶笔记(解决数据不全、粘包分包、帧尾校验)
一、核心解决问题(重点)
普通串口代码存在致命问题:
-
串口缓存数据是流式断续接收,一次事件可能收不全一整包数据
-
多条数据粘连在一起(粘包)
-
一条数据被拆分多次接收(分包)
-
直接读取会导致:数据缺失、乱码、截断、错乱
本方案核心原理:全局缓冲区缓存 + 帧尾标志匹配截取完整数据包,彻底解决串口数据接收不完整问题。
二、命名空间
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.IO.Ports; // 串口核心
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Windows.Forms;
三、全局变量定义(核心缓存体系)
namespace _2解决串口通信的出现数据不全的情况
{
public partial class Form1 : Form
{
// 实例化串口对象
SerialPort SerialPort = new SerialPort();
// GBK编码:解决中文乱码
public Encoding gbk = Encoding.GetEncoding("GBK");
//【最关键】全局字节缓冲区:累积所有接收的字节,解决分包、粘包
public List<byte> byteBuffer = new List<byte>();
// 帧结束标志:\r\n 对应的十六进制 0D 0A
public byte[] endFlag = new byte[] { 0x0D, 0x0A };
变量作用解析
-
byteBuffer 全局缓存:所有零散数据先存入缓存,不直接处理
-
endFlag:约定数据包结束标记,遇到 \r\n 代表一帧数据接收完成
四、窗体构造函数:串口参数初始化
public Form1()
{
InitializeComponent();
// 绑定数据接收事件
SerialPort.DataReceived += SerialPort_DataReceived;
// 串口标准参数配置
SerialPort.DataBits = 8;
SerialPort.Parity = Parity.None;
SerialPort.StopBits = StopBits.One;
SerialPort.BaudRate = 9600;
// 指定GBK编码
SerialPort.Encoding = gbk;
}
五、核心工具方法:FindEndFlageIndex 帧尾匹配算法
功能:在全局缓存字节集合中,查找结束标志\r\n的索引,判断是否接收完整一帧数据
/// <summary>
/// 在字节缓存中查找帧结束标记 \r\n
/// </summary>
/// <param name="socurce">全局缓存字节集合</param>
/// <param name="endF">结束标志字节数组</param>
/// <returns>成功返回索引,未找到返回-1</returns>
public int FindEndFlageIndex(List<byte> socurce, byte[] endF)
{
// 缓存数据长度不足结束符长度,直接返回
if (socurce.Count < endF.Length)
return -1;
// 遍历缓存字节,逐位匹配结束符
for (int i = 0; i <= socurce.Count - endF.Length; i++)
{
bool match = true;
for (int j = 0; j < endF.Length; j++)
{
// 任意字节不匹配,跳出
if (socurce[i + j] != endF[j])
{
match = false;
break;
}
}
// 完全匹配,返回当前起始索引
if (match)
return i;
}
return -1;
}
算法原理
-
滑动窗口逐字节比对
-
精准匹配
0x0D 0x0A(\r\n) -
找到则说明:已有完整一包数据
六、核心难点:DataReceived 接收事件(粘包分包终极解决方案)
private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
try
{
// 1. 读取当前串口缓冲区所有字节
int readLen = SerialPort.BytesToRead;
if (readLen <= 0) return;
// 2. 临时接收本次零散数据
byte[] tempBuffer = new byte[readLen];
SerialPort.Read(tempBuffer, 0, readLen);
// 3.【关键】追加到全局缓存,数据累积,不丢失、不截断
byteBuffer.AddRange(tempBuffer);
// 4. 循环解析:保证一次接收多条数据也能全部解析完成
while (true)
{
// 查找结束符位置
int index = FindEndFlageIndex(byteBuffer, endFlag);
if (index == -1) break; // 没有完整包,退出等待下次数据
// 5. 截取【完整一帧数据】(包含结束符)
byte[] fullData = byteBuffer.Take(index + endFlag.Length).ToArray();
// 6.【关键】移除已解析完毕的数据,避免重复解析
byteBuffer.RemoveRange(0, index + endFlag.Length);
// 7. 子线程更新UI,必须Invoke
Invoke(new Action(() =>
{
// GBK解码
string txt = gbk.GetString(fullData);
// 清除自带换行符,界面排版整洁
string t1 = txt.Replace("\r", "").Replace("\n", "");
// 追加显示
richTextBox1.AppendText(t1);
// 自动滚动到最新数据
richTextBox1.ScrollToCaret();
}));
}
}
catch (Exception)
{
throw;
}
}
满分核心流程(必背)
-
零散接收:每次收到多少存多少
-
全局累积:全部存入 List<byte> 缓冲区
-
循环匹配帧尾:判断是否收完一整包
-
截取完整帧:只解析合法完整数据
-
裁剪缓存:已解析数据删除,残留数据继续等待拼接
-
UI刷新:完整数据包展示,杜绝残缺数据
七、按钮功能模块
1. 打开串口
private void button1_Click(object sender, EventArgs e)
{
// 设置串口号
SerialPort.PortName = textBox1.Text;
// 防重复打开
if (!SerialPort.IsOpen)
{
SerialPort.Open();
}
}
2. 发送数据
private void button2_Click(object sender, EventArgs e)
{
// 发送内容自带帧尾,与接收协议统一
SerialPort.Write(textBox2.Text + "\r\n");
// 清空缓存(可选,防止新旧数据混杂)
byteBuffer.Clear();
}
3. 关闭串口(规范释放资源)
private void button3_Click(object sender, EventArgs e)
{
if (SerialPort.IsOpen)
{
// 先解绑事件,防止关闭后触发报错
SerialPort.DataReceived -= SerialPort_DataReceived;
// 关闭端口释放资源
SerialPort.Close();
}
}
}
八、本代码升级亮点(对比基础版)
-
解决分包:一次没收完,数据留在缓存,下次继续拼接
-
解决粘包:循环截取,多条粘连数据逐条拆分解析
-
数据绝对完整:只解析带结束符的完整数据包
-
资源规范释放:关闭前解绑事件,杜绝闪退报错
-
界面自动滚动:实时锁定最新消息
九、考试/面试核心考点
1. 为什么会数据不全?
串口DataReceived事件触发时机不确定,底层是流式字节传输,单次事件不一定接收完整一帧数据。
2. 解决粘包分包标准方案
缓冲区累积 + 帧头帧尾校验 + 循环截取完整包(工业级标准写法)
3. \r\n 对应字节
-
\r =
0x0D -
\n =
0x0A
4. List<byte> 优势
长度动态可变,适合不定长串口数据缓存,比固定byte[]更适合通信项目。
十、原代码微小BUG修正(笔记优化)
-
原异常直接抛出,容易闪退,可优化为弹窗提示
更多推荐



所有评论(0)