×

使用.NET8实现一个完整的串口通讯工具类

独孤求败 独孤求败 发表于2026-01-23 18:53:27 浏览22 评论0

抢沙发发表评论

(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.错误处理:健壮的异常处理和错误通知机制

通过这种封装,我们可以在不同的项目中轻松重用串口通信功能,而无需重复编写底层代码。此外,通过继承和扩展,可以轻松添加如帧处理、自动重连等高级功能。

在工业自动化、物联网设备通信和嵌入式系统开发中,这种封装方式能够显著提高开发效率和代码质量,是开发串口通信应用的理想起点。

群贤毕至

访客