C# 串口通讯异步实现的高效可靠设计模式

381 阅读4分钟

前言

嵌入式系统、物联网(IoT)及工业通信领域,串行通信作为经典的数据交换手段依然占据重要地位。

本文将详细介绍如何采用C#构建一个既稳固又高效的异步串行通信管理器,确保设备间数据传输的可靠性与性能。

探讨关键的设计理念与实践技巧,以实现低延迟和高吞吐量的数据交互,同时保证代码的可维护性和扩展性。通过优化串口通信机制,可以更好地支持现代应用对实时性和稳定性的严格要求。

正文

异步编程

异步编程是.NET 中重要特性

非阻塞的I/O操作

提高应用程序的响应性

有效利用系统资源

主要特性

串口管理器将具备以下特性:

异步连接和读取

错误处理

灵活的数据接收机制

资源释放管理

代码示例

类定义与构造函数

public class SerialPortManager : IDisposable
{
    private SerialPort _serialPort;
    private readonly string _portName;
    private readonly int _baudRate;
    private CancellationTokenSource _cancellationTokenSource;

    // 数据接收事件
    public event EventHandler<byte[]> DataReceived;

    public SerialPortManager(string portName, int baudRate)
    {
        _portName = portName;
        _baudRate = baudRate;
        _cancellationTokenSource = new CancellationTokenSource();
    }

异步连接方法

public async Task StartAsync()
{
    try
    {
        await ConnectSerialPortAsync();
    }
    catch (Exception ex)
    {
        LogError($"启动失败:{ex.Message}");
        throw;
    }
}

private async Task ConnectSerialPortAsync()
{
    _serialPort = new SerialPort(_portName, _baudRate)
    {
        ReadTimeout = 500,
        WriteTimeout = 500,
        ReadBufferSize = 4096,
        WriteBufferSize = 4096
    };

    _serialPort.Open();
    _ = StartAsyncReading();
}

异步数据读取

private async Task StartAsyncReading()
{
    byte[] buffer = new byte[4096];
    while (_serialPort.IsOpen 
    && !_cancellationTokenSource.Token.IsCancellationRequested)
    {
        int bytesToRead = _serialPort.BytesToRead;
        if (bytesToRead > 0)
        {
            int readBytes = await _serialPort.BaseStream.ReadAsync(
                buffer, 0, bytesToRead,
                _cancellationTokenSource.Token);

            ProcessReceivedData(buffer.Take(readBytes).ToArray());
        }

        await Task.Delay(50, _cancellationTokenSource.Token);
    }
}

数据发送方法

public async Task<bool> SendDataAsync(byte[] data)
{
    if (_serialPort?.IsOpen == true)
    {
        await _serialPort.BaseStream.WriteAsync(
            data, 0, data.Length, _cancellationTokenSource.Token);
        return true;
    }
    return false;
}

使用示例

public async Task ExampleUsage()
{
    var serialManager = new SerialPortManager("COM3", 9600);

    // 订阅数据接收事件
    serialManager.DataReceived += (sender, data) => {
        Console.WriteLine($"接收到数据: {BitConverter.ToString(data)}");
    };

    // 启动串口
    await serialManager.StartAsync();

    // 发送数据
    byte[] dataToSend = new byte[] { 0x01, 0x02, 0x03 };
    await serialManager.SendDataAsync(dataToSend);
}

完整代码

namespace AppSerialPortTry
{
    /// <summary>  
    /// 串口管理器  
    /// </summary>  
    public class SerialPortManager : IDisposable
    {
        private SerialPort _serialPort;
        private readonly string _portName;
        private readonly int _baudRate;
        private CancellationTokenSource _cancellationTokenSource;

        // 数据接收事件  
        public event EventHandler<byte[]> DataReceived;

        public SerialPortManager(string portName, int baudRate)
        {
            _portName = portName;
            _baudRate = baudRate;
            _cancellationTokenSource = 
            new CancellationTokenSource();
        }

