总目录
前言
在工业控制、物联网、嵌入式开发等领域,串口通信(Serial Port Communication)是连接串行设备(如条码扫描器、GPS接收器等)与计算机的重要手段。C# 提供了内置的 SerialPort
类,简化了串口开发的流程。本文将详细介绍如何在C#中使用SerialPort
类进行串口通信。
一、什么是 SerialPort?
1. 定义
System.IO.Ports.SerialPort
类(简称 SerialPort
)是 .NET 框架中用于串口通信的核心类,它提供了对串行端口的访问,允许我们进行数据的读写、配置串口参数以及处理事件等操作。
2. 串行通信简介
串口通信(Serial Communication)是通过单条数据线按顺序传输数据的通信方式,具有接线简单、成本低廉的特点。在工业控制、物联网设备、传感器等领域广泛应用,波特率范围常见为 9600 9600 9600 到 115200 115200 115200。
- 在工业自动化、嵌入式系统开发等场景中,串口通信(Serial Communication)仍是硬件交互的核心技术之一。
- 串行通信是一种数据按位顺序传输的通信方式,常用于连接各种串行设备。
- C# 通过 System.IO.Ports.SerialPort 类为开发者提供了简洁高效的串口操作接口,支持参数配置、数据读写及事件处理等功能。
3. SerialPort
核心功能
- 端口配置参数:波特率、数据位、校验位、停止位等。
- 数据读写操作:同步或异步发送/接收数据。
- 事件驱动通信:通过
DataReceived
事件实时响应数据到达。
4. 典型应用场景
- 工业设备通信
- 传感器数据采集
- 嵌入式系统交互
二、常用的属性和方法
1. 常用属性
属性名 | 类型/说明 |
---|---|
PortName | string 串口名称(如 COM1 、COM3 )。 |
BaudRate | int 波特率,表示数据传输速率。常用的有 9600 、19200 、38400 、57600 以及115200 等 |
Parity | Parity 奇偶校验类型如 None 无奇偶校验、Even 偶检验、Odd 奇检验。 |
DataBits | int 数据位数。常见数据位为8位,如需要特别设置,还可设置位5位,6位,7位 |
StopBits | StopBits 停止位数如 None 不使用停止位One 1个停止位、Two 2个停止位、OnePointFive 1.5个停止位)。 |
Handshake | Handshake 握手协议(如 None 、RequestToSend 、XOnXOff )。 |
Encoding | Encoding 数据编码方式(如 Encoding.UTF8 )。 |
ReadTimeout | int 读取操作超时时间(毫秒),默认为 InfiniteTimeout 。 |
WriteTimeout | int 写入操作超时时间(毫秒),默认为 InfiniteTimeout 。 |
IsOpen | bool 表示串口是否已打开。 |
NewLine | string 定义行结束符(如 "\r\n" ),用于 ReadLine() 和 WriteLine() 。 |
DtrEnable | bool 控制数据终端就绪(DTR)信号状态。 |
RtsEnable | bool 控制请求发送(RTS)信号状态。 |
ReceivedBytesThreshold | int 触发 DataReceived 事件的最小字节数。 |
2. 常用方法
方法名 | 说明 |
---|---|
Open() | 打开串口。 |
Close() | 关闭已打开的串口。 |
Read(byte[] buffer, int offset, int count) | 从串口读取指定字节数到缓冲区。 |
ReadByte() | 读取单个字节(返回 int ,范围 0-255 ,失败返回 -1 )。 |
ReadLine() | 读取一行数据,直到遇到 NewLine 定义的结束符。 |
ReadExisting() | 读取接收缓冲区中所有可用数据(不阻塞)。 |
Write(string text) | 写入字符串到串口(自动按 Encoding 编码)。 |
Write(byte[] buffer, int offset, int count) | 写入字节数组到串口。 |
DiscardInBuffer() | 清空输入缓冲区中的数据。 |
DiscardOutBuffer() | 清空输出缓冲区中的数据。 |
3. 常用事件
事件名 | 说明 |
---|---|
DataReceived | 当接收到数据且字节数达到 ReceivedBytesThreshold 时触发。 |
ErrorReceived | 当串口发生错误(如奇偶校验错误)时触发。 |
4. 其他重要属性(扩展)
属性名 | 类型/说明 |
---|---|
BytesToRead | int 接收缓冲区中已接收的字节数。 |
BytesToWrite | int 输出缓冲区中待发送的字节数。 |
ReadBufferSize | int 设置输入缓冲区大小(默认 4096 )。 |
WriteBufferSize | int 设置输出缓冲区大小(默认 2048 )。 |
三、SerialPort 基础
1. 添加命名空间引用
在使用SerialPort
类之前,需要先添加对System.IO.Ports
命名空间的引用:
using System.IO.Ports;
2. 初始化与参数配置
串口通信的核心是正确配置参数,确保与硬件设备匹配,否则通信失败。
1)方式1:构造函数
using System.IO.Ports;// 创建对象并指定端口名称(如COM3)
SerialPort serialPort = new SerialPort("COM3", 9600, Parity.None, 8, StopBits.One);
2)方式2:属性配置
// 可选:手动设置参数(适用于动态配置)
SerialPort serialPort = new SerialPort();
serialPort.PortName = "COM3"; // 端口号
serialPort.BaudRate = 9600; // 波特率
serialPort.Parity = Parity.None; // 校验位
serialPort.DataBits = 8; // 数据位
serialPort.StopBits = StopBits.One; // 停止位
3. 打开与关闭串口
1)打开串口
在进行数据传输之前,需要打开串行端口:
serialPort.Open(); // 打开串口
优化一下:加上try catch
做异常处理
try
{if (!serialPort.IsOpen){serialPort.Open();Console.WriteLine("端口已打开");}
}
catch (Exception ex)
{Console.WriteLine($"打开失败:{ex.Message}");
}
2)关闭串口
数据传输完成后,应及时关闭串行端口。务必在程序退出前调用Close
serialPort.Close(); // 关闭端口
优化一下:加上try catch
做异常处理
try
{if (serialPort.IsOpen){// 关闭端口serialPort.Close();Console.WriteLine("端口已关闭");// 销毁serialPort对象serialPort.Dispose();}
}
catch (Exception ex)
{Console.WriteLine($"关闭失败:{ex.Message}");
}
4. 读取和写入数据
1)写入数据
▶ 写入字符串数据
- 使用
Write()
方法:将指定的字符串写入串行端口。 - 使用
WriteLine
方法:将指定的字符串和SerialPort.NewLine
值写入串行端口。
serialPort.Write("Hello, Serial!");// 自动添加换行符:发送字符串并添加换行符(WriteLine)
serialPort.WriteLine("Hello, Serial Port!");
▶ 写入字节流(字节数组)数据
// 发送字节数组
byte[] data = Encoding.UTF8.GetBytes("Test Data");
serialPort.Write(data, 0, data.Length);byte[] data = { 0x01, 0xFF, 0xAB }; // 十六进制数据
serialPort.Write(data, 0, data.Length); // 发送字节
2)读取数据
可以通过Read()
、ReadLine()
等方法从串口读取数据。
// 读取指定字节数
byte[] buffer = new byte[1024];
int bytesRead = serialPort.Read(buffer, 0, buffer.Length);
string received = Encoding.UTF8.GetString(buffer, 0, bytesRead);// 读取一行数据(依赖 NewLine 配置)
string line = serialPort.ReadLine();
5. 处理数据接收事件
通过 DataReceived
事件实时接收串口数据。当有数据到达时,DataReceived
事件会被触发。可以通过注册该事件来实时处理接收到的数据:
1)使用方式1
// 注册 DataReceived 事件
serialPort.DataReceived += new SerialDataReceivedEventHandler(DataReceivedHandler);private void DataReceivedHandler(object sender, SerialDataReceivedEventArgs e)
{SerialPort sp = (SerialPort)sender;string received = sp.ReadExisting(); // 读取所有可用数据Console.WriteLine($"Received: {received}");
}
2)使用方式2
serialPort.DataReceived += SerialPort_DataReceived;
private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{//首先实例化一个字节数组// 字节数组的大小:通过BytesToRead 属性,获取得到缓冲区中数据的字节数byte[] buffer = new byte[serialPort.BytesToRead];//再通过 Read() 方法 读取缓存区内全部数据,并将数据写入字节数组中serialPort.Read(buffer, 0, buffer.Length);// 解码string received = Encoding.ASCII.GetString(buffer); Console.WriteLine(received);
}
3)使用方式3
serialPort.DataReceived += (sender, e) =>
{if (e.EventType == SerialData.Chars){byte[] buffer = new byte[serialPort.BytesToRead];serialPort.Read(buffer, 0, buffer.Length);string data = Encoding.ASCII.GetString(buffer);Console.WriteLine($"收到数据:{data}");}
};
以上三种方式,基本原理一致,只是部分的读取数据方式和事件定义的方式稍有不同而已。
6. 完整示例
示例1:简单示例
using System;
using System.IO.Ports;
using System.Windows.Forms;public class SerialPortDemo
{//配置并创建SerialPort实例private SerialPort sp = new SerialPort("COM1", 9600, Parity.None, 8, StopBits.One);public void Start(){try{// 注册数据接收事件sp.DataReceived += DataReceivedHandler;// 打开串口sp.Open();//写入数据sp.WriteLine("Start Communication");}catch (Exception ex){MessageBox.Show($"初始化失败:{ex.Message}");}}// 接收数据private void DataReceivedHandler(object sender, SerialDataReceivedEventArgs e){// 读取数据string data = sp.ReadExisting();Console.WriteLine($"实时数据:{data}");}// 关闭端口public void Stop() => sp.Close();
}
示例2:WinForm 示例
以下是一个完整的示例,展示如何在C#中使用SerialPort
类进行串口通信。
using System;
using System.IO.Ports;
using System.Windows.Forms;namespace SerialPortExample
{public partial class MainForm : Form{private SerialPort serialPort;public MainForm(){InitializeComponent();InitializeSerialPort();}private void InitializeSerialPort(){serialPort = new SerialPort();serialPort.PortName = "COM1";serialPort.BaudRate = 9600;serialPort.Parity = Parity.None;serialPort.DataBits = 8;serialPort.StopBits = StopBits.One;serialPort.Handshake = Handshake.None;serialPort.DataReceived += new SerialDataReceivedEventHandler(SerialPort_DataReceived);}private void btnOpen_Click(object sender, EventArgs e){try{if (!serialPort.IsOpen){serialPort.Open();MessageBox.Show("串口已打开");}}catch (Exception ex){MessageBox.Show("打开串口时出错: " + ex.Message);}}private void btnSend_Click(object sender, EventArgs e){if (serialPort.IsOpen){string message = txtSend.Text;serialPort.WriteLine(message);txtReceive.AppendText("发送: " + message + Environment.NewLine);}else{MessageBox.Show("请先打开串口");}}private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e){SerialPort sp = (SerialPort)sender;string indata = sp.ReadExisting();this.Invoke(new Action(() =>{txtReceive.AppendText("接收: " + indata);}));}private void btnClose_Click(object sender, EventArgs e){if (serialPort.IsOpen){serialPort.Close();MessageBox.Show("串口已关闭");}}}
}
示例3:扫码枪数据接收
SerialPort port = new("COM3", 115200, Parity.None, 8, StopBits.One);
port.DataReceived += (s, e) =>
{byte[] barcode = new byte[port.BytesToRead];port.Read(barcode, 0, barcode.Length);// 解析扫码枪数据(通常以回车结尾)if (barcode.Last() == 0x0D) {string code = Encoding.ASCII.GetString(barcode);ProcessBarcode(code);}
};
四、SerialPort 进阶
1. 高级参数配置
1)配置超时时间
防止长时间阻塞(单位:毫秒)。
// 设置读取超时为 1000ms
serialPort.ReadTimeout = 1000;
// 设置写入超时
serialPort.WriteTimeout = 500;
2)缓冲区设置
ReceivedBytesThreshold
:指定触发 DataReceived
事件的字节阈值,即触发 DataReceived
事件的最小字节数。
serialPort.ReceivedBytesThreshold = 2; //默认值 为 1
3)其余参数配置
sp.Encoding = Encoding.ASCII; // 编码格式
sp.NewLine = "\r\n"; // 换行符定义
2. 获取所有可用 COM 端口
可以通过SerialPort.GetPortNames()
方法获取系统中所有可用的串行端口名称:
// 获取所有可用 COM 端口
string[] ports = SerialPort.GetPortNames();
foreach (var port in ports)
{Console.WriteLine(port);
}
3. ErrorReceived事件
当串行通信中发生错误(如帧错误/缓冲区溢出)时,ErrorReceived
事件会被触发:
serialPort.ErrorReceived += new SerialErrorReceivedEventHandler(serialPort_ErrorReceived);
private static void serialPort_ErrorReceived(object sender, SerialErrorReceivedEventArgs e)
{if ((e.EventType & SerialError.Frame) == SerialError.Frame){Console.WriteLine("Frame error detected.");}if ((e.EventType & SerialError.Overrun) == SerialError.Overrun){Console.WriteLine("Overrun error detected.");}if ((e.EventType & SerialError.RXParity) == SerialError.RXParity){Console.WriteLine("Parity error detected.");}
}
sp.ErrorReceived += (sender, e) =>
{Console.WriteLine($"错误类型:{e.EventType}");
};
4. 异常处理
使用try-catch
块捕获和处理可能的异常,确保程序的稳定性:
try
{serialPort.Open();serialPort.WriteLine("Hello, Serial Port!");serialPort.Close();
}
catch (Exception ex)
{Console.WriteLine("Error: " + ex.Message);
}
// 常见异常类型
try
{// 串口操作代码
}
catch (UnauthorizedAccessException ex)
{Console.WriteLine("端口访问被拒绝");
}
catch (TimeoutException ex)
{Console.WriteLine("操作超时");
}
catch (IOException ex)
{Console.WriteLine("I/O错误");
}
5. 跨线程处理
为了能够实时响应串口接收到的数据,可以使用DataReceived
事件。需要注意的是,该事件在单独的线程中触发,若要更新UI控件,涉及到跨线程,需要使用Invoke
方法。
private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{SerialPort sp = (SerialPort)sender;string received = sp.ReadExisting();// 数据接收后,通过委托更新UI(假设在 WinForms 中更新TextBox)this.Invoke(new Action(() =>{textBox.Text += received ;}));
}
6. 多线程处理
推荐使用生产者-消费者模式:
BlockingCollection<string> dataQueue = new BlockingCollection<string>();// 生产者线程
void DataReceivedHandler(...)
{dataQueue.Add(receivedData);
}// 消费者线程
Task.Run(() =>
{foreach (var data in dataQueue.GetConsumingEnumerable()){// 处理数据}
});
五、常见问题与注意事项
1. 注意事项
- 参数一致性:确保双方设备的波特率、数据位、停止位、校验位一致。
- 权限问题:在访问串口时,需要确保程序有相应的权限,否则可能会抛出异常。
- 线程安全:在
DataReceived
事件中处理数据时,由于它是在单独的线程中触发的,若要更新UI控件,必须使用Invoke
方法来确保线程安全 - 异常处理:使用
try-catch
处理Open()
、Read()
、Write()
可能抛出的异常。 - 缓冲区管理:通过
DiscardInBuffer
和DiscardOutBuffer
清空缓冲区,避免数据残留。- 避免重复打开同一端口,关闭前调用
sp.DiscardInBuffer()
清空缓存。
- 避免重复打开同一端口,关闭前调用
- 硬件流控制
在高速传输场景中启用Handshake.RequestToSend
,避免数据丢失。
2. 常见问题
- 端口无法打开
- 检查设备是否占用(如其他程序已连接)
- 确认串口号是否正确(虚拟串口可能动态变化)
- 异常处理:
try...catch(IOException)
捕获端口不存在错误
- 数据接收不完整
- 使用
BytesToRead
确保读取全部缓存数据 - 添加延迟(如
Thread.Sleep(50)
)等待完整帧 - 硬件延迟:部分设备发送数据有间隔,需调整时序
- 使用
- 数据乱码
- 发送/接收端波特率必须一致,否则数据乱码。
- 字符编码问题
- 若接收中文字符乱码,需设置:
serialPort.Encoding = Encoding.GetEncoding("GB2312")
。- 或
serialPort.Encoding = Encoding.UTF8;
六、扩展
1. 跨平台方案(RJCP.SerialPortStream)
对于需要跨平台(Linux/macOS)的场景,推荐使用 RJCP.SerialPortStream 库(通过 NuGet 安装 RJCP.SerialPortStream
):
using RJCP.IO.Ports;SerialPortStream serialPort = new SerialPortStream("COM3");
serialPort.BaudRate = 9600;
serialPort.Open();// 事件处理与读写方式与 SerialPort 类似
serialPort.DataReceived += (sender, e) =>
{byte[] data = new byte[serialPort.BytesToRead];serialPort.Read(data, 0, data.Length);Console.WriteLine(Encoding.UTF8.GetString(data));
};
2. 编码
使用串口通信的时候,会涉及到数据的编码格式,如果我们使用ASCII编码格式,但是接收的时候采用的是其他的编码格式,可能就会导致数据解析错误,因此在一些特定场景下我们需要规定好发送和接收数据的编码格式,这个时候就需要用到System.Text.Encoding
类中的编码解码的功能。
详见:C# System.Text.Encoding 使用详解
3. 清空缓冲区的方法
关于 DiscardInBuffer / DiscardOutBuffer
的使用,详见:C# SerialPort 类中清空缓存区的方法。
4. Handshake 设置
常用握手协议:
Handshake.None
:无握手。Handshake.RequestToSend
:RTS/CTS(请求发送/清除发送)。Handshake.XOnXOff
:软件流控制(XON/XOFF 字符)。
serialPort.Handshake = Handshake.RequestToSend;
详见:C# SerialPort 类中 Handshake 属性的作用
结语
回到目录页:C# 上位机知识汇总
希望以上内容可以帮助到大家,如文中有不对之处,还请批评指正。
参考资料:
- 官方文档:SerialPort Class