C# SerialPort类实现串口通信的实战指南

 更新时间:2026年01月18日 09:39:35   作者:小码编匠  
在工业自动化、物联网和嵌入式系统中,串口通信仍然扮演着不可替代的角色,本文将从基础使用出发,深入讲解 SerialPort 类的属性、事件处理、异常捕获及 WinForm 中的实际应用,帮助开发开发稳定、安全的串口通信程序

前言

在工业自动化、物联网和嵌入式系统中,串口通信仍然扮演着不可替代的角色。尽管网络通信技术发展迅速,但在一些对稳定性、实时性要求较高的场景中,串口通信依然具有广泛的应用基础。

C# 语言通过 System.IO.Ports 命名空间中的 SerialPort 类,为开发者提供了便捷的串口编程接口。本文将从基础使用出发,深入讲解 SerialPort 类的属性、事件处理、异常捕获及 WinForm 中的实际应用,帮助开发开发稳定、安全的串口通信程序。

一、SerialPort 类

1、SerialPort 类的基本属性与构造函数

C# 提供了 SerialPort 类用于串口通信,它支持多种构造函数。一个完整的构造函数如下:

public SerialPort(
    string portName,
    int baudRate,
    Parity parity,
    int dataBits,
    StopBits stopBits
)

例如,设置串口为 COM1、波特率 9600、无奇偶校验、数据位 8 和停止位 1 的代码如下:

SerialPort serialPort = new SerialPort("COM1", 9600, Parity.None, 8, StopBits.One);

2、属性说明

属性名称描述常用取值
PortName串口号,如 "COM1"、"COM2"具体的系统端口号
BaudRate数据传输速率9600、115200 等
Parity奇偶校验None、Even、Odd
DataBits每个数据帧的位数8
StopBits数据帧结束标志One、Two
Handshake数据传输时的流控制措施None、XOnXOff、RequestToSend
ReadTimeout读取数据的超时时间毫秒数,如 500
WriteTimeout写入数据的超时时间毫秒数,如 500
CtsHoldingtrue 表示对方设备已准备好接收数据true、false,需硬件 CTS 支持
CDHoldingtrue 表示检测到载波信号true、false,需硬件 CD 支持

二、串口事件:数据接收与 UI 更新

在串口通信中,数据接收是核心环节。SerialPort 类通过 DataReceived 事件实现异步接收。在 WinForm 应用中,事件处理函数运行在后台线程,不能直接更新 UI 控件。为此,应使用 BeginInvoke 方法进行异步更新,避免阻塞主线程。

public delegate void UpdateUIDelegate(byte[] data);

private void Comm_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
    byte[] receivedData = new byte[8];
    try
    {
        serialPort.Read(receivedData, 0, 6);
        this.BeginInvoke(new UpdateUIDelegate(UpdateUI), receivedData);
    }
    catch (TimeoutException ex)
    {
        MessageBox.Show("超时:" + ex.Message);
    }
}

private void UpdateUI(byte[] data)
{
    string receivedStr = System.Text.Encoding.Default.GetString(data);
    this.textBoxData.Text = receivedStr;
}

三、串口的打开、关闭与参数配置

1、打开串口

打开串口前应确保参数配置完成,并进行异常捕获:

try
{
    serialPort.Open();
}
catch (UnauthorizedAccessException ex)
{
    MessageBox.Show("权限不足或串口正在使用:" + ex.Message);
}
catch (IOException ex)
{
    MessageBox.Show("I/O错误:" + ex.Message);
}

2、关闭串口与安全退出

关闭串口时,若存在未完成的线程操作,可能导致死锁。

建议使用标志变量控制流程:

private bool isReceiving = false;
private bool isTryingToClose = false;

public void SafeCloseSerialPort()
{
    isTryingToClose = true;
    while (isReceiving)
    {
        System.Windows.Forms.Application.DoEvents();
    }
    serialPort.Close();
}

3、参数配置示例

SerialPort serialPort = new SerialPort();
serialPort.PortName = "COM5";
serialPort.BaudRate = 115200;
serialPort.Parity = Parity.None;
serialPort.DataBits = 8;
serialPort.StopBits = StopBits.One;
serialPort.Handshake = Handshake.None;
serialPort.ReadTimeout = 500;
serialPort.WriteTimeout = 500;

四、常见异常处理策略

1、端口占用与权限问题

try
{
    serialPort.Open();
}
catch (UnauthorizedAccessException ex)
{
    MessageBox.Show("串口访问权限不足:" + ex.Message);
}
catch (IOException ex)
{
    MessageBox.Show("串口可能不存在或被占用:" + ex.Message);
}

2、超时异常处理

try
{
    byte[] buffer = new byte[serialPort.BytesToRead];
    int bytesRead = serialPort.Read(buffer, 0, buffer.Length);
}
catch (TimeoutException ex)
{
    MessageBox.Show("数据读取超时:" + ex.Message);
}

3、未打开串口的操作