        /// <summary>  
        /// 异步启动串口通信  
        /// </summary>  
        public async Task StartAsync()
        {
            try
            {
                await ConnectSerialPortAsync();
            }
            catch (Exception ex)
            {
                LogError
                ($"启动失败:{ex.Message}");
                throw;
            }
        }

        /// <summary>  
        /// 连接串口  
        /// </summary>  
        private async Task ConnectSerialPortAsync()
        {
            try
            {
                // 创建串口实例  
                _serialPort = new SerialPort(_portName, _baudRate)
                {
                    ReadTimeout = 500,
                    WriteTimeout = 500,
                    ReadBufferSize = 4096,
                    WriteBufferSize = 4096
                };

                // 配置串口事件  
                _serialPort.DataReceived += SerialPort_DataReceived;
                _serialPort.ErrorReceived += SerialPort_ErrorReceived;

                // 打开串口  
                _serialPort.Open();
                LogInfo
                ($"串口 {_portName} 连接成功");

                // 启动异步读取  
                _ = StartAsyncReading();
            }
            catch (Exception ex)
            {
                LogError
                ($"连接串口失败:{ex.Message}");
                throw;
            }
        }

        /// <summary>  
        /// 异步读取数据  
        /// </summary>  
        private async Task StartAsyncReading()
        {
            try
            {
                byte[] buffer = new byte[4096];
                while 
                (_serialPort != null &&
                _serialPort.IsOpen && 
                !_cancellationTokenSource.Token.IsCancellationRequested)
                {
                    int bytesToRead = _serialPort.BytesToRead;
                    if (bytesToRead > 0)
                    {
                        bytesToRead = Math.Min(bytesToRead, buffer.Length);
                        int readBytes = 
                        await _serialPort.BaseStream.ReadAsync(buffer, 0, bytesToRead, _cancellationTokenSource.Token);

                        if (readBytes > 0)
                        {
                            byte[] receivedData = 
                            new byte[readBytes];
                            Array.Copy(buffer, receivedData, readBytes);
                            ProcessReceivedData(receivedData);
                        }
                    }

                    await Task.Delay(50, _cancellationTokenSource.Token);
                }
            }
            catch (OperationCanceledException)
            {
                // 正常取消  
            }
            catch (Exception ex)
            {
                LogError
                ($"异步读取数据错误:{ex.Message}");
            }
        }

        /// <summary>  
        /// 处理接收的数据  
        /// </summary>  
        private void ProcessReceivedData(byte[] data)
        {
            string hexData = BitConverter.ToString(data);
            LogInfo
            ($"接收到数据:{hexData}");

            // 触发数据接收事件  
            DataReceived?.Invoke(this, data);
        }

        /// <summary>  
        /// 串口错误事件  
        /// </summary>  
        private void SerialPort_ErrorReceived(object sender, SerialErrorReceivedEventArgs e)
        {
            LogError
            ($"串口错误:{e.EventType}");
        }

        /// <summary>  
        /// 数据接收事件  
        /// </summary>  
        private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
        {
            try
            {
                // 根据不同的接收模式处理  
                switch (e.EventType)
                {
                    case SerialData.Chars:
                        // 有字符可读  
                        int bytesToRead = 
                        _serialPort.BytesToRead;
                        if (bytesToRead > 0)
                        {
                            byte[] buffer = new byte[bytesToRead];
                            _serialPort.Read(buffer, 0, bytesToRead);

                            // 可以触发一个事件或进行进一步处理  
                            ProcessReceivedData(buffer);
                        }
                        break;

                    case SerialData.Eof:
                        LogWarning("串口接收到文件结束符");
                        break;

                    default:
                        LogInfo
                        ($"接收到未处理的事件类型: {e.EventType}");
                        break;
                }
            }
            catch (Exception ex)
            {
                LogError
                ($"串口数据接收事件处理错误:{ex.Message}");
            }
        }

