串口通信是上位机开发中的核心技能,广泛应用于工业控制、物联网和自动化测试等领域。C# 通过 System.IO.Ports.SerialPort 类提供了完整的串口通信支持。本教程从配置参数、数据发送、数据接收、异步操作和错误处理五个方面,系统讲解 SerialPort 的发送与接收实现。核心逻辑可以概括为三步:配置参数 → 发送 Write → 事件接收 DataReceived。
1. 引言
在工业控制、嵌入式开发、仪器仪表等场景中,串口通信依然是最基础、最常用的通信方式之一。C# 通过 System.IO.Ports 命名空间下的 SerialPort 类,为开发者提供了简洁而强大的串口操作接口。本文将系统讲解 C# 中 SerialPort 的发送与接收机制,从基础配置到高级应用,帮助你快速掌握串口编程的核心要点。
2. 环境准备与基础配置
2.1 引入命名空间
使用 SerialPort 前,需要引入 System.IO.Ports 命名空间:
using System.IO.Ports;
2.2 创建并配置 SerialPort 实例
SerialPort serialPort = new SerialPort(); // 基本配置 serialPort.PortName = "COM3"; // 串口号 serialPort.BaudRate = 9600; // 波特率 serialPort.DataBits = 8; // 数据位 serialPort.Parity = Parity.None; // 校验位 serialPort.StopBits = StopBits.One; // 停止位 // 可选配置 serialPort.ReadTimeout = 500; // 读取超时(毫秒) serialPort.WriteTimeout = 500; // 写入超时(毫秒) serialPort.NewLine = "\r\n"; // 换行符(用于 ReadLine)
2.3 打开与关闭串口
// 打开串口
try
{
serialPort.Open();
Console.WriteLine("串口已打开");
}
catch (Exception ex)
{
Console.WriteLine($"打开失败: {ex.Message}");
}
// 关闭串口
serialPort.Close();
3. 数据发送
3.1 发送字符串
// 直接发送字符串
serialPort.Write("Hello, Serial!");
// 发送带换行符的字符串
serialPort.WriteLine("AT+CSQ");
3.2 发送字节数组
// 构造要发送的字节数据
byte[] data = new byte[] { 0x01, 0x02, 0x03, 0xFF, 0x00 };
// 发送字节数组
serialPort.Write(data, 0, data.Length);
3.3 发送十六进制字符串
/// <summary>
/// 将十六进制字符串转换为字节数组并发送
/// </summary>
public static void SendHexString(SerialPort port, string hexString)
{
// 去除空格
hexString = hexString.Replace(" ", "");
// 将十六进制字符串转换为字节数组
byte[] data = new byte[hexString.Length / 2];
for (int i = 0; i < data.Length; i++)
{
data[i] = Convert.ToByte(hexString.Substring(i * 2, 2), 16);
}
// 发送数据
port.Write(data, 0, data.Length);
}
// 使用示例
SendHexString(serialPort, "01 03 00 00 00 0A C5 CD");
4. 数据接收
4.1 使用 DataReceived 事件(推荐)
DataReceived 事件是串口接收最常用的方式,它在数据到达时异步触发:
// 订阅事件
serialPort.DataReceived += SerialPort_DataReceived;
// 事件处理方法
private static void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
SerialPort sp = (SerialPort)sender;
// 读取当前缓冲区中所有可用数据
int bytesToRead = sp.BytesToRead;
byte[] buffer = new byte[bytesToRead];
sp.Read(buffer, 0, bytesToRead);
// 处理接收到的数据
string receivedText = System.Text.Encoding.UTF8.GetString(buffer);
Console.WriteLine($"收到数据: {receivedText}");
// 或者以十六进制显示
string hexText = BitConverter.ToString(buffer);
Console.WriteLine($"收到十六进制: {hexText}");
}
4.2 同步读取
// 读取一个字节 int byteValue = serialPort.ReadByte(); // 读取到指定缓冲区 byte[] buffer = new byte[1024]; int bytesRead = serialPort.Read(buffer, 0, buffer.Length); // 读取一行(直到遇到 NewLine) string line = serialPort.ReadLine();
4.3 完整接收示例:处理不定长数据
private static void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
SerialPort sp = (SerialPort)sender;
// 防止多线程同时访问
lock (sp)
{
// 读取所有可用数据
int bytesToRead = sp.BytesToRead;
byte[] buffer = new byte[bytesToRead];
int bytesRead = sp.Read(buffer, 0, bytesToRead);
if (bytesRead > 0)
{
// 将数据追加到接收缓冲区
receiveBuffer.AddRange(buffer);
// 尝试解析完整的数据帧
ProcessReceivedData();
}
}
}
private static List<byte> receiveBuffer = new List<byte>();
private static void ProcessReceivedData()
{
// 假设协议帧格式: 帧头(0xAA) + 长度(1字节) + 数据 + 校验(1字节)
while (receiveBuffer.Count >= 3)
{
// 查找帧头
if (receiveBuffer[0] != 0xAA)
{
receiveBuffer.RemoveAt(0);
continue;
}
int dataLength = receiveBuffer[1];
int totalLength = 2 + dataLength + 1; // 帧头 + 长度 + 数据 + 校验
if (receiveBuffer.Count < totalLength)
{
break; // 数据不完整,等待更多数据
}
// 提取完整帧
byte[] frame = receiveBuffer.GetRange(0, totalLength).ToArray();
receiveBuffer.RemoveRange(0, totalLength);
// 校验并处理帧
if (VerifyChecksum(frame))
{
Console.WriteLine($"收到完整帧: {BitConverter.ToString(frame)}");
}
}
}
private static bool VerifyChecksum(byte[] frame)
{
// 简单的校验和验证
byte checksum = 0;
for (int i = 0; i < frame.Length - 1; i++)
{
checksum ^= frame[i];
}
return checksum == frame[frame.Length - 1];
}
5. 完整示例:串口调试助手
下面是一个完整的控制台串口通信示例,包含发送和接收功能:
using System;
using System.IO.Ports;
using System.Text;
using System.Threading;
class SerialPortDemo
{
private static SerialPort serialPort;
private static bool isRunning = true;
static void Main(string[] args)
{
// 初始化串口
serialPort = new SerialPort("COM3", 9600, Parity.None, 8, StopBits.One);
serialPort.DataReceived += SerialPort_DataReceived;
try
{
serialPort.Open();
Console.WriteLine("串口已打开,输入 'exit' 退出程序");
Console.WriteLine("输入 'send:内容' 发送字符串");
Console.WriteLine("输入 'hex:01 02 03' 发送十六进制数据");
// 主线程处理用户输入
while (isRunning)
{
string input = Console.ReadLine();
if (input == "exit")
{
isRunning = false;
}
else if (input.StartsWith("send:"))
{
string data = input.Substring(5);
serialPort.Write(data);
Console.WriteLine($"已发送: {data}");
}
else if (input.StartsWith("hex:"))
{
string hexData = input.Substring(4);
SendHexString(hexData);
}
}
}
catch (Exception ex)
{
Console.WriteLine($"错误: {ex.Message}");
}
finally
{
if (serialPort.IsOpen)
{
serialPort.Close();
}
Console.WriteLine("串口已关闭");
}
}
private static void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
SerialPort sp = (SerialPort)sender;
int bytesToRead = sp.BytesToRead;
byte[] buffer = new byte[bytesToRead];
sp.Read(buffer, 0, bytesToRead);
string text = Encoding.UTF8.GetString(buffer);
string hex = BitConverter.ToString(buffer);
Console.WriteLine($"[接收] 文本: {text}");
Console.WriteLine($"[接收] 十六进制: {hex}");
}
private static void SendHexString(string hexString)
{
hexString = hexString.Replace(" ", "");
byte[] data = new byte[hexString.Length / 2];
for (int i = 0; i < data.Length; i++)
{
data[i] = Convert.ToByte(hexString.Substring(i * 2, 2), 16);
}
serialPort.Write(data, 0, data.Length);
Console.WriteLine($"已发送十六进制: {BitConverter.ToString(data)}");
}
}
6. 常见问题与注意事项
6.1 串口被占用
// 检查串口是否可用
string[] ports = SerialPort.GetPortNames();
if (Array.IndexOf(ports, "COM3") == -1)
{
Console.WriteLine("COM3 不可用");
return;
}
6.2 跨线程访问 UI
在 WinForms 或 WPF 中,DataReceived 事件在后台线程触发,不能直接操作 UI 控件:
// WinForms 示例
private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
{
if (this.InvokeRequired)
{
this.Invoke(new Action(() => SerialPort_DataReceived(sender, e)));
return;
}
// 现在可以安全地操作 UI 控件
textBox1.AppendText("收到数据");
}
6.3 数据粘包与拆包
串口通信中常见的数据粘包问题,解决方案:
- 使用固定帧头帧尾
- 使用长度字段
- 使用超时机制
// 使用超时机制处理粘包
private static void ReadWithTimeout()
{
DateTime startTime = DateTime.Now;
List<byte> dataBuffer = new List<byte>();
while ((DateTime.Now - startTime).TotalMilliseconds < 100)
{
if (serialPort.BytesToRead > 0)
{
int b = serialPort.ReadByte();
dataBuffer.Add((byte)b);
startTime = DateTime.Now; // 重置超时
}
Thread.Sleep(10);
}
if (dataBuffer.Count > 0)
{
Console.WriteLine($"接收完成: {BitConverter.ToString(dataBuffer.ToArray())}");
}
}
7. 总结
本文详细介绍了 C# 中 SerialPort 的发送与接收方法:
- 发送:支持字符串、字节数组、十六进制字符串等多种格式
- 接收:推荐使用
DataReceived事件实现异步接收 - 协议处理:通过帧头、长度、校验等机制确保数据完整性
- 注意事项:跨线程访问、数据粘包、串口占用等常见问题
掌握这些核心要点后,你就可以在 C# 项目中灵活实现串口通信功能。建议在实际开发中,根据具体的通信协议设计合适的收发逻辑,并做好异常处理和资源释放。










