使用.NET8实现一个完整的串口通讯工具类
作者:code_shenbing
引言
串口通信(Serial Communication)在工业控制、物联网设备、嵌入式系统和自动化领域仍然广泛应用。.NET 8 提供了强大的 System.IO.Ports命名空间,使得实现串口通信变得简单高效。本文将详细介绍如何使用 .NET 8 实现一个功能完整的串口通信工具类,包含配置管理、数据收发、事件处理和错误处理等功能。
1. 串口通信工具类设计
首先,我们设计一个 SerialPortTool类,封装所有串口操作:
using System;
using System.IO.Ports;
using System.Threading;
using System.Threading.Tasks;
public class SerialPortTool : IDisposable
{
private SerialPort _serialPort;
private CancellationTokenSource _cancellationTokenSource;
private bool _isOpen = false;
// 事件定义
public event EventHandler<string> PortOpened;
public event EventHandler<string> PortClosed;
public event EventHandler<byte[]> DataReceived;
public event EventHandler<string> MessageReceived;
public event EventHandler<Exception> ErrorOccurred;
// 配置属性
public string PortName { get; private set; }
public int BaudRate { get; private set; }
public Parity Parity { get; private set; }
public int DataBits { get; private set; }
public StopBits StopBits { get; private set; }
public Handshake Handshake { get; private set; }
public int ReadTimeout { get; private set; }
public int WriteTimeout { get; private set; }
public bool IsOpen => _isOpen && _serialPort?.IsOpen == true;
public SerialPortTool(string portName, int baudRate = 9600,
Parity parity = Parity.None, int dataBits = 8,
StopBits stopBits = StopBits.One,
Handshake handshake = Handshake.None,
int readTimeout = 1000, int writeTimeout = 1000)
{
PortName = portName;
BaudRate = baudRate;
Parity = parity;
DataBits = dataBits;
StopBits = stopBits;
Handshake = handshake;
ReadTimeout = readTimeout;
WriteTimeout = writeTimeout;
_cancellationTokenSource = new CancellationTokenSource();
}
// 其余实现将在下面展开...
}2. 实现串口打开和关闭
2.1 打开串口
public bool Open()
{
if (IsOpen)
return true;
try
{
_serialPort = new SerialPort(PortName, BaudRate, Parity, DataBits, StopBits)
{
Handshake = Handshake,
ReadTimeout = ReadTimeout,
WriteTimeout = WriteTimeout
};
_serialPort.Open();
_isOpen = true;
// 启动数据接收后台任务
_ = Task.Run(() => ReceiveDataAsync(_cancellationTokenSource.Token));
PortOpened?.Invoke(this, $"串口 {PortName} 已打开");
return true;
}
catch (Exception ex)
{
ErrorOccurred?.Invoke(this, ex);
Close();
return false;
}
}2.2 关闭串口
public void Close()
{
try
{
_cancellationTokenSource.Cancel();
_serialPort?.Close();
_serialPort?.Dispose();
_serialPort = null;
_isOpen = false;
PortClosed?.Invoke(this, $"串口 {PortName} 已关闭");
}
catch (Exception ex)
{
ErrorOccurred?.Invoke(this, ex);
}
}3. 实现数据发送和接收
3.1 发送数据
public bool Send(byte[] data)
{
if (!IsOpen)
return false;
try
{
_serialPort.Write(data, 0, data.Length);
return true;
}
catch (Exception ex)
{
ErrorOccurred?.Invoke(this, ex);
Close();
return false;
}
}
public bool SendString(string message, Encoding encoding = null)
{
encoding ??= Encoding.UTF8;
byte[] data = encoding.GetBytes(message);
return Send(data);
}
public async Task<bool> SendAsync(byte[] data)
{
if (!IsOpen)
return false;
try
{
await _serialPort.BaseStream.WriteAsync(data, 0, data.Length);
return true;
}
catch (Exception ex)
{
ErrorOccurred?.Invoke(this, ex);
Close();
return false;
}
}
public async Task<bool> SendStringAsync(string message, Encoding encoding = null)
{
encoding ??= Encoding.UTF8;
byte[] data = encoding.GetBytes(message);
return await SendAsync(data);
}3.2 接收数据(后台任务)
private async Task ReceiveDataAsync(CancellationToken cancellationToken)
{
byte[] buffer = new byte[4096];
while (!cancellationToken.IsCancellationRequested && IsOpen)
{
try
{
// 异步读取数据
int bytesRead = await _serialPort.BaseStream.ReadAsync(buffer, 0, buffer.Length, cancellationToken);
if (bytesRead > 0)
{
// 复制接收到的数据
byte[] receivedData = new byte[bytesRead];
Array.Copy(buffer, receivedData, bytesRead);
// 触发数据接收事件
DataReceived?.Invoke(this, receivedData);
// 转换为字符串并触发消息接收事件
string message = Encoding.UTF8.GetString(receivedData);
MessageReceived?.Invoke(this, message);
}
}
catch (OperationCanceledException)
{
// 任务被取消,正常退出
break;
}
catch (TimeoutException)
{
// 读取超时,继续等待
}
catch (Exception ex)
{
if (IsOpen) // 只在串口打开时报告错误
{
ErrorOccurred?.Invoke(this, ex);
}
break;
}
}
}4. 完整工具类实现
下面是完整的 SerialPortTool类实现:
using System;
using System.IO.Ports;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
public class SerialPortTool : IDisposable
{
private SerialPort _serialPort;
private CancellationTokenSource _cancellationTokenSource;
private bool _isOpen = false;
// 事件定义
public event EventHandler<string> PortOpened;
public event EventHandler<string> PortClosed;
public event EventHandler<byte[]> DataReceived;
public event EventHandler<string> MessageReceived;
public event EventHandler<Exception> ErrorOccurred;
// 配置属性
public string PortName { get; private set; }
public int BaudRate { get; private set; }
public Parity Parity { get; private set; }
public int DataBits { get; private set; }
public StopBits StopBits { get; private set; }
public Handshake Handshake { get; private set; }
public int ReadTimeout { get; private set; }
public int WriteTimeout { get; private set; }
public bool IsOpen => _isOpen && _serialPort?.IsOpen == true;
public SerialPortTool(string portName, int baudRate = 9600,
Parity parity = Parity.None, int dataBits = 8,
StopBits stopBits = StopBits.One,
Handshake handshake = Handshake.None,
int readTimeout = 1000, int writeTimeout = 1000)
{
PortName = portName;
BaudRate = baudRate;
Parity = parity;
DataBits = dataBits;
StopBits = stopBits;
Handshake = handshake;
ReadTimeout = readTimeout;
WriteTimeout = writeTimeout;
_cancellationTokenSource = new CancellationTokenSource();
}
public bool Open()
{
if (IsOpen)
return true;
try
{
_serialPort = new SerialPort(PortName, BaudRate, Parity, DataBits, StopBits)
{
Handshake = Handshake,
ReadTimeout = ReadTimeout,
WriteTimeout = WriteTimeout
};
_serialPort.Open();
_isOpen = true;
// 启动数据接收后台任务
_ = Task.Run(() => ReceiveDataAsync(_cancellationTokenSource.Token));
PortOpened?.Invoke(this, $"串口 {PortName} 已打开");
return true;
}
catch (Exception ex)
{
ErrorOccurred?.Invoke(this, ex);
Close();
return false;
}
}
public void Close()
{
try
{
_cancellationTokenSource.Cancel();
_serialPort?.Close();
_serialPort?.Dispose();
_serialPort = null;
_isOpen = false;
PortClosed?.Invoke(this, $"串口 {PortName} 已关闭");
}
catch (Exception ex)
{
ErrorOccurred?.Invoke(this, ex);
}
}
public bool Send(byte[] data)
{
if (!IsOpen)
return false;
try
{
_serialPort.Write(data, 0, data.Length);
return true;
}
catch (Exception ex)
{
ErrorOccurred?.Invoke(this, ex);
Close();
return false;
}
}
public bool SendString(string message, Encoding encoding = null)
{
encoding ??= Encoding.UTF8;
byte[] data = encoding.GetBytes(message);
return Send(data);
}
public async Task<bool> SendAsync(byte[] data)
{
if (!IsOpen)
return false;
try
{
await _serialPort.BaseStream.WriteAsync(data, 0, data.Length);
return true;
}
catch (Exception ex)
{
ErrorOccurred?.Invoke(this, ex);
Close();
return false;
}
}
public async Task<bool> SendStringAsync(string message, Encoding encoding = null)
{
encoding ??= Encoding.UTF8;
byte[] data = encoding.GetBytes(message);
return await SendAsync(data);
}
private async Task ReceiveDataAsync(CancellationToken cancellationToken)
{
byte[] buffer = new byte[4096];
while (!cancellationToken.IsCancellationRequested && IsOpen)
{
try
{
int bytesRead = await _serialPort.BaseStream.ReadAsync(buffer, 0, buffer.Length, cancellationToken);
if (bytesRead > 0)
{
byte[] receivedData = new byte[bytesRead];
Array.Copy(buffer, receivedData, bytesRead);
DataReceived?.Invoke(this, receivedData);
string message = Encoding.UTF8.GetString(receivedData);
MessageReceived?.Invoke(this, message);
}
}
catch (OperationCanceledException)
{
break;
}
catch (TimeoutException)
{
// 超时是正常情况,继续等待
}
catch (Exception ex)
{
if (IsOpen)
{
ErrorOccurred?.Invoke(this, ex);
}
break;
}
}
}
#region IDisposable Implementation
private bool _disposed = false;
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
Close();
_cancellationTokenSource?.Dispose();
}
_disposed = true;
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
#endregion
}5. 使用示例
下面是如何使用串口通信工具类的示例:
class Program
{
static async Task Main(string[] args)
{
// 获取可用串口列表
string[] ports = SerialPort.GetPortNames();
Console.WriteLine("可用串口:");
foreach (string port in ports)
{
Console.WriteLine(port);
}
if (ports.Length == 0)
{
Console.WriteLine("没有找到可用串口");
return;
}
// 使用第一个可用串口
string selectedPort = ports[0];
using var serialTool = new SerialPortTool(
portName: selectedPort,
baudRate: 115200,
parity: Parity.None,
dataBits: 8,
stopBits: StopBits.One
);
// 订阅事件
serialTool.PortOpened += (sender, message) => Console.WriteLine(message);
serialTool.PortClosed += (sender, message) => Console.WriteLine(message);
serialTool.DataReceived += (sender, data) =>
{
Console.WriteLine($"收到字节数据: {BitConverter.ToString(data)}");
};
serialTool.MessageReceived += (sender, message) =>
{
Console.WriteLine($"收到消息: {message}");
};
serialTool.ErrorOccurred += (sender, ex) =>
{
Console.WriteLine($"发生错误: {ex.Message}");
};
// 打开串口
if (serialTool.Open())
{
Console.WriteLine("按 'S' 发送字符串,按 'B' 发送字节数据,按 'Q' 退出");
while (true)
{
var key = Console.ReadKey(intercept: true).Key;
if (key == ConsoleKey.S)
{
Console.Write("输入要发送的字符串: ");
string message = Console.ReadLine();
serialTool.SendString(message);
}
else if (key == ConsoleKey.B)
{
Console.Write("输入要发送的十六进制字节 (例如: 01 02 AA FF): ");
string hexInput = Console.ReadLine();
try
{
byte[] data = ParseHexString(hexInput);
serialTool.Send(data);
Console.WriteLine($"已发送: {BitConverter.ToString(data)}");
}
catch (Exception ex)
{
Console.WriteLine($"解析错误: {ex.Message}");
}
}
else if (key == ConsoleKey.Q)
{
break;
}
}
// 关闭串口(using语句也会自动调用Dispose)
serialTool.Close();
}
else
{
Console.WriteLine("串口打开失败");
}
}
private static byte[] ParseHexString(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++)
{
string byteValue = hex.Substring(i * 2, 2);
bytes[i] = Convert.ToByte(byteValue, 16);
}
return bytes;
}
}6. 高级功能扩展
6.1 添加帧处理功能
对于需要处理特定帧格式的应用,可以添加帧处理功能:
public class FramedSerialPortTool : SerialPortTool
{
private readonly byte[] _frameDelimiter;
private List<byte> _buffer = new List<byte>();
public FramedSerialPortTool(string portName, byte[] frameDelimiter,
int baudRate = 9600, Parity parity = Parity.None,
int dataBits = 8, StopBits stopBits = StopBits.One,
Handshake handshake = Handshake.None,
int readTimeout = 1000, int writeTimeout = 1000)
: base(portName, baudRate, parity, dataBits, stopBits, handshake, readTimeout, writeTimeout)
{
_frameDelimiter = frameDelimiter;
this.DataReceived += OnRawDataReceived;
}
public new event EventHandler<byte[]> FrameReceived;
private void OnRawDataReceived(object sender, byte[] data)
{
_buffer.AddRange(data);
ProcessBuffer();
}
private void ProcessBuffer()
{
while (true)
{
// 查找帧分隔符
int delimiterIndex = FindDelimiter(_buffer.ToArray(), _frameDelimiter);
if (delimiterIndex == -1)
break;
// 提取完整帧
byte[] frameData = new byte[delimiterIndex];
Array.Copy(_buffer.ToArray(), frameData, delimiterIndex);
// 从缓冲区中移除已处理的数据(包括分隔符)
_buffer.RemoveRange(0, delimiterIndex + _frameDelimiter.Length);
// 触发帧接收事件
FrameReceived?.Invoke(this, frameData);
}
}
private int FindDelimiter(byte[] data, byte[] delimiter)
{
for (int i = 0; i <= data.Length - delimiter.Length; i++)
{
bool found = true;
for (int j = 0; j < delimiter.Length; j++)
{
if (data[i + j] != delimiter[j])
{
found = false;
break;
}
}
if (found)
return i;
}
return -1;
}
public bool SendFrame(byte[] frameData)
{
byte[] framedData = new byte[frameData.Length + _frameDelimiter.Length];
Array.Copy(frameData, framedData, frameData.Length);
Array.Copy(_frameDelimiter, 0, framedData, frameData.Length, _frameDelimiter.Length);
return Send(framedData);
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
this.DataReceived -= OnRawDataReceived;
}
base.Dispose(disposing);
}
}6.2 添加自动重连功能
对于需要长时间运行的串口应用,可以添加自动重连功能:
public class AutoReconnectSerialPortTool : SerialPortTool
{
private Timer _reconnectTimer;
private readonly TimeSpan _reconnectInterval;
private int _reconnectAttempts = 0;
private const int MAX_RECONNECT_ATTEMPTS = 10;
public AutoReconnectSerialPortTool(string portName, TimeSpan reconnectInterval,
int baudRate = 9600, Parity parity = Parity.None,
int dataBits = 8, StopBits stopBits = StopBits.One,
Handshake handshake = Handshake.None,
int readTimeout = 1000, int writeTimeout = 1000)
: base(portName, baudRate, parity, dataBits, stopBits, handshake, readTimeout, writeTimeout)
{
_reconnectInterval = reconnectInterval;
this.PortClosed += OnPortClosed;
}
private void OnPortClosed(object sender, string message)
{
if (_reconnectAttempts < MAX_RECONNECT_ATTEMPTS)
{
_reconnectTimer = new Timer(AttemptReconnect, null, _reconnectInterval, Timeout.InfiniteTimeSpan);
}
}
private void AttemptReconnect(object state)
{
_reconnectAttempts++;
if (Open())
{
_reconnectAttempts = 0; // 重置重试计数器
_reconnectTimer?.Dispose();
_reconnectTimer = null;
}
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_reconnectTimer?.Dispose();
this.PortClosed -= OnPortClosed;
}
base.Dispose(disposing);
}
}7. 串口通信最佳实践
1.资源管理:
- 始终使用
using语句或手动调用Dispose()确保资源释放 - 在不再需要时关闭串口连接
2.错误处理:
- 处理所有可能的异常(端口不存在、权限问题、设备断开等)
- 使用事件机制通知上层应用错误发生
3.线程安全:
- 串口事件可能在后台线程触发,确保UI操作在正确的线程执行
- 使用同步机制保护共享资源
4.性能优化:
- 使用异步方法避免阻塞UI线程
- 合理设置缓冲区大小平衡内存使用和性能
- 避免在事件处理中执行耗时操作
5.配置管理:
- 保存和加载串口配置(波特率、数据位等)
- 提供配置验证功能
8. 总结
本文介绍了如何使用 .NET 8 实现一个功能完整的串口通信工具类,包含以下核心功能:
1.串口管理:打开、关闭和状态监控
2.数据收发:支持同步和异步的字节数组和字符串传输
3.事件通知:提供串口状态变化、数据接收和错误通知
4.资源管理:正确实现 IDisposable接口
5.错误处理:健壮的异常处理和错误通知机制
通过这种封装,我们可以在不同的项目中轻松重用串口通信功能,而无需重复编写底层代码。此外,通过继承和扩展,可以轻松添加如帧处理、自动重连等高级功能。
在工业自动化、物联网设备通信和嵌入式系统开发中,这种封装方式能够显著提高开发效率和代码质量,是开发串口通信应用的理想起点。
以上就是使用.NET8实现一个完整的串口通讯工具类的详细内容,更多关于.NET8串口通讯工具类的资料请关注脚本之家其它相关文章!
