C#常用类库-详解SerialPort
在C#硬件通信开发中,串口(Serial Port)是最基础、最常用的通信方式之一,广泛应用于工业控制、物联网设备、嵌入式系统、智能硬件等场景(如PLC、传感器、RFID读卡器、单片机通信)。.NET框架内置的System.IO.Ports.SerialPort类库,无需第三方依赖,即可实现串口的打开、关闭、数据发送与接收,是C#开发者进行硬件串口通信的首选工具。
本文摒弃冗余理论,聚焦"简练、详细、有深度",从SerialPort核心定位、底层原理、环境搭建、基础实战、进阶技巧,到企业级避坑指南与实战案例,全方位解析SerialPort类库的用法,帮你快速掌握串口通信全流程,解决硬件通信中的稳定性、兼容性痛点。
一、核心定位:SerialPort 核心价值与应用场景
SerialPort类库是.NET框架对串口通信的封装,底层基于Windows API(如CreateFile、ReadFile、WriteFile)实现,屏蔽了底层硬件的复杂操作,提供简洁的API接口,支持同步/异步数据传输、参数配置、事件驱动接收等核心功能,其核心价值是"简单易用、稳定可靠、无依赖"。
1. 核心优势
-
内置集成:属于.NET基础类库(System.IO.Ports),无需安装第三方NuGet包,支持.NET Framework 2.0+、.NET Core 2.1+、.NET 5+,兼容性极强。
-
API简洁:封装了串口打开、关闭、发送、接收的全流程,几行代码即可实现基础串口通信,降低硬件开发门槛。
-
功能完整:支持同步/异步传输、事件驱动接收、参数动态配置(波特率、数据位、校验位等),适配绝大多数串口设备。
-
稳定可靠:底层对接系统串口驱动,传输效率高,支持断点续传、异常捕获,满足工业级通信需求。
2. 与第三方串口类库对比(选型关键)
| 类库 | 核心优势 | 不足 | 选型建议 |
|---|---|---|---|
| System.IO.Ports.SerialPort(内置) | 无依赖、API简洁、兼容性强、稳定可靠,支持所有.NET框架 | 高级功能(如多串口并发、自定义缓冲区)需手动封装 | 绝大多数场景首选,尤其是工业控制、简单硬件通信 |
| SerialPortStream(第三方) | 支持多线程并发、自定义缓冲区,性能略优于内置SerialPort | 需安装NuGet包,兼容性略差(不支持旧版.NET Framework) | 多串口并发、高吞吐场景 |
| SPCom(第三方) | 封装完善,支持多种通信协议(如Modbus),上手快 | 闭源、更新不及时,不适用于复杂定制化场景 | 快速开发简单串口设备通信(如单片机调试) |
3. 核心应用场景
-
工业控制:与PLC、变频器、传感器等工业设备通信,实现数据采集与指令下发。
-
物联网设备:与RFID读卡器、蓝牙模块、GPS模块等串口设备通信,传输设备数据。
-
嵌入式开发:与单片机(如51、STM32)通信,调试程序、传输控制指令。
-
智能硬件:与打印机、扫码枪、POS机等外设通信,实现数据交互。
-
调试工具:开发串口调试助手,用于硬件设备调试、数据监控。
二、底层原理:SerialPort 工作机制(深度解析)
要熟练使用SerialPort,需理解其底层工作机制,避免因配置错误、使用不当导致通信失败。SerialPort本质是对系统串口驱动的封装,核心工作流程分为"参数配置→串口打开→数据传输→串口关闭"四个阶段。
1. 核心工作流程
-
参数配置:设置串口名称(如COM1)、波特率、数据位、停止位、校验位等,这些参数必须与串口设备(如单片机)的配置完全一致,否则会出现数据乱码、通信失败。
-
串口打开:通过调用Open()方法,底层调用Windows API的CreateFile()函数,获取串口设备句柄,建立与硬件的连接。
-
数据传输:
-
发送数据:调用Write()/WriteLine()方法,将数据转换为字节流,通过串口发送给硬件设备,底层调用WriteFile()函数。
-
接收数据:通过事件驱动(DataReceived事件)或同步读取(Read()/ReadLine())获取硬件发送的数据,底层调用ReadFile()函数。
- 串口关闭:调用Close()方法,释放串口句柄,关闭与硬件的连接,避免资源泄漏。
2. 核心参数解析(通信成功的关键)
串口通信的核心是"参数匹配",SerialPort的关键参数必须与硬件设备完全一致,以下是常用参数的详细说明:
-
PortName:串口名称(必填),Windows系统中通常为COM1、COM2...,Linux/macOS中为/dev/ttyS0、/dev/ttyUSB0,可通过SerialPort.GetPortNames()方法获取系统可用串口。
-
BaudRate:波特率(必填),数据传输速率(单位:bps),常用值为9600、19200、38400、115200,需与硬件一致(最常用9600,兼容性最好)。
-
DataBits:数据位(可选,默认8),表示每帧数据的位数,常用值为7、8,一般设置为8。
-
StopBits:停止位(可选,默认One),表示每帧数据结束后的停止信号位数,可选值:One(1位)、Two(2位)、None(无)、OnePointFive(1.5位),常用One。
-
Parity:校验位(可选,默认None),用于校验数据传输是否出错,可选值:None(无校验)、Odd(奇校验)、Even(偶校验)、Mark(标记校验)、Space(空格校验),常用None(无校验)。
-
Handshake:握手协议(可选,默认None),用于控制数据传输的同步,可选值:None(无握手)、XOnXOff(软件握手)、RequestToSend(硬件握手RTS/CTS)、RequestToSendXOnXOff(混合握手),常用None。
-
ReadTimeout/WriteTimeout:读写超时时间(可选,默认Infinite),单位:毫秒,设置为Infinite表示无限等待,避免因硬件响应慢导致程序卡死。
3. 数据传输方式(同步vs异步)
SerialPort支持两种数据传输方式,适用于不同场景,核心区别如下:
-
同步传输:调用Read()、Write()等同步方法,程序会阻塞等待操作完成,适用于简单、低频率的通信场景(如单次发送指令、读取少量数据)。
-
异步传输:通过DataReceived事件(接收数据)、BeginWrite()/EndWrite()(发送数据)实现,程序不会阻塞,适用于高频率、大量数据传输,或UI界面开发(避免界面卡死)。
三、环境搭建:快速集成SerialPort到C#项目
SerialPort是.NET内置类库,无需安装第三方依赖,只需引入对应命名空间,即可快速使用,以下是完整搭建流程(适配.NET Framework、.NET Core、.NET 5+)。
1. 引入命名空间(必做)
所有SerialPort相关的类、方法都在System.IO.Ports命名空间下,引入即可使用:
csharp
using System.IO.Ports; // 核心命名空间,包含SerialPort类
using System.Text; // 用于数据编码转换(如字符串与字节流转换)
2. 项目配置(可选)
-
.NET Framework项目:无需额外配置,直接引入命名空间即可。
-
.NET Core/.NET 5+项目:需在项目文件(.csproj)中添加对System.IO.Ports的引用(默认已引用,若缺失可手动添加):
xml
<!-- .csproj文件中添加 -->
<ItemGroup>
<PackageReference Include="System.IO.Ports" Version="7.0.0" />
</ItemGroup>
3. 验证环境(可选)
编写简单代码,获取系统可用串口,验证SerialPort类库是否正常可用:
csharp
// 获取系统中所有可用的串口名称
string[] ports = SerialPort.GetPortNames();
if (ports.Length == 0)
{
Console.WriteLine("❌ 未检测到可用串口,请检查硬件连接");
}
else
{
Console.WriteLine("✅ 检测到可用串口:");
foreach (var port in ports)
{
Console.WriteLine($"- {port}");
}
}
提示:若未检测到可用串口,需检查硬件设备是否正确连接、串口驱动是否安装正常。
四、基础实战:SerialPort核心用法(同步+异步)
SerialPort的核心用法分为"基础配置→串口操作→数据传输",以下分别实现同步通信与异步通信,覆盖绝大多数简单场景,代码可直接复制使用。
1. 基础配置(通用)
无论同步还是异步通信,串口配置都是基础,需先创建SerialPort实例,设置核心参数:
csharp
// 1. 创建SerialPort实例(推荐单例,避免频繁创建/关闭串口)
SerialPort serialPort = new SerialPort();
// 2. 配置核心参数(必须与硬件设备一致)
serialPort.PortName = "COM1"; // 串口名称(根据实际情况修改)
serialPort.BaudRate = 9600; // 波特率(常用9600)
serialPort.DataBits = 8; // 数据位
serialPort.StopBits = StopBits.One; // 停止位
serialPort.Parity = Parity.None; // 校验位
serialPort.Handshake = Handshake.None; // 握手协议
serialPort.ReadTimeout = 5000; // 读取超时时间(5秒)
serialPort.WriteTimeout = 5000; // 写入超时时间(5秒)
serialPort.Encoding = Encoding.ASCII; // 编码格式(根据硬件设置,常用ASCII、UTF8)
2. 同步通信(简单场景首选)
同步通信适用于单次发送指令、读取少量数据的场景,代码简洁,易于调试,核心方法:Open()、Write()、Read()、Close()。
csharp
/// <summary>
/// 同步串口通信示例(发送指令+读取响应)
/// </summary>
public void SyncSerialCommunication()
{
try
{
// 1. 打开串口
if (!serialPort.IsOpen)
{
serialPort.Open();
Console.WriteLine("✅ 串口打开成功");
}
// 2. 发送数据(向硬件发送指令,如"GET_DATA")
string sendData = "GET_DATA\r\n"; // \r\n是换行符,多数硬件需此结束符
serialPort.Write(sendData);
Console.WriteLine($"📤 发送数据:{sendData.Trim()}");
// 3. 读取数据(等待硬件响应,同步读取,会阻塞程序)
// 方式1:读取指定长度的数据(适用于固定长度响应)
byte[] buffer = new byte[1024];
int readCount = serialPort.Read(buffer, 0, buffer.Length);
string receiveData = serialPort.Encoding.GetString(buffer, 0, readCount);
// 方式2:读取一行数据(适用于以换行符结尾的响应)
// string receiveData = serialPort.ReadLine();
Console.WriteLine($"📥 接收数据:{receiveData.Trim()}");
}
catch (Exception ex)
{
Console.WriteLine($"❌ 同步通信失败:{ex.Message}");
}
finally
{
// 4. 关闭串口(无论成功失败,都需关闭,避免资源泄漏)
if (serialPort.IsOpen)
{
serialPort.Close();
Console.WriteLine("🔒 串口已关闭");
}
}
}
3. 异步通信(推荐,避免阻塞)
异步通信通过DataReceived事件接收数据,发送数据可使用BeginWrite()/EndWrite(),适用于高频率通信、UI界面开发(避免界面卡死),核心是事件绑定与异步回调。
csharp
/// <summary>
/// 异步串口通信示例(事件驱动接收数据)
/// </summary>
public void AsyncSerialCommunication()
{
try
{
// 1. 绑定DataReceived事件(接收数据时触发)
serialPort.DataReceived += SerialPort_DataReceived;
// 2. 打开串口
if (!serialPort.IsOpen)
{
serialPort.Open();
Console.WriteLine("✅ 串口打开成功(异步模式)");
}
// 3. 异步发送数据(不会阻塞程序)
string sendData = "GET_DATA\r\n";
byte[] sendBuffer = serialPort.Encoding.GetBytes(sendData);
serialPort.BeginWrite(sendBuffer, 0, sendBuffer.Length, (ar) =>
{
// 异步发送回调
serialPort.EndWrite(ar);
Console.WriteLine($"📤 异步发送数据:{sendData.Trim()}");
}, null);
// 防止程序退出(实际项目中可结合UI或循环)
Console.WriteLine("🔄 等待接收数据,按任意键退出...");
Console.ReadKey();
}
catch (Exception ex)
{
Console.WriteLine($"❌ 异步通信失败:{ex.Message}");
}
finally
{
// 4. 解绑事件、关闭串口
serialPort.DataReceived -= SerialPort_DataReceived;
if (serialPort.IsOpen)
{
serialPort.Close();
Console.WriteLine("🔒 串口已关闭");
}
}
}
/// <summary>
/// DataReceived事件回调(接收数据时触发)
/// </summary>
private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
try
{
SerialPort sp = (SerialPort)sender;
// 读取接收的数据(注意:需判断数据是否读取完整)
string receiveData = sp.ReadExisting(); // 读取所有可用数据
if (!string.IsNullOrEmpty(receiveData))
{
Console.WriteLine($"📥 异步接收数据:{receiveData.Trim()}");
}
}
catch (Exception ex)
{
Console.WriteLine($"❌ 数据接收失败:{ex.Message}");
}
}
4. 核心方法与事件解析(深度重点)
常用方法
-
Open():打开串口,获取串口设备句柄,若串口已被占用或参数错误,会抛出异常。
-
Close():关闭串口,释放句柄与资源,必须在程序退出或通信结束时调用。
-
Write(string):发送字符串数据,底层自动转换为字节流。
-
Write(byte[], int, int):发送字节数组数据,适用于二进制通信(如工业设备指令)。
-
Read(byte[], int, int):同步读取指定长度的字节数据,返回实际读取的字节数。
-
ReadLine():同步读取一行数据(以换行符\r\n结尾),适用于文本类通信。
-
ReadExisting():读取当前串口缓冲区中的所有可用数据,适用于异步接收。
-
BeginWrite()/EndWrite():异步发送数据,避免程序阻塞。
-
GetPortNames():静态方法,获取系统中所有可用的串口名称。
常用事件
-
DataReceived:当串口接收到数据时触发,是异步接收数据的核心事件,需注意线程安全(UI项目中需Invoke切换线程)。
-
ErrorReceived:当串口通信出现错误时触发(如校验错误、帧错误),可用于异常监控。
-
PinChanged:当串口引脚状态变化时触发(如RTS、CTS引脚),适用于硬件握手场景。
五、进阶实战:企业级串口通信优化(稳定性+兼容性)
基础用法可满足简单场景,但企业级项目(如工业控制、物联网)中,需解决数据丢失、通信不稳定、多串口并发、异常处理等问题,以下是核心进阶技巧。
1. 数据完整性处理(避免数据丢失/乱码)
串口通信中,数据可能分帧传输(如一次发送的数据被拆分为多段接收),直接读取易导致数据不完整、乱码,需通过"结束符判断"或"固定长度判断"保证数据完整性。
csharp
// 进阶:通过结束符(\r\n)保证数据完整性
private StringBuilder receiveBuffer = new StringBuilder(); // 接收缓冲区
private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
try
{
SerialPort sp = (SerialPort)sender;
// 读取当前可用数据,存入缓冲区
string tempData = sp.ReadExisting();
receiveBuffer.Append(tempData);
// 判断是否接收到结束符(\r\n),若有则处理完整数据
if (receiveBuffer.ToString().Contains("\r\n"))
{
string completeData = receiveBuffer.ToString().Split("\r\n")[0]; // 取完整数据
receiveBuffer.Clear(); // 清空缓冲区,准备接收下一组数据
Console.WriteLine($"📥 完整接收数据:{completeData}");
// 处理完整数据(如解析指令、存储数据)
ProcessReceivedData(completeData);
}
}
catch (Exception ex)
{
Console.WriteLine($"❌ 数据接收失败:{ex.Message}");
receiveBuffer.Clear(); // 异常时清空缓冲区,避免数据错乱
}
}
/// <summary>
/// 处理完整接收的数据(解析指令、业务逻辑)
/// </summary>
private void ProcessReceivedData(string data)
{
// 示例:解析硬件返回的指令(如"DATA:123"表示数据为123)
if (data.StartsWith("DATA:"))
{
string value = data.Split(":")[1];
Console.WriteLine($"📊 解析数据:{value}");
}
}
2. 多串口并发通信(企业级常用)
工业控制中常需同时连接多个串口设备(如多个传感器),需通过多线程+SerialPort实例池实现多串口并发,避免线程冲突。
csharp
/// <summary>
/// 多串口并发通信示例(管理多个串口设备)
/// </summary>
public class MultiSerialPortManager
{
// 串口实例字典(key:串口名称,value:SerialPort实例)
private Dictionary<string, SerialPort> _serialPorts = new Dictionary<string, SerialPort>();
// 线程安全锁
private readonly object _lockObj = new object();
/// <summary>
/// 初始化多串口
/// </summary>
/// <param name="portNames">串口名称列表(如new string[]{"COM1", "COM2"})</param>
public void InitMultiSerialPorts(string[] portNames)
{
lock (_lockObj)
{
foreach (var portName in portNames)
{
// 创建串口实例,配置参数
SerialPort sp = new SerialPort
{
PortName = portName,
BaudRate = 9600,
DataBits = 8,
StopBits = StopBits.One,
Parity = Parity.None,
ReadTimeout = 5000,
WriteTimeout = 5000,
Encoding = Encoding.ASCII
};
// 绑定接收事件
sp.DataReceived += (sender, e) =>
{
SerialPort currentSp = (SerialPort)sender;
string data = currentSp.ReadExisting();
Console.WriteLine($"📥 串口{currentSp.PortName}接收数据:{data.Trim()}");
};
// 打开串口并添加到字典
if (!sp.IsOpen)
{
sp.Open();
Console.WriteLine($"✅ 串口{portName}打开成功");
}
_serialPorts.Add(portName, sp);
}
}
}
/// <summary>
/// 向指定串口发送数据
/// </summary>
/// <param name="portName">串口名称</param>
/// <param name="data">发送数据</param>
public void SendDataToPort(string portName, string data)
{
lock (_lockObj)
{
if (_serialPorts.ContainsKey(portName) && _serialPorts[portName].IsOpen)
{
_serialPorts[portName].Write(data + "\r\n");
Console.WriteLine($"📤 向串口{portName}发送数据:{data}");
}
else
{
Console.WriteLine($"❌ 串口{portName}未打开或不存在");
}
}
}
/// <summary>
/// 关闭所有串口
/// </summary>
public void CloseAllPorts()
{
lock (_lockObj)
{
foreach (var sp in _serialPorts.Values)
{
if (sp.IsOpen)
{
sp.Close();
Console.WriteLine($"🔒 串口{sp.PortName}已关闭");
}
}
_serialPorts.Clear();
}
}
}
// 使用示例
var manager = new MultiSerialPortManager();
manager.InitMultiSerialPorts(new string[] { "COM1", "COM2" });
manager.SendDataToPort("COM1", "GET_DATA");
manager.SendDataToPort("COM2", "SET_PARAM:100");
// 程序退出时关闭所有串口
// manager.CloseAllPorts();
3. 异常处理与重试机制(提升稳定性)
串口通信易受硬件、环境影响(如串口断开、数据传输错误),需完善异常处理与重试机制,避免程序崩溃,保证通信稳定性。
csharp
/// <summary>
/// 带重试机制的串口发送方法(企业级推荐)
/// </summary>
/// <param name="data">发送数据</param>
/// <param name="retryCount">重试次数</param>
/// <returns>是否发送成功</returns>
public bool SendDataWithRetry(string data, int retryCount = 3)
{
for (int i = 0; i < retryCount; i++)
{
try
{
if (!serialPort.IsOpen)
{
serialPort.Open(); // 若串口关闭,重新打开
}
serialPort.Write(data + "\r\n");
Console.WriteLine($"📤 第{i+1}次发送数据:{data},发送成功");
return true;
}
catch (Exception ex)
{
Console.WriteLine($"❌ 第{i+1}次发送失败:{ex.Message}");
// 关闭串口,准备重试
if (serialPort.IsOpen)
{
serialPort.Close();
}
Thread.Sleep(1000); // 等待1秒后重试
}
}
Console.WriteLine($"❌ 重试{retryCount}次均失败,发送失败");
return false;
}
// 异常类型说明(针对性处理)
private void HandleSerialException(Exception ex)
{
if (ex is UnauthorizedAccessException)
{
Console.WriteLine("❌ 串口被占用,请关闭其他串口工具");
}
else if (ex is IOException)
{
Console.WriteLine("❌ 串口通信异常,可能是硬件断开或驱动错误");
}
else if (ex is TimeoutException)
{
Console.WriteLine("❌ 串口读写超时,请检查硬件响应");
}
else
{
Console.WriteLine($"❌ 未知异常:{ex.Message}");
}
}
4. UI项目中的线程安全(避免界面卡死)
WinForm、WPF等UI项目中,DataReceived事件运行在后台线程,直接操作UI控件会抛出线程安全异常,需通过Invoke/BeginInvoke切换到UI线程。
csharp
// WinForm示例:线程安全操作UI控件
private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
try
{
SerialPort sp = (SerialPort)sender;
string data = sp.ReadExisting();
if (!string.IsNullOrEmpty(data))
{
// 切换到UI线程,更新控件(如TextBox显示接收数据)
this.Invoke(new Action(() =>
{
txtReceive.Text += $"📥 {DateTime.Now:HH:mm:ss}:{data.Trim()}\r\n";
}));
}
}
catch (Exception ex)
{
this.Invoke(new Action(() =>
{
txtReceive.Text += $"❌ 接收失败:{ex.Message}\r\n";
}));
}
}
5. 二进制通信(工业设备常用)
多数工业设备(如PLC、传感器)采用二进制通信(而非文本通信),需发送/接收字节数组,通过SerialPort的Write(byte[])方法实现。
csharp
/// <summary>
/// 二进制串口通信示例(发送/接收字节数组)
/// </summary>
public void BinarySerialCommunication()
{
try
{
if (!serialPort.IsOpen)
{
serialPort.Open();
Console.WriteLine("✅ 串口打开成功(二进制模式)");
}
// 发送二进制指令(示例:PLC读取指令,0x01 0x03 0x00 0x00 0x00 0x01 0x84 0x0A)
byte[] sendBuffer = new byte[] { 0x01, 0x03, 0x00, 0x00, 0x00, 0x01, 0x84, 0x0A };
serialPort.Write(sendBuffer, 0, sendBuffer.Length);
Console.WriteLine($"📤 发送二进制指令:{BitConverter.ToString(sendBuffer).Replace("-", " ")}");
// 接收二进制数据(固定长度8字节)
byte[] receiveBuffer = new byte[8];
int readCount = serialPort.Read(receiveBuffer, 0, receiveBuffer.Length);
if (readCount == 8)
{
Console.WriteLine($"📥 接收二进制数据:{BitConverter.ToString(receiveBuffer).Replace("-", " ")}");
// 解析二进制数据(如从第3、4字节获取数值)
int value = BitConverter.ToInt16(receiveBuffer, 2);
Console.WriteLine($"📊 解析数据:{value}");
}
}
catch (Exception ex)
{
Console.WriteLine($"❌ 二进制通信失败:{ex.Message}");
}
finally
{
if (serialPort.IsOpen)
{
serialPort.Close();
}
}
}
六、避坑指南与最佳实践(企业级重点)
SerialPort使用过程中,易出现串口占用、数据丢失、乱码、程序卡死等问题,以下是实战避坑要点与最佳实践,覆盖90%以上的实际场景。
1. 常见坑与解决方案
- 坑1:串口被占用,无法打开(UnauthorizedAccessException)
原因:串口已被其他程序(如串口调试助手)占用,或串口驱动异常。
解决方案:关闭其他占用串口的程序;重新安装串口驱动;更换串口线或COM口。
- 坑2:数据乱码、接收不完整
原因:串口参数(波特率、数据位等)与硬件不一致;数据分帧传输未处理;编码格式错误。
解决方案:核对串口参数与硬件一致;使用缓冲区+结束符/固定长度判断数据完整性;选择正确的编码(如ASCII、GBK)。
- 坑3:程序卡死(无限阻塞)
原因:同步读取时未设置ReadTimeout,或硬件未响应;UI项目中同步操作串口未切换线程。
解决方案:设置合理的ReadTimeout/WriteTimeout;优先使用异步通信;UI项目中通过Invoke切换线程。
- 坑4:DataReceived事件不触发
原因:串口未打开;未绑定DataReceived事件;数据未到达或数据量过小;Handshake参数设置错误。
解决方案:检查串口是否打开;确认事件已绑定;检查硬件是否正常发送数据;将Handshake设置为None。
- 坑5:资源泄漏(串口无法再次打开)
原因:未调用Close()方法关闭串口;异常时未释放资源;SerialPort实例被多次创建。
解决方案:在finally块中关闭串口;使用using语句包裹SerialPort实例;采用单例模式管理SerialPort。
2. 最佳实践
-
单例管理SerialPort:避免频繁创建/关闭串口,一个串口设备对应一个SerialPort实例,减少资源占用与冲突。
-
使用using语句:自动释放串口资源,避免忘记关闭串口导致的资源泄漏:
csharp
// 使用using语句,自动关闭串口
using (SerialPort sp = new SerialPort("COM1", 9600))
{
sp.Open();
sp.Write("GET_DATA\r\n");
string data = sp.ReadLine();
Console.WriteLine($"接收数据:{data}");
// 无需手动Close(),using语句会自动释放
}
-
参数校验与日志记录:通信前校验串口参数、硬件连接状态;记录串口操作日志(发送/接收数据、异常信息),便于问题排查。
-
避免频繁读写:高频率通信时,批量发送/接收数据,减少读写次数,提升通信效率。
-
适配不同系统:跨平台项目(如.NET Core)中,注意串口名称格式(Windows:COM1,Linux:/dev/ttyS0),可通过判断系统类型动态设置。
-
定期检查串口状态:长时间运行的项目,定期检查串口是否正常打开,若异常则自动重新连接,提升稳定性。
七、实战案例:C# + SerialPort 实现串口调试助手
结合前文知识点,实现一个简单的串口调试助手,支持串口选择、参数配置、数据发送/接收、日志记录,可直接复用,贴合实际开发场景。
1. 项目结构(WinForm示例)
text
SerialPortDebugger/
├─ Form1.cs(主界面)
│ ├─ 串口选择下拉框(cboPort)
│ ├─ 波特率下拉框(cboBaudRate)
│ ├─ 数据位/停止位/校验位下拉框
│ ├─ 打开/关闭串口按钮(btnOpen/btnClose)
│ ├─ 发送数据文本框(txtSend)
│ ├─ 发送按钮(btnSend)
│ └─ 接收日志文本框(txtReceive)
└─ Program.cs
2. 核心代码(Form1.cs)
csharp
using System;
using System.IO.Ports;
using System.Text;
using System.Windows.Forms;
namespace SerialPortDebugger
{
public partial class Form1 : Form
{
private SerialPort _serialPort;
private StringBuilder _receiveBuffer = new StringBuilder();
public Form1()
{
InitializeComponent();
// 初始化串口参数
InitSerialPortParams();
}
/// <summary>
/// 初始化串口参数(下拉框赋值)
/// </summary>
private void InitSerialPortParams()
{
// 加载可用串口
string[] ports = SerialPort.GetPortNames();
cboPort.Items.AddRange(ports);
if (ports.Length > 0)
{
cboPort.SelectedIndex = 0;
}
// 加载波特率(常用值)
cboBaudRate.Items.AddRange(new object[] { 9600, 19200, 38400, 115200 });
cboBaudRate.SelectedItem = 9600;
// 加载数据位、停止位、校验位
cboDataBits.Items.AddRange(new object[] { 7, 8 });
cboDataBits.SelectedItem = 8;
cboStopBits.Items.AddRange(new object[] { StopBits.One, StopBits.Two, StopBits.None });
cboStopBits.SelectedItem = StopBits.One;
cboParity.Items.AddRange(new object[] { Parity.None, Parity.Odd, Parity.Even });
cboParity.SelectedItem = Parity.None;
// 初始化SerialPort
_serialPort = new SerialPort();
_serialPort.DataReceived += SerialPort_DataReceived;
}
/// <summary>
/// 打开串口按钮点击事件
/// </summary>
private void btnOpen_Click(object sender, EventArgs e)
{
try
{
if (string.IsNullOrEmpty(cboPort.Text))
{
MessageBox.Show("请选择串口!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
// 配置串口参数
_serialPort.PortName = cboPort.Text;
_serialPort.BaudRate = int.Parse(cboBaudRate.Text);
_serialPort.DataBits = int.Parse(cboDataBits.Text);
_serialPort.StopBits = (StopBits)cboStopBits.SelectedItem;
_serialPort.Parity = (Parity)cboParity.SelectedItem;
_serialPort.ReadTimeout = 5000;
_serialPort.WriteTimeout = 5000;
_serialPort.Encoding = Encoding.ASCII;
// 打开串口
if (!_serialPort.IsOpen)
{
_serialPort.Open();
btnOpen.Enabled = false;
btnClose.Enabled = true;
btnSend.Enabled = true;
Log($"✅ 串口{cboPort.Text}打开成功,参数:{cboBaudRate.Text},{cboDataBits.Text},{cboStopBits.Text},{cboParity.Text}");
}
}
catch (Exception ex)
{
MessageBox.Show($"打开串口失败:{ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>
/// 关闭串口按钮点击事件
/// </summary>
private void btnClose_Click(object sender, EventArgs e)
{
try
{
if (_serialPort.IsOpen)
{
_serialPort.Close();
btnOpen.Enabled = true;
btnClose.Enabled = false;
btnSend.Enabled = false;
Log($"🔒 串口{cboPort.Text}已关闭");
}
}
catch (Exception ex)
{
MessageBox.Show($"关闭串口失败:{ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>
/// 发送数据按钮点击事件
/// </summary>
private void btnSend_Click(object sender, EventArgs e)
{
try
{
if (string.IsNullOrEmpty(txtSend.Text))
{
MessageBox.Show("请输入发送数据!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
if (!_serialPort.IsOpen)
{
MessageBox.Show("串口未打开,请先打开串口!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
// 发送数据(添加换行符)
string sendData = txtSend.Text + "\r\n";
_serialPort.Write(sendData);
Log($"📤 发送:{sendData.Trim()}");
txtSend.Clear();
}
catch (Exception ex)
{
MessageBox.Show($"发送数据失败:{ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
/// <summary>
/// 数据接收事件
/// </summary>
private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
try
{
string tempData = _serialPort.ReadExisting();
_receiveBuffer.Append(tempData);
// 按换行符分割完整数据
if (_receiveBuffer.ToString().Contains("\r\n"))
{
string[] completeDatas = _receiveBuffer.ToString().Split(new[] { "\r\n" }, StringSplitOptions.RemoveEmptyEntries);
foreach (var data in completeDatas)
{
Log($"📥 接收:{data}");
}
_receiveBuffer.Clear();
}
}
catch (Exception ex)
{
Log($"❌ 接收失败:{ex.Message}");
_receiveBuffer.Clear();
}
}
/// <summary>
/// 日志记录(线程安全)
/// </summary>
private void Log(string message)
{
if (txtReceive.InvokeRequired)
{
txtReceive.Invoke(new Action(() =>
{
txtReceive.AppendText($"[{DateTime.Now:HH:mm:ss}] {message}\r\n");
// 滚动到最后一行
txtReceive.ScrollToCaret();
}));
}
else
{
txtReceive.AppendText($"[{DateTime.Now:HH:mm:ss}] {message}\r\n");
txtReceive.ScrollToCaret();
}
}
/// <summary>
/// 窗体关闭事件,释放资源
/// </summary>
private void Form1_FormClosing(object sender, FormClosingEventArgs e)
{
if (_serialPort.IsOpen)
{
_serialPort.Close();
}
_serialPort.DataReceived -= SerialPort_DataReceived;
_serialPort.Dispose();
}
}
}
3. 运行效果
-
启动程序,自动加载系统可用串口,选择串口并配置参数(波特率、数据位等)。
-
点击"打开串口",提示打开成功,日志框显示相关信息。
-
在发送文本框输入数据(如"GET_DATA"),点击"发送",日志框显示发送记录。
-
当串口接收到硬件发送的数据时,日志框自动显示接收记录,支持多帧数据自动拼接。
-
点击"关闭串口",释放资源,日志框显示关闭记录。
八、总结
System.IO.Ports.SerialPort作为C#内置的串口通信类库,其核心价值是"简单易用、稳定可靠、无依赖",是C#开发者进行硬件串口通信的首选工具,广泛应用于工业控制、物联网、嵌入式开发等场景。
掌握SerialPort的关键是:理解串口通信的底层原理与核心参数,熟练掌握同步/异步通信方式,解决数据完整性、线程安全、异常处理等问题,结合企业级最佳实践,提升通信稳定性与兼容性。
本文从基础配置到进阶优化,从避坑指南到实战案例,覆盖了SerialPort的全知识点,无论是简单的串口调试,还是复杂的多串口并发通信,都能找到对应的解决方案。
扩展建议:深入学习串口通信协议(如Modbus、RS232/RS485),结合SerialPort实现工业设备的完整通信流程;探索跨平台场景下的串口通信(如.NET Core在Linux中的应用);封装通用的串口通信工具类,实现多项目复用。