        /// <summary>  
        /// 发送数据  
        /// </summary>  
        public async Task<bool> SendDataAsync(byte[] data)
        {
            try
            {
                if (_serialPort != null && _serialPort.IsOpen)
                {
                    await _serialPort.BaseStream.WriteAsync
                    (data, 0, data.Length, _cancellationTokenSource.Token);
                    return true;
                }
                return false;
            }
            catch (Exception ex)
            {
                LogError
                ($"发送数据失败:{ex.Message}");
                return false;
            }
        }

        /// <summary>  
        /// 关闭串口  
        /// </summary>  
        public void Close()
        {
            _cancellationTokenSource.Cancel();

            if (_serialPort != null)
            {
                try
                {
                    if (_serialPort.IsOpen)
                    {
                        _serialPort.Close();
                    }

                    // 取消事件绑定  
                    _serialPort.DataReceived -=
                    SerialPort_DataReceived;
                    _serialPort.ErrorReceived -= 
                    SerialPort_ErrorReceived;
                }
                catch (Exception ex)
                {
                    LogError
                    ($"关闭串口时发生错误:{ex.Message}");
                }
            }
        }

        /// <summary>  
        /// 实现 IDisposable  
        /// </summary>  
        public void Dispose()
        {
            Close();
            _cancellationTokenSource?.Dispose();
            _serialPort?.Dispose();
        }

        #region 日志方法  

        private void LogInfo(string message)
        {
            Console.ForegroundColor = ConsoleColor.Green;
            Console.WriteLine
            ($"[INFO] {DateTime.Now:yyyy-MM-dd HH:mm:ss} - {message}");
            Console.ResetColor();
        }

        private void LogWarning(string message)
        {
            Console.ForegroundColor = ConsoleColor.Yellow;
            Console.WriteLine
            ($"[WARNING] {DateTime.Now:yyyy-MM-dd HH:mm:ss} - {message}");
            Console.ResetColor();
        }

        private void LogError(string message)
        {
            Console.ForegroundColor = ConsoleColor.Red;
            Console.WriteLine
            ($"[ERROR] {DateTime.Now:yyyy-MM-dd HH:mm:ss} - {message}");
            Console.ResetColor();
        }

        #endregion
    }
}
using System.IO.Ports;

namespace AppSerialPortTry
{
    internal class Program
    {

        static void Main(string[] args)
        {
            Console.WriteLine
            ("串口通信监控演示程序");
            RunSerialPortMonitorAsync().Wait();

            Console.ReadKey();
        }

        static async Task RunSerialPortMonitorAsync()
        {
            var serialPortManager =
            new SerialPortManager("COM1", 9600);
            await serialPortManager.StartAsync();

            await serialPortManager.SendDataAsync
            (System.Text.Encoding.Default.GetBytes(" Hello World!"));

            serialPortManager.DataReceived += (sender, e) =>
            {
                Console.WriteLine
                (System.Text.Encoding.Default.GetString(e));
            };
        }
    }
}

注意事项

50ms的延迟可以根据实际应用场景调整

4096字节的缓冲区大小适用于大多数中小型通讯场景

对于高频率、大数据量的通讯,可能需要进一步优化

总结

通过采用异步编程模式,开发一个灵活且高效的串口通信管理器。

方法不仅增强了代码的清晰度和可维护性,还大幅提升了程序的性能与响应速度。

异步处理使得程序能够在等待串口操作完成的同时继续执行其他任务,从而优化资源利用,确保了系统的流畅性和高效性。

最后

如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。

也可以加入微信公众号 [DotNet技术匠] 社区,与其他热爱技术的同行一起交流心得,共同成长!

优秀是一种习惯,欢迎大家留言学习!

作者:技术老小子

出处:mp.weixin.qq.com/s/BUOeTmyym5TH0tuwnOz-Nw

声明:网络内容,仅供学习,尊重版权,侵权速删,歉意致谢!