if (!serialPort.IsOpen)
{
    MessageBox.Show("串口尚未打开,请先调用 Open() 方法。");
    return;
}

4、CtsHolding 与 CDHolding 支持

public static bool IsHardwareFlowControlSupported(SerialPort port)
{
    try
    {
        bool originalRts = port.RtsEnable;
        bool originalDtr = port.DtrEnable;

        port.RtsEnable = false;
        Thread.Sleep(10);
        bool ctsLow = port.CtsHolding;

        port.RtsEnable = true;
        Thread.Sleep(10);
        bool ctsHigh = port.CtsHolding;

        port.RtsEnable = originalRts;
        port.DtrEnable = originalDtr;

        return ctsLow != ctsHigh;
    }
    catch
    {
        return false;
    }
}

五、WinForm 环境下串口通信的实现示例

以下是一个完整的 WinForm 示例界面和核心代码结构:

using System.IO.Ports;
using Timer = System.Windows.Forms.Timer;

namespace AppSerialPortExplained
{
    public partial class Form1 : Form
    {
        private SerialPort serialPort = new SerialPort();
        private Timer statusTimer = new Timer();

        public Form1()
        {
            InitializeComponent();
            RefreshPortList();
            comboBoxBaud.Items.AddRange(new object[] { "1200", "2400", "4800", "9600", "19200", "38400", "57600", "115200" });
            comboBoxBaud.SelectedIndex = 3;
            comboBoxParity.Items.AddRange(new object[] { "None", "Even", "Odd", "Mark", "Space" });
            comboBoxParity.SelectedIndex = 0;
            comboBoxDataBits.Items.AddRange(new object[] { "5", "6", "7", "8" });
            comboBoxDataBits.SelectedIndex = 3;
            comboBoxStopBits.Items.AddRange(new object[] { "One", "Two", "OnePointFive" });
            comboBoxStopBits.SelectedIndex = 0;
            comboBoxHandshake.Items.AddRange(new object[] { "None", "XOnXOff", "RequestToSend", "RequestToSendXOnXOff" });
            comboBoxHandshake.SelectedIndex = 0;
            numericUpDownReadTimeout.Value = 500;
            numericUpDownWriteTimeout.Value = 500;
            serialPort.DataReceived += SerialPort_DataReceived;
            serialPort.ErrorReceived += SerialPort_ErrorReceived;
            serialPort.PinChanged += SerialPort_PinChanged;
            InitializeTimer();
        }

        private void InitializeTimer()
        {
            statusTimer.Interval = 100;
            statusTimer.Tick += StatusTimer_Tick;
        }

        private void RefreshPortList()
        {
            string selectedPort = comboBoxPort.Text;
            comboBoxPort.Items.Clear();
            comboBoxPort.Items.AddRange(SerialPort.GetPortNames());
            if (comboBoxPort.Items.Count > 0)
            {
                if (comboBoxPort.Items.Contains(selectedPort))
                    comboBoxPort.Text = selectedPort;
                else
                    comboBoxPort.SelectedIndex = 0;
            }
        }

        private void buttonRefresh_Click(object sender, EventArgs e)
        {
            RefreshPortList();
        }

        private void buttonOpen_Click(object sender, EventArgs e)
        {
            if (!serialPort.IsOpen)
            {
                try
                {
                    serialPort.PortName = comboBoxPort.Text;
                    serialPort.BaudRate = int.Parse(comboBoxBaud.Text);
                    switch (comboBoxParity.Text)
                    {
                        case "None": serialPort.Parity = Parity.None; break;
                        case "Even": serialPort.Parity = Parity.Even; break;
                        case "Odd": serialPort.Parity = Parity.Odd; break;
                        case "Mark": serialPort.Parity = Parity.Mark; break;
                        case "Space": serialPort.Parity = Parity.Space; break;
                    }
                    serialPort.DataBits = int.Parse(comboBoxDataBits.Text);
                    switch (comboBoxStopBits.Text)
                    {
                        case "One": serialPort.StopBits = StopBits.One; break;
                        case "Two": serialPort.StopBits = StopBits.Two; break;
                        case "OnePointFive": serialPort.StopBits = StopBits.OnePointFive; break;
                    }
                    serialPort.Handshake = (Handshake)Enum.Parse(typeof(Handshake), comboBoxHandshake.Text.Replace("XOnXOff", "XonXOff"));
                    serialPort.ReadTimeout = (int)numericUpDownReadTimeout.Value;
                    serialPort.WriteTimeout = (int)numericUpDownWriteTimeout.Value;
                    serialPort.Open();
                }
                catch (Exception ex)
                {
                    MessageBox.Show("打开串口失败:" + ex.Message);
                }
            }
        }
    }
}

效果预览

六、常见问题与解决方案

在实际开发过程中,使用 SerialPort 类时会遇到许多常见问题,下面列举并详细介绍解决方案:

死锁问题与 UI 更新阻塞

