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);
    }
}

逐行核心原理(必考大题)

  1. DataReceived 事件:串口缓冲区有数据自动触发,运行在独立子线程

  2. BytesToRead:获取当前缓存区滞留数据字节数

  3. byte[] 缓冲区接收:串口通信底层全部是字节传输

  4. GBK解码:字节转中文字符串

  5. Invoke 跨线程更新UI:子线程禁止操作控件,必须切回主线程渲染文本


九、代码BUG修复(原代码问题汇总)

原代码存在问题

  • 串口参数配置代码被注释,参数未生效

  • 接收事件未绑定,无法接收数据

  • 缺少 System.IO.Ports 命名空间会报错

  • 异常直接抛出,会导致程序崩溃

本笔记已全部修复,可直接运行


十、期末必考核心知识点总结

1. 串口通信五要素

串口号、波特率、数据位、停止位、校验位 设备两端必须完全一致 才能通信

2. 乱码解决核心

Windows默认UTF8,硬件设备大多GBK,手动指定 Encoding.GBK 完美解决中文乱码

3. 跨线程UI报错原因

DataReceived 是后台子线程,WinForm控件仅主线程可操作,必须使用 Invoke 回调

4. 收发本质

串口底层传输永远是 字节数组 byte[],所有字符串都是编码转换得到

十一、标准运行流程

  1. 输入串口号(COMx)

  2. 点击打开串口

  3. 输入发送内容,点击发送

  4. 下位机返回数据自动接收、显示在富文本框

  5. 关闭串口释放资源

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;
            }
        }

满分核心流程(必背)

  1. 零散接收:每次收到多少存多少

  2. 全局累积:全部存入 List<byte> 缓冲区

  3. 循环匹配帧尾:判断是否收完一整包

  4. 截取完整帧:只解析合法完整数据

  5. 裁剪缓存:已解析数据删除,残留数据继续等待拼接

  6. 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修正(笔记优化)

  • 原异常直接抛出,容易闪退,可优化为弹窗提示

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