SerialPort类是.NET Framework提供的专门用于串口通信的类,它封装了Windows API的串口操作,让开发者能够方便地进行串口数据的收发。本文将全面介绍如何使用SerialPort类实现可靠的串口通信。
![图片[1]_C# SerialPort类实现串口通信实战指南_知途无界](https://zhituwujie.com/wp-content/uploads/2026/01/d2b5ca33bd20260118103610.png)
目录
基础概念与环境准备
串口通信基本概念
- 波特率(BaudRate): 数据传输速率,常见值:9600, 19200, 38400, 57600, 115200
- 数据位(DataBits): 每个字节的数据位数,通常7或8位
- 停止位(StopBits): 标识数据帧结束,None/One/Two
- 校验位(Parity): 奇偶校验,None/Odd/Even/Mark/Space
- 握手协议(Handshake): 流控制,None/XOnXOff/RequestToSend/RequestToSendXOnXOff
环境准备
using System;
using System.IO.Ports;
using System.Text;
using System.Threading;
using System.Windows.Forms; // 如果是WinForms应用
SerialPort类核心属性与方法
主要属性
SerialPort sp = new SerialPort();
// 基本配置属性
sp.PortName = "COM1"; // 串口号
sp.BaudRate = 9600; // 波特率
sp.DataBits = 8; // 数据位
sp.StopBits = StopBits.One; // 停止位
sp.Parity = Parity.None; // 校验位
sp.Handshake = Handshake.None; // 握手协议
// 超时设置
sp.ReadTimeout = 500; // 读超时(毫秒)
sp.WriteTimeout = 500; // 写超时(毫秒)
// 缓冲区设置
sp.ReadBufferSize = 4096; // 读缓冲区大小
sp.WriteBufferSize = 2048; // 写缓冲区大小
// 编码设置
sp.Encoding = Encoding.UTF8; // 数据编码
主要方法
// 打开/关闭串口
sp.Open(); // 打开串口
sp.Close(); // 关闭串口
bool isOpen = sp.IsOpen; // 检查串口是否打开
// 数据读写
int bytesRead = sp.Read(buffer, offset, count); // 同步读取
string data = sp.ReadLine(); // 读取一行
string data = sp.ReadExisting(); // 读取现有数据
int bytesWritten = sp.Write(data); // 写入数据
void sp.WriteLine(string text); // 写入一行
// 异步操作
IAsyncResult result = sp.BaseStream.BeginRead(...); // 异步读取
sp.BaseStream.EndRead(result); // 结束异步读取
基本串口通信实现
1. 简单串口通信类封装
using System;
using System.IO.Ports;
using System.Text;
using System.Threading;
public class SerialPortHelper : IDisposable
{
private SerialPort _serialPort;
private StringBuilder _receiveBuffer;
private bool _disposed = false;
public event EventHandler<string> DataReceived;
public event EventHandler<string> ErrorOccurred;
public SerialPortHelper()
{
_serialPort = new SerialPort();
_receiveBuffer = new StringBuilder();
InitializeSerialPort();
}
private void InitializeSerialPort()
{
// 默认配置
_serialPort.BaudRate = 9600;
_serialPort.DataBits = 8;
_serialPort.StopBits = StopBits.One;
_serialPort.Parity = Parity.None;
_serialPort.Handshake = Handshake.None;
_serialPort.ReadTimeout = 500;
_serialPort.WriteTimeout = 500;
_serialPort.Encoding = Encoding.UTF8;
_serialPort.DtrEnable = true; // 数据终端就绪
_serialPort.RtsEnable = true; // 请求发送
// 数据接收事件
_serialPort.DataReceived += OnDataReceived;
}
public bool Connect(string portName, int baudRate = 9600)
{
try
{
if (_serialPort.IsOpen)
_serialPort.Close();
_serialPort.PortName = portName;
_serialPort.BaudRate = baudRate;
_serialPort.Open();
return _serialPort.IsOpen;
}
catch (Exception ex)
{
OnErrorOccurred($"连接串口失败: {ex.Message}");
return false;
}
}
public void Disconnect()
{
if (_serialPort.IsOpen)
{
_serialPort.Close();
_receiveBuffer.Clear();
}
}
public bool IsConnected => _serialPort.IsOpen;
// 数据接收处理
private void OnDataReceived(object sender, SerialDataReceivedEventArgs e)
{
try
{
// 读取可用数据
string receivedData = _serialPort.ReadExisting();
if (!string.IsNullOrEmpty(receivedData))
{
_receiveBuffer.Append(receivedData);
// 触发数据接收事件
DataReceived?.Invoke(this, receivedData);
// 处理完整数据包(可根据协议自定义)
ProcessCompleteData();
}
}
catch (Exception ex)
{
OnErrorOccurred($"数据接收错误: {ex.Message}");
}
}
private void ProcessCompleteData()
{
// 示例:按换行符分割数据包
string bufferStr = _receiveBuffer.ToString();
string[] lines = bufferStr.Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
if (lines.Length > 0)
{
// 处理完整行(保留最后不完整的部分在缓冲区)
for (int i = 0; i < lines.Length - 1; i++)
{
DataReceived?.Invoke(this, lines[i]);
}
// 更新缓冲区,保留最后一个可能不完整的行
_receiveBuffer.Clear();
if (!bufferStr.EndsWith("\r") && !bufferStr.EndsWith("\n"))
{
_receiveBuffer.Append(lines);
}
}
}
// 数据发送
public void SendData(string data)
{
if (!_serialPort.IsOpen)
{
OnErrorOccurred("串口未打开");
return;
}
try
{
_serialPort.Write(data);
}
catch (Exception ex)
{
OnErrorOccurred($"数据发送失败: {ex.Message}");
}
}
public void SendData(byte[] data)
{
if (!_serialPort.IsOpen)
{
OnErrorOccurred("串口未打开");
return;
}
try
{
_serialPort.Write(data, 0, data.Length);
}
catch (Exception ex)
{
OnErrorOccurred($"数据发送失败: {ex.Message}");
}
}
public void SendDataLine(string data)
{
SendData(data + "\r\n");
}
private void OnErrorOccurred(string errorMessage)
{
ErrorOccurred?.Invoke(this, errorMessage);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
Disconnect();
_serialPort?.Dispose();
}
_disposed = true;
}
}
~SerialPortHelper()
{
Dispose(false);
}
}
2. 基本使用示例
class Program
{
static SerialPortHelper serialHelper;
static void Main(string[] args)
{
serialHelper = new SerialPortHelper();
// 订阅事件
serialHelper.DataReceived += OnDataReceived;
serialHelper.ErrorOccurred += OnErrorOccurred;
Console.WriteLine("可用串口:");
foreach (string port in SerialPort.GetPortNames())
{
Console.WriteLine($" {port}");
}
Console.Write("请输入串口号 (如 COM1): ");
string portName = Console.ReadLine();
Console.Write("请输入波特率 (默认 9600): ");
if (!int.TryParse(Console.ReadLine(), out int baudRate))
baudRate = 9600;
// 连接串口
if (serialHelper.Connect(portName, baudRate))
{
Console.WriteLine("串口连接成功!");
// 发送测试数据
serialHelper.SendDataLine("Hello Serial Port!");
Console.WriteLine("按任意键退出...");
Console.ReadKey();
}
else
{
Console.WriteLine("串口连接失败!");
}
serialHelper.Disconnect();
}
static void OnDataReceived(object sender, string data)
{
Console.WriteLine($"收到数据: {data}");
}
static void OnErrorOccurred(object sender, string error)
{
Console.WriteLine($"错误: {error}");
}
}
数据接收与事件处理
1. 不同数据接收方式对比
方式1: DataReceived事件(推荐)
_serialPort.DataReceived += (sender, e) =>
{
try
{
string data = _serialPort.ReadExisting();
this.Invoke(new Action(() =>
{
// 在UI线程更新界面
textBoxReceive.AppendText($"{DateTime.Now:HH:mm:ss} 接收: {data}\r\n");
}));
}
catch (Exception ex)
{
MessageBox.Show($"接收错误: {ex.Message}");
}
};
方式2: 定时轮询读取
private Timer _readTimer;
private void StartPollingRead()
{
_readTimer = new Timer(_ =>
{
try
{
if (_serialPort.IsOpen && _serialPort.BytesToRead > 0)
{
byte[] buffer = new byte[_serialPort.BytesToRead];
int bytesRead = _serialPort.Read(buffer, 0, buffer.Length);
if (bytesRead > 0)
{
string data = Encoding.UTF8.GetString(buffer, 0, bytesRead);
// 处理接收到的数据
}
}
}
catch (Exception ex)
{
// 处理异常
}
}, null, 0, 50); // 每50ms检查一次
}
方式3: 异步读取
private async Task StartAsyncReading()
{
byte[] buffer = new byte[1024];
try
{
while (_serialPort.IsOpen)
{
int bytesRead = await _serialPort.BaseStream.ReadAsync(buffer, 0, buffer.Length);
if (bytesRead > 0)
{
string data = Encoding.UTF8.GetString(buffer, 0, bytesRead);
// 处理数据
}
}
}
catch (OperationCanceledException)
{
// 正常取消
}
catch (Exception ex)
{
// 处理异常
}
}
2. 二进制数据接收处理
private List<byte> _binaryBuffer = new List<byte>();
private void OnBinaryDataReceived(object sender, SerialDataReceivedEventArgs e)
{
try
{
int bytesToRead = _serialPort.BytesToRead;
byte[] buffer = new byte[bytesToRead];
int bytesRead = _serialPort.Read(buffer, 0, bytesToRead);
if (bytesRead > 0)
{
// 添加到缓冲区
_binaryBuffer.AddRange(buffer.Take(bytesRead));
// 处理完整数据包(假设数据包以特定字节开头和结尾)
ProcessBinaryPackets();
}
}
catch (Exception ex)
{
OnErrorOccurred($"二进制数据接收错误: {ex.Message}");
}
}
private void ProcessBinaryPackets()
{
// 示例:查找数据包边界(假设以0x02开头,0x03结尾)
const byte STX = 0x02; // Start of Text
const byte ETX = 0x03; // End of Text
while (_binaryBuffer.Count >= 2)
{
// 查找起始标记
int startIndex = _binaryBuffer.FindIndex(b => b == STX);
if (startIndex == -1)
{
_binaryBuffer.Clear();
return;
}
// 移除起始标记前的无效数据
if (startIndex > 0)
{
_binaryBuffer.RemoveRange(0, startIndex);
continue;
}
// 查找结束标记
int endIndex = _binaryBuffer.FindIndex(startIndex + 1, b => b == ETX);
if (endIndex == -1)
{
// 没有完整数据包,等待更多数据
return;
}
// 提取完整数据包(包含STX和ETX)
byte[] packet = _binaryBuffer.GetRange(0, endIndex + 1).ToArray();
// 从缓冲区移除已处理的数据
_binaryBuffer.RemoveRange(0, endIndex + 1);
// 处理数据包(去掉STX和ETX)
if (packet.Length > 2)
{
byte[] actualData = new byte[packet.Length - 2];
Array.Copy(packet, 1, actualData, 0, actualData.Length);
OnBinaryPacketReceived(actualData);
}
}
}
private void OnBinaryPacketReceived(byte[] data)
{
// 处理二进制数据包
Console.WriteLine($"收到二进制数据,长度: {data.Length}");
// 可以在这里解析具体协议
if (data.Length >= 4)
{
int value1 = BitConverter.ToInt16(data, 0);
int value2 = BitConverter.ToInt16(data, 2);
Console.WriteLine($"解析值: {value1}, {value2}");
}
}
数据发送与编码处理
1. 字符串数据发送
public void SendText(string text)
{
if (!_serialPort.IsOpen)
throw new InvalidOperationException("串口未打开");
try
{
// 方式1: 直接写入字符串
_serialPort.Write(text);
// 方式2: 写入带换行符的字符串
// _serialPort.WriteLine(text);
// 方式3: 按指定编码写入
// byte[] data = Encoding.UTF8.GetBytes(text);
// _serialPort.Write(data, 0, data.Length);
}
catch (TimeoutException)
{
throw new TimeoutException("数据发送超时");
}
catch (Exception ex)
{
throw new Exception($"发送失败: {ex.Message}", ex);
}
}
2. 二进制数据发送
public void SendBytes(byte[] data)
{
if (!_serialPort.IsOpen)
throw new InvalidOperationException("串口未打开");
try
{
_serialPort.Write(data, 0, data.Length);
}
catch (TimeoutException)
{
throw new TimeoutException("数据发送超时");
}
catch (Exception ex)
{
throw new Exception($"发送失败: {ex.Message}", ex);
}
}
// 发送带协议头尾的二进制数据
public void SendPacket(byte[] data)
{
const byte STX = 0x02;
const byte ETX = 0x03;
const byte CHECKSUM = 0x00; // 简化的校验和
using (MemoryStream ms = new MemoryStream())
{
// 构建数据包: STX + 长度 + 数据 + 校验和 + ETX
ms.WriteByte(STX);
ms.WriteByte((byte)data.Length);
ms.Write(data, 0, data.Length);
// 计算校验和(简单求和)
byte checksum = 0;
foreach (byte b in data)
{
checksum = (byte)(checksum + b);
}
ms.WriteByte(checksum);
ms.WriteByte(ETX);
SendBytes(ms.ToArray());
}
}
3. 编码问题处理
public enum SerialEncoding
{
ASCII,
UTF8,
GB2312,
Unicode
}
public void SendEncodedText(string text, SerialEncoding encoding)
{
if (!_serialPort.IsOpen)
throw new InvalidOperationException("串口未打开");
byte[] data;
switch (encoding)
{
case SerialEncoding.ASCII:
data = Encoding.ASCII.GetBytes(text);
break;
case SerialEncoding.UTF8:
data = Encoding.UTF8.GetBytes(text);
break;
case SerialEncoding.GB2312:
// 注意:需要添加System.Text.Encoding.CodePages包
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
data = Encoding.GetEncoding("GB2312").GetBytes(text);
break;
case SerialEncoding.Unicode:
data = Encoding.Unicode.GetBytes(text);
break;
default:
data = Encoding.UTF8.GetBytes(text);
break;
}
_serialPort.Write(data, 0, data.Length);
}
异常处理与资源管理
1. 完善的异常处理
public class RobustSerialPort : IDisposable
{
private SerialPort _serialPort;
private readonly object _lockObject = new object();
public bool Connect(string portName, int baudRate = 9600)
{
lock (_lockObject)
{
try
{
// 检查端口是否存在
if (!SerialPort.GetPortNames().Contains(portName))
throw new ArgumentException($"串口 {portName} 不存在");
// 检查端口是否被占用
var testPort = new SerialPort(portName);
try
{
testPort.Open();
testPort.Close();
}
catch (UnauthorizedAccessException)
{
throw new IOException($"串口 {portName} 被其他程序占用");
}
// 配置并打开串口
ConfigureSerialPort(portName, baudRate);
_serialPort.Open();
// 等待串口稳定
Thread.Sleep(100);
return true;
}
catch (UnauthorizedAccessException ex)
{
throw new IOException($"没有权限访问串口 {portName}", ex);
}
catch (ArgumentOutOfRangeException ex)
{
throw new ArgumentException("串口参数配置错误", ex);
}
catch (ArgumentException ex)
{
throw new ArgumentException($"无效的串口参数: {ex.Message}", ex);
}
catch (IOException ex)
{
throw new IOException($"串口IO错误: {ex.Message}", ex);
}
catch (InvalidOperationException ex)
{
throw new InvalidOperationException($"串口状态错误: {ex.Message}", ex);
}
catch (Exception ex)
{
throw new Exception($"连接串口时发生未知错误: {ex.Message}", ex);
}
}
}
private void ConfigureSerialPort(string portName, int baudRate)
{
if (_serialPort != null && _serialPort.IsOpen)
_serialPort.Close();
_serialPort = new SerialPort(portName)
{
BaudRate = baudRate,
DataBits = 8,
StopBits = StopBits.One,
Parity = Parity.None,
Handshake = Handshake.None,
ReadTimeout = 1000,
WriteTimeout = 1000,
ReadBufferSize = 4096,
WriteBufferSize = 2048,
Encoding = Encoding.UTF8,
DtrEnable = true,
RtsEnable = true
};
// 订阅事件
_serialPort.DataReceived += OnDataReceived;
_serialPort.ErrorReceived += OnErrorReceived;
}
private void OnErrorReceived(object sender, SerialErrorReceivedEventArgs e)
{
string errorMsg = e.EventType switch
{
SerialError.Frame => "帧错误",
SerialError.Overrun => "数据溢出",
SerialError.RXOver => "接收缓冲区溢出",
SerialError.RXParity => "奇偶校验错误",
SerialError.TXFull => "发送缓冲区已满",
_ => "未知串口错误"
};
// 触发错误事件或记录日志
Console.WriteLine($"串口错误: {errorMsg}");
}
public void SafeClose()
{
lock (_lockObject)
{
try
{
if (_serialPort != null && _serialPort.IsOpen)
{
// 清空缓冲区
_serialPort.DiscardInBuffer();
_serialPort.DiscardOutBuffer();
// 取消事件订阅
_serialPort.DataReceived -= OnDataReceived;
_serialPort.ErrorReceived -= OnErrorReceived;
// 关闭串口
_serialPort.Close();
}
}
catch (Exception ex)
{
// 记录日志,但不抛出异常
Console.WriteLine($"关闭串口时发生错误: {ex.Message}");
}
finally
{
_serialPort?.Dispose();
_serialPort = null;
}
}
}
public void Dispose()
{
SafeClose();
}
}
2. 资源管理和清理
public class SerialPortManager : IDisposable
{
private List<SerialPort> _activePorts = new List<SerialPort>();
private bool _disposed = false;
public SerialPort CreateAndOpenPort(string portName, int baudRate = 9600)
{
CheckDisposed();
var port = new SerialPort(portName, baudRate);
try
{
port.Open();
_activePorts.Add(port);
return port;
}
catch (Exception)
{
port.Dispose();
throw;
}
}
public void CloseAllPorts()
{
CheckDisposed();
foreach (var port in _activePorts.ToArray())
{
try
{
if (port.IsOpen)
port.Close();
port.Dispose();
}
catch (Exception ex)
{
Console.WriteLine($"关闭端口时出错: {ex.Message}");
}
}
_activePorts.Clear();
}
private void CheckDisposed()
{
if (_disposed)
throw new ObjectDisposedException(nameof(SerialPortManager));
}
public void Dispose()
{
if (!_disposed)
{
CloseAllPorts();
_disposed = true;
}
}
}
高级功能与配置
1. 自定义握手协议实现
public class AdvancedSerialPort : SerialPort
{
public bool UseCustomHandshake { get; set; } = false;
public AdvancedSerialPort() : base() { }
public AdvancedSerialPort(string portName) : base(portName) { }
public void SendWithXonXoff(string data)
{
if (!IsOpen)
throw new InvalidOperationException("串口未打开");
try
{
// XOn/XOff流控制
byte xon = 0x11; // DC1
byte xoff = 0x13; // DC3
// 发送XOn开始传输
Write(new byte[] { xon }, 0, 1);
Thread.Sleep(10); // 等待对方准备
// 发送数据
byte[] dataBytes = Encoding.UTF8.GetBytes(data);
Write(dataBytes, 0, dataBytes.Length);
// 发送XOff停止传输
Write(new byte[] { xoff }, 0, 1);
}
catch (Exception ex)
{
throw new Exception($"XOn/XOff传输失败: {ex.Message}", ex);
}
}
public void SendWithRtsCts(string data)
{
if (!IsOpen)
throw new InvalidOperationException("串口未打开");
try
{
// RTS/CTS硬件流控制
// RTS (Request To Send) 已经在基类中有 RtsEnable 属性
// CTS (Clear To Send) 可以通过 PinChanged 事件监控
// 启用RTS
RtsEnable = true;
// 等待CTS就绪(这里简化处理,实际应用中需要监听PinChanged事件)
Thread.Sleep(50);
// 发送数据
Write(data);
// 禁用RTS
RtsEnable = false;
}
catch (Exception ex)
{
// 确保RTS被重置
RtsEnable = false;
throw new Exception($"RTS/CTS传输失败: {ex.Message}", ex);
}
}
}
2. 串口参数动态配置
public class SerialPortConfig
{
public string PortName { get; set; } = "COM1";
public int BaudRate { get; set; } = 9600;
public int DataBits { get; set; } = 8;
public StopBits StopBits { get; set; } = StopBits.One;
public Parity Parity { get; set; } = Parity.None;
public Handshake Handshake { get; set; } = Handshake.None;
public int ReadTimeout { get; set; } = 500;
public int WriteTimeout { get; set; } = 500;
public Encoding Encoding { get; set; } = Encoding.UTF8;
public bool DtrEnable { get; set; } = true;
public bool RtsEnable { get; set; } = true;
}
public class ConfigurableSerialPort
{
private SerialPort _serialPort;
private SerialPortConfig _config;
public ConfigurableSerialPort(SerialPortConfig config)
{
_config = config ?? throw new ArgumentNullException(nameof(config));
InitializeSerialPort();
}
private void InitializeSerialPort()
{
_serialPort = new SerialPort(_config.PortName)
{
BaudRate = _config.BaudRate,
DataBits = _config.DataBits,
StopBits = _config.StopBits,
Parity = _config.Parity,
Handshake = _config.Handshake,
ReadTimeout = _config.ReadTimeout,
WriteTimeout = _config.WriteTimeout,
Encoding = _config.Encoding,
DtrEnable = _config.DtrEnable,
RtsEnable = _config.RtsEnable
};
}
public bool UpdateConfiguration(SerialPortConfig newConfig)
{
if (newConfig == null)
throw new ArgumentNullException(nameof(newConfig));
try
{
bool wasOpen = _serialPort.IsOpen;
if (wasOpen)
_serialPort.Close();
_config = newConfig;
InitializeSerialPort();
if (wasOpen)
_serialPort.Open();
return true;
}
catch (Exception ex)
{
throw new Exception($"配置更新失败: {ex.Message}", ex);
}
}
public SerialPortConfig GetCurrentConfiguration()
{
return new SerialPortConfig
{
PortName = _serialPort.PortName,
BaudRate = _serialPort.BaudRate,
DataBits = _serialPort.DataBits,
StopBits = _serialPort.StopBits,
Parity = _serialPort.Parity,
Handshake = _serialPort.Handshake,
ReadTimeout = _serialPort.ReadTimeout,
WriteTimeout = _serialPort.WriteTimeout,
Encoding = _serialPort.Encoding,
DtrEnable = _serialPort.DtrEnable,
RtsEnable = _serialPort.RtsEnable
};
}
}
3. 串口监控和日志记录
public class LoggingSerialPort : SerialPort
{
private readonly string _logFilePath;
private readonly object _logLock = new object();
public LoggingSerialPort(string logFilePath = null)
{
_logFilePath = logFilePath ?? $"SerialPortLog_{DateTime.Now:yyyyMMdd}.txt";
DataReceived += OnDataReceived;
}
private void OnDataReceived(object sender, SerialDataReceivedEventArgs e)
{
try
{
string data = ReadExisting();
LogMessage($"RECV [{PortName}]: {data}");
}
catch (Exception ex)
{
LogMessage($"ERROR [{PortName}]: 接收数据时出错 - {ex.Message}");
}
}
public new void Write(string text)
{
LogMessage($"SEND [{PortName}]: {text}");
base.Write(text);
}
public new void Write(byte[] buffer, int offset, int count)
{
string hexData = BitConverter.ToString(buffer, offset, count);
LogMessage($"SEND [{PortName}] (HEX): {hexData}");
base.Write(buffer, offset, count);
}
private void LogMessage(string message)
{
lock (_logLock)
{
try
{
string logEntry = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} - {message}{Environment.NewLine}";
File.AppendAllText(_logFilePath, logEntry, Encoding.UTF8);
}
catch (Exception ex)
{
// 日志写入失败不应该影响主要功能
Debug.WriteLine($"日志写入失败: {ex.Message}");
}
}
}
public void EnableHexLogging(bool enable = true)
{
// 可以通过这种方式切换日志格式
// 实际实现中可能需要更复杂的逻辑
}
}
实际应用案例
案例1: 工业设备数据采集器
public class IndustrialDeviceReader
{
private SerialPortHelper _serialPort;
private Timer _pollingTimer;
private readonly List<SensorData> _sensorDataHistory = new List<SensorData>();
public class SensorData
{
public DateTime Timestamp { get; set; }
public double Temperature { get; set; }
public double Pressure { get; set; }
public double Humidity { get; set; }
public bool IsValid { get; set; }
}
public IndustrialDeviceReader()
{
_serialPort = new SerialPortHelper();
_serialPort.DataReceived += OnDeviceDataReceived;
_serialPort.ErrorOccurred += OnDeviceError;
}
public bool ConnectToDevice(string portName, int baudRate = 19200)
{
return _serialPort.Connect(portName, baudRate);
}
public void StartPolling(int intervalMs = 1000)
{
_pollingTimer = new Timer(_ =>
{
try
{
if (_serialPort.IsConnected)
{
// 发送读取命令
_serialPort.SendData("READ_SENSORS\r\n");
}
}
catch (Exception ex)
{
OnDeviceError(this, $"轮询错误: {ex.Message}");
}
}, null, 0, intervalMs);
}
public void StopPolling()
{
_pollingTimer?.Dispose();
_pollingTimer = null;
}
private void OnDeviceDataReceived(object sender, string data)
{
try
{
// 解析设备数据 (示例格式: TEMP:25.6,PRES:1013.2,HUM:45.8)
var sensorData = ParseSensorData(data);
if (sensorData != null)
{
sensorData.Timestamp = DateTime.Now;
_sensorDataHistory.Add(sensorData);
// 限制历史数据大小
if (_sensorDataHistory.Count > 1000)
_sensorDataHistory.RemoveAt(0);
// 触发数据更新事件
DataUpdated?.Invoke(this, sensorData);
}
}
catch (Exception ex)
{
OnDeviceError(this, $"数据解析错误: {ex.Message}");
}
}
private SensorData ParseSensorData(string data)
{
try
{
var sensorData = new SensorData();
// 简单的键值对解析
string[] pairs = data.Split(',');
foreach (string pair in pairs)
{
string[] keyValue = pair.Split(':');
if (keyValue.Length == 2)
{
string key = keyValue[0].Trim().ToUpper();
string value = keyValue[1].Trim();
switch (key)
{
case "TEMP":
double temp;
if (double.TryParse(value, out temp))
sensorData.Temperature = temp;
break;
case "PRES":
double pres;
if (double.TryParse(value, out pres))
sensorData.Pressure = pres;
break;
case "HUM":
double hum;
if (double.TryParse(value, out hum))
sensorData.Humidity = hum;
break;
}
}
}
sensorData.IsValid = sensorData.Temperature != 0 || sensorData.Pressure != 0 || sensorData.Humidity != 0;
return sensorData;
}
catch
{
return null;
}
}
private void OnDeviceError(object sender, string error)
{
ErrorOccurred?.Invoke(this, error);
}
public void Dispose()
{
StopPolling();
_serialPort?.Dispose();
}
// 事件定义
public event EventHandler<SensorData> DataUpdated;
public event EventHandler<string> ErrorOccurred;
}
案例2: Modbus RTU协议实现
public class ModbusRTU
{
private SerialPortHelper _serialPort;
private readonly byte _slaveAddress;
public ModbusRTU(byte slaveAddress = 1)
{
_slaveAddress = slaveAddress;
_serialPort = new SerialPortHelper();
_serialPort.DataReceived += OnModbusDataReceived;
}
public bool Connect(string portName, int baudRate = 9600)
{
return _serialPort.Connect(portName, baudRate);
}
// 读取保持寄存器 (功能码 0x03)
public byte[] ReadHoldingRegisters(ushort startAddress, ushort quantity)
{
byte[] request = BuildReadRequest(0x03, startAddress, quantity);
_serialPort.SendData(request);
// 等待响应(简化处理,实际需要超时和重试机制)
Thread.Sleep(100);
return null; // 实际实现中需要返回解析的响应数据
}
// 写入单个寄存器 (功能码 0x06)
public bool WriteSingleRegister(ushort address, ushort value)
{
byte[] request = BuildWriteSingleRequest(0x06, address, value);
_serialPort.SendData(request);
Thread.Sleep(100);
return true; // 实际实现中需要验证响应
}
private byte[] BuildReadRequest(byte functionCode, ushort startAddress, ushort quantity)
{
List<byte> frame = new List<byte>();
// 设备地址
frame.Add(_slaveAddress);
// 功能码
frame.Add(functionCode);
// 起始地址
frame.Add((byte)(startAddress >> 8));
frame.Add((byte)(startAddress & 0xFF));
// 寄存器数量
frame.Add((byte)(quantity >> 8));
frame.Add((byte)(quantity & 0xFF));
// CRC校验
ushort crc = CalculateCRC(frame.ToArray());
frame.Add((byte)(crc & 0xFF));
frame.Add((byte)(crc >> 8));
return frame.ToArray();
}
private byte[] BuildWriteSingleRequest(byte functionCode, ushort address, ushort value)
{
List<byte> frame = new List<byte>();
frame.Add(_slaveAddress);
frame.Add(functionCode);
frame.Add((byte)(address >> 8));
frame.Add((byte)(address & 0xFF));
frame.Add((byte)(value >> 8));
frame.Add((byte)(value & 0xFF));
ushort crc = CalculateCRC(frame.ToArray());
frame.Add((byte)(crc & 0xFF));
frame.Add((byte)(crc >> 8));
return frame.ToArray();
}
private ushort CalculateCRC(byte[] data)
{
ushort crc = 0xFFFF;
foreach (byte b in data)
{
crc ^= b;
for (int i = 0; i < 8; i++)
{
if ((crc & 0x0001) != 0)
{
crc >>= 1;
crc ^= 0xA001;
}
else
{
crc >>= 1;
}
}
}
return crc;
}
private void OnModbusDataReceived(object sender, string data)
{
// 实际实现中需要将字符串转换为字节数组并解析Modbus响应
Console.WriteLine($"Modbus响应: {data}");
}
}
案例3: 串口调试助手
public partial class SerialDebugForm : Form
{
private SerialPortHelper _serialPort;
private StringBuilder _receiveBuffer = new StringBuilder();
public SerialDebugForm()
{
InitializeComponent();
InitializeSerialPort();
LoadAvailablePorts();
}
private void InitializeSerialPort()
{
_serialPort = new SerialPortHelper();
_serialPort.DataReceived += OnDataReceived;
_serialPort.ErrorOccurred += OnErrorOccurred;
}
private void LoadAvailablePorts()
{
cmbPort.Items.Clear();
string[] ports = SerialPort.GetPortNames();
cmbPort.Items.AddRange(ports);
if (ports.Length > 0)
cmbPort.SelectedIndex = 0;
}
private void btnRefreshPorts_Click(object sender, EventArgs e)
{
LoadAvailablePorts();
}
private void btnConnect_Click(object sender, EventArgs e)
{
if (_serialPort.IsConnected)
{
_serialPort.Disconnect();
btnConnect.Text = "连接";
cmbPort.Enabled = true;
cmbBaudRate.Enabled = true;
return;
}
if (cmbPort.SelectedItem == null)
{
MessageBox.Show("请选择串口号");
return;
}
string portName = cmbPort.SelectedItem.ToString();
if (!int.TryParse(cmbBaudRate.SelectedItem?.ToString() ?? "9600", out int baudRate))
baudRate = 9600;
if (_serialPort.Connect(portName, baudRate))
{
btnConnect.Text = "断开";
cmbPort.Enabled = false;
cmbBaudRate.Enabled = false;
AppendToLog($"已连接到 {portName} @ {baudRate}bps");
}
else
{
MessageBox.Show("连接失败");
}
}
private void btnSend_Click(object sender, EventArgs e)
{
if (!_serialPort.IsConnected)
{
MessageBox.Show("请先连接串口");
return;
}
string data = txtSend.Text;
if (string.IsNullOrEmpty(data))
return;
try
{
if (rbText.Checked)
{
_serialPort.SendDataLine(data);
}
else // Hex模式
{
byte[] hexData = HexStringToByteArray(data);
_serialPort.SendData(hexData);
}
AppendToLog($"[发送] {data}");
txtSend.Clear();
}
catch (Exception ex)
{
MessageBox.Show($"发送失败: {ex.Message}");
}
}
private void OnDataReceived(object sender, string data)
{
this.Invoke(new Action(() =>
{
if (chkTimestamp.Checked)
AppendToLog($"[{DateTime.Now:HH:mm:ss}] [接收] {data}");
else
AppendToLog($"[接收] {data}");
}));
}
private void OnErrorOccurred(object sender, string error)
{
this.Invoke(new Action(() =>
{
AppendToLog($"[错误] {error}");
}));
}
private void AppendToLog(string message)
{
txtLog.AppendText($"{message}\r\n");
// 自动滚动到底部
txtLog.SelectionStart = txtLog.TextLength;
txtLog.ScrollToCaret();
}
private byte[] HexStringToByteArray(string hex)
{
hex = hex.Replace(" ", "").Replace("-", "");
if (hex.Length % 2 != 0)
throw new ArgumentException("十六进制字符串长度必须为偶数");
byte[] bytes = new byte[hex.Length / 2];
for (int i = 0; i < bytes.Length; i++)
{
bytes[i] = Convert.ToByte(hex.Substring(i * 2, 2), 16);
}
return bytes;
}
private void SerialDebugForm_FormClosing(object sender, FormClosingEventArgs e)
{
_serialPort?.Dispose();
}
}
调试技巧与常见问题
1. 调试技巧
串口检测工具
public class SerialPortDiagnostics
{
public static void DiagnosePort(string portName)
{
Console.WriteLine($"=== 诊断串口 {portName} ===");
// 检查端口是否存在
string[] availablePorts = SerialPort.GetPortNames();
bool portExists = availablePorts.Contains(portName);
Console.WriteLine($"端口存在: {portExists}");
if (!portExists)
{
Console.WriteLine("可用的串口:");
foreach (string port in availablePorts)
Console.WriteLine($" {port}");
return;
}
// 检查端口是否被占用
try
{
using (var testPort = new SerialPort(portName))
{
testPort.Open();
Console.WriteLine("端口未被占用,可以正常打开");
testPort.Close();
}
}
catch (UnauthorizedAccessException)
{
Console.WriteLine("端口被其他程序占用");
}
catch (Exception ex)
{
Console.WriteLine($"检查端口占用时出错: {ex.Message}");
}
// 尝试不同配置打开
TestCommonConfigurations(portName);
}
private static void TestCommonConfigurations(string portName)
{
var commonBaudRates = new[] { 9600, 19200, 38400, 57600, 115200 };
foreach (int baudRate in commonBaudRates)
{
try
{
using (var port = new SerialPort(portName, baudRate))
{
port.Open();
Console.WriteLine($"波特率 {baudRate} 可用");
port.Close();
}
}
catch (Exception ex)
{
Console.WriteLine($"波特率 {baudRate} 不可用: {ex.Message}");
}
}
}
public static void ListAllPortsInfo()
{
string[] ports = SerialPort.GetPortNames();
Console.WriteLine($"发现 {ports.Length} 个串口:");
foreach (string port in ports)
{
Console.WriteLine($"\n--- {port} ---");
try
{
using (var portObj = new SerialPort(port))
{
portObj.Open();
Console.WriteLine($"波特率: {portObj.BaudRate}");
Console.WriteLine($"数据位: {portObj.DataBits}");
Console.WriteLine($"停止位: {portObj.StopBits}");
Console.WriteLine($"校验位: {portObj.Parity}");
portObj.Close();
}
}
catch (Exception ex)
{
Console.WriteLine($"获取端口信息失败: {ex.Message}");
}
}
}
}
实时监控工具
public class SerialPortMonitor : IDisposable
{
private SerialPort _monitorPort;
private readonly List<byte> _rawBuffer = new List<byte>();
private Timer _analysisTimer;
public event EventHandler<string> RawDataReceived;
public event EventHandler<string> AnalysisReport;
public void StartMonitoring(string portName, int baudRate = 9600)
{
try
{
_monitorPort = new SerialPort(portName, baudRate)
{
ReadTimeout = 100,
WriteTimeout = 100
};
_monitorPort.DataReceived += OnRawDataReceived;
_monitorPort.Open();
// 启动分析定时器
_analysisTimer = new Timer(_ => AnalyzeTraffic(), null, 1000, 1000);
Console.WriteLine($"开始监控串口 {portName}");
}
catch (Exception ex)
{
Console.WriteLine($"启动监控失败: {ex.Message}");
}
}
private void OnRawDataReceived(object sender, SerialDataReceivedEventArgs e)
{
try
{
int bytesToRead = _monitorPort.BytesToRead;
byte[] buffer = new byte[bytesToRead];
int bytesRead = _monitorPort.Read(buffer, 0, bytesToRead);
if (bytesRead > 0)
{
lock (_rawBuffer)
{
_rawBuffer.AddRange(buffer.Take(bytesRead));
}
// 触发原始数据事件
string hexData = BitConverter.ToString(buffer, 0, bytesRead);
RawDataReceived?.Invoke(this, $"收到 {bytesRead} 字节: {hexData}");
}
}
catch (Exception ex)
{
Console.WriteLine($"监控数据接收错误: {ex.Message}");
}
}
private void AnalyzeTraffic()
{
lock (_rawBuffer)
{
if (_rawBuffer.Count == 0)
return;
// 简单的流量分析
int totalBytes = _rawBuffer.Count;
double avgBytesPerSecond = totalBytes; // 简化计算
// 统计控制字符比例
int controlChars = _rawBuffer.Count(b => b < 32 && b != 9 && b != 10 && b != 13);
double controlCharRatio = (double)controlChars / totalBytes * 100;
string report = $"流量分析 - 总字节: {totalBytes}, 控制字符比例: {controlCharRatio:F2}%";
AnalysisReport?.Invoke(this, report);
// 清空缓冲区(实际实现中可能需要更复杂的逻辑)
_rawBuffer.Clear();
}
}
public void StopMonitoring()
{
_analysisTimer?.Dispose();
_monitorPort?.Close();
_monitorPort?.Dispose();
Console.WriteLine("停止监控");
}
public void Dispose()
{
StopMonitoring();
}
}
2. 常见问题与解决方案
问题1: 端口被占用
症状: 打开串口时抛出 UnauthorizedAccessException
解决方案:
try
{
_serialPort.Open();
}
catch (UnauthorizedAccessException)
{
// 1. 检查是否有其他程序在使用该端口
// 2. 重启应用程序
// 3. 使用不同的串口号
// 4. 在任务管理器中结束占用端口的进程
}
问题2: 数据接收不完整或乱码
症状: 收到的数据不完整或出现乱码
解决方案:
// 1. 检查波特率、数据位、停止位、校验位是否匹配
_serialPort.BaudRate = 9600;
_serialPort.DataBits = 8;
_serialPort.StopBits = StopBits.One;
_serialPort.Parity = Parity.None;
// 2. 设置正确的编码
_serialPort.Encoding = Encoding.UTF8; // 或 Encoding.ASCII, Encoding.GetEncoding("GB2312")
// 3. 增加读取超时时间
_serialPort.ReadTimeout = 2000;
// 4. 使用缓冲区累积数据,按协议解析完整数据包
问题3: 数据发送失败或超时
症状: 发送数据时抛出 TimeoutException
解决方案:
try
{
_serialPort.WriteTimeout = 3000; // 增加超时时间
_serialPort.Write(data);
}
catch (TimeoutException)
{
// 1. 检查接收方是否准备好接收数据
// 2. 检查流控制设置
// 3. 减小单次发送的数据量
// 4. 实现分块发送
}
问题4: 中文乱码问题
症状: 发送或接收中文时出现乱码
解决方案:
// 1. 确保双方使用相同的编码
_serialPort.Encoding = Encoding.UTF8; // 发送方和接收方都使用UTF-8
// 2. 对于GB2312编码的设备
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
_serialPort.Encoding = Encoding.GetEncoding("GB2312");
// 3. 手动编码转换
byte[] data = Encoding.UTF8.GetBytes(chineseText);
_serialPort.Write(data, 0, data.Length);
// 接收时
string received = _serialPort.ReadExisting();
string decodedText = Encoding.UTF8.GetString(Encoding.Default.GetBytes(received));
性能优化建议
1. 缓冲区优化
public class OptimizedSerialPort : SerialPort
{
public OptimizedSerialPort() : base()
{
// 优化缓冲区大小
ReadBufferSize = 8192; // 增大读缓冲区
WriteBufferSize = 4096; // 增大写缓冲区
// 禁用不必要的功能以提高性能
DiscardNull = true; // 丢弃null字节
ReceivedBytesThreshold = 1; // 每收到1字节就触发事件(根据需求调整)
}
// 使用异步操作避免阻塞
public async Task<int> ReadAsync(byte[] buffer, int offset, int count)
{
return await BaseStream.ReadAsync(buffer, offset, count);
}
public async Task WriteAsync(byte[] buffer, int offset, int count)
{
await BaseStream.WriteAsync(buffer, offset, count);
}
}
2. 数据处理优化
public class HighPerformanceDataProcessor
{
private readonly CircularBuffer<byte> _receiveBuffer;
private readonly object _bufferLock = new object();
public HighPerformanceDataProcessor(int bufferSize = 8192)
{
_receiveBuffer = new CircularBuffer<byte>(bufferSize);
}
// 高性能的字节数组接收(避免频繁的字符串操作)
public void ProcessReceivedBytes(byte[] data, int length)
{
lock (_bufferLock)
{
_receiveBuffer.Write(data, 0, length);
}
// 处理完整数据包(在后台线程中处理,避免阻塞接收线程)
Task.Run(() => ProcessCompletePackets());
}
private void ProcessCompletePackets()
{
lock (_bufferLock)
{
while (_receiveBuffer.Count >= PacketSize) // PacketSize根据协议定义
{
byte[] packet = new byte[PacketSize];
_receiveBuffer.Read(packet, 0, PacketSize);
// 处理数据包(避免在此处进行复杂的字符串操作)
ProcessPacketFast(packet);
}
}
}
// 快速数据包处理(避免装箱拆箱和复杂计算)
private void ProcessPacketFast(byte[] packet)
{
// 使用结构体或ref局部变量优化性能
ref byte firstByte = ref packet[0];
// 简单的数值比较(避免字符串比较)
if (firstByte == 0x02) // STX
{
// 处理有效数据包
ExtractValuesFast(packet);
}
}
// 快速数值提取
private void ExtractValuesFast(byte[] packet)
{
// 使用BitConverter或移位操作(比解析字符串快得多)
short value1 = BitConverter.ToInt16(packet, 1);
short value2 = BitConverter.ToInt16(packet, 3);
// 直接存储或触发事件(避免在循环中创建对象)
OnValuesExtracted(value1, value2);
}
}
3. 连接池管理
public class SerialPortConnectionPool : IDisposable
{
private readonly Dictionary<string, Queue<SerialPort>> _portPool;
private readonly Dictionary<string, SemaphoreSlim> _portSemaphores;
private readonly int _maxPoolSizePerPort;
private bool _disposed = false;
public SerialPortConnectionPool(int maxPoolSizePerPort = 5)
{
_portPool = new Dictionary<string, Queue<SerialPort>>();
_portSemaphores = new Dictionary<string, SemaphoreSlim>();
_maxPoolSizePerPort = maxPoolSizePerPort;
}
public SerialPort GetPort(string portName, int baudRate = 9600)
{
CheckDisposed();
string key = $"{portName}_{baudRate}";
lock (_portPool)
{
if (!_portPool.ContainsKey(key))
{
_portPool[key] = new Queue<SerialPort>();
_portSemaphores[key] = new SemaphoreSlim(_maxPoolSizePerPort, _maxPoolSizePerPort);
}
}
// 等待可用的端口
_portSemaphores[key].Wait();
lock (_portPool)
{
if (_portPool[key].Count > 0)
{
return _portPool[key].Dequeue();
}
}
// 创建新的端口
var port = new SerialPort(portName, baudRate);
port.Open();
return port;
}
public void ReturnPort(SerialPort port, int baudRate = 9600)
{
if (port == null || !port.IsOpen)
return;
string key = $"{port.PortName}_{baudRate}";
lock (_portPool)
{
if (_portPool.ContainsKey(key) && _portPool[key].Count < _maxPoolSizePerPort)
{
_portPool[key].Enqueue(port);
_portSemaphores[key].Release();
return;
}
}
// 池已满或不存在,关闭端口
port.Close();
port.Dispose();
}
public void ClearPool()
{
lock (_portPool)
{
foreach (var kvp in _portPool)
{
while (kvp.Value.Count > 0)
{
var port = kvp.Value.Dequeue();
port?.Dispose();
}
}
_portPool.Clear();
}
}
private void CheckDisposed()
{
if (_disposed)
throw new ObjectDisposedException(nameof(SerialPortConnectionPool));
}
public void Dispose()
{
if (!_disposed)
{
ClearPool();
_disposed = true;
}
}
}
总结
通过本文的全面介绍,你应该已经掌握了使用C# SerialPort类进行串口通信的各种技术和最佳实践:
核心要点回顾:
- 基础配置:正确设置波特率、数据位、停止位、校验位等参数
- 数据收发:掌握字符串和二进制数据的发送接收方法
- 事件处理:合理使用DataReceived事件进行异步数据处理
- 异常处理:完善的错误处理机制确保程序稳定性
- 资源管理:正确使用Dispose模式管理串口资源
- 协议实现:能够处理自定义协议和数据包解析
- 性能优化:通过缓冲区优化、异步操作等提升性能
最佳实践建议:
- 总是使用异常处理包装串口操作
- 及时释放资源,使用using语句或Dispose模式
- 根据设备特性配置参数,特别是波特率和流控制
- 实现数据校验机制确保数据完整性
- 使用合适的编码处理文本数据,避免乱码
- 考虑使用后台线程处理大量数据,避免阻塞UI
- 实现重连机制提高系统可靠性
串口通信是工业控制和嵌入式系统开发中的重要技术,掌握好SerialPort类的使用将为你的项目开发提供强有力的支持。在实际开发中,还需要根据具体设备的协议规范进行相应的调整和优化。
© 版权声明
文中内容均来源于公开资料,受限于信息的时效性和复杂性,可能存在误差或遗漏。我们已尽力确保内容的准确性,但对于因信息变更或错误导致的任何后果,本站不承担任何责任。如需引用本文内容,请注明出处并尊重原作者的版权。
THE END
























暂无评论内容