在调用 serialPort.Close() 时,如果数据接收线程仍在运行,采用 Invoke 调用 UI 更新方法会导致同步等待,最终引起死锁问题。解决这一问题的方法是改为使用 BeginInvoke 进行异步调用,以避免线程阻塞。

串口线程安全性问题

在多线程环境下,数据接收线程与 UI 主线程可能同时访问共享资源,若不加保护,容易引起数据竞争问题。通常的解决办法是采用标志控制(如 isReceivingisTryingToClose)以及使用 Application.DoEvents() 循环确保所有后台线程结束后再关闭串口。

异常捕获不足

许多开发在编写串口通信代码时,往往忽略了对各种异常(超时、I/O 错误、未打开串口等)的充分捕获。应在关键操作(如 Open、Read、Write)处使用 try-catch 结构,将异常信息反馈给用户,并记录日志以便后续分析。

串口数据粘包或格式不正确

在数据连续传输的场景中,串口可能会因为数据粘包的问题导致解析错误。为解决这一问题,建议在数据传输协议中明确数据边界,如采用特定的分隔符,或者在数据头部增加包长度信息,然后在接收时进行数据拆包解析。

总结

通过对 SerialPort 类的详细解析,本文展示了如何在 WinForm 环境下正确设置串口参数、打开关闭串口以及处理常见的异常情况。合理的异常捕获、线程安全机制以及 UI 数据更新策略,不仅提高了应用的稳定性,也为编写高质量串口通信程序提供了有效的技术支持。

在工业自动化、嵌入式设备通信等领域,串口通信依然是不可替代的技术手段。随着国产软硬件生态的不断完善,开发在串口通信方面的实践经验也日益丰富。面对未来不断变化的硬件通信需求,开发应继续关注异常自愈和智能数据解析技术的进步,为行业应用提供更全面、可靠的解决方案。

以上就是C# SerialPort类实现串口通信的实战指南的详细内容,更多关于C# SerialPort串口通信的资料请关注脚本之家其它相关文章!

相关文章

  • 使用Spire.PDF在C#中创建和绘制PDF表单的方法步骤

    使用Spire.PDF在C#中创建和绘制PDF表单的方法步骤

    在日常开发中,我们经常需要处理各种文档,其中PDF因其跨平台、安全性高而成为企业级应用中不可或缺的一部分,然而,手动创建、填写或管理PDF表单无疑是一项耗时且易出错的任务,所以本文给大家介绍了如何使用Spire.PDF在C#中创建和绘制 PDF 表单,需要的朋友可以参考下
    2025-08-08
  • C# 如何获取出错的错误所在行数信息

    C# 如何获取出错的错误所在行数信息

    本文主要介绍 C# 中获取错误所在行的方法,在开发过程中或是用户在使用过程中,出错的话方便我们快速定位到错误的位置,以便我们处理。
    2016-04-04
  • C#/VB.NET 自定义PPT动画路径的步骤

    C#/VB.NET 自定义PPT动画路径的步骤

    这篇文章主要介绍了C#/VB.NET 自定义PPT动画路径的步骤,帮助大家更好的理解和学习使用c#,感兴趣的朋友可以了解下
    2021-05-05
  • C#实现gRPC服务和调用示例详解

    C#实现gRPC服务和调用示例详解

    gRPC 是一种与语言无关的高性能远程过程调用 (RPC) 框架,这篇文章主要为大家详细介绍了C#如何实现gRPC服务和调用,需要的可以参考一下
    2024-01-01
  • Unity的IPreprocessBuildWithReport实用案例深入解析

    Unity的IPreprocessBuildWithReport实用案例深入解析

    这篇文章主要为大家介绍了Unity的IPreprocessBuildWithReport实用案例深入解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-05-05
  • C#中Winfrom默认输入法的设置方法

    C#中Winfrom默认输入法的设置方法

    这篇文章主要介绍了C#中Winfrom默认输入法的设置方法,以实例形式较为详细的分析了C#中输入法设置的相关技巧,需要的朋友可以参考下
    2015-05-05
  • c#添加图片、文本水印到PDF文件

    c#添加图片、文本水印到PDF文件

    这篇文章主要介绍了如何用c#给PDF文件添加文本、图片水印,文中代码非常详细供大家学习参考,感兴趣的朋友可以了解下
    2020-06-06
  • C#流程控制详解

    C#流程控制详解

    这篇文章主要介绍了C#流程控制详解,文章围绕主题展开详细的内容介绍,具有一定的参考价值,需要的小伙伴可以参考一下
    2022-07-07
  • Unity实现虚拟摇杆效果

    Unity实现虚拟摇杆效果

    这篇文章主要为大家详细介绍了Unity实现虚拟摇杆效果,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-04-04
  • 基于C#实现鼠标设置功能

    基于C#实现鼠标设置功能

    这篇文章主要为大家详细介绍了如何利用C#实现鼠标设置功能,文中的示例代码讲解详细,对我们学习C#有一定的帮助,感兴趣的小伙伴可以跟随小编一起了解一下
    2022-12-12

最新评论