C# 串口通讯

在开发C#上位机程序中经常需要使用串口对下位机设备进行数据收发操作。以下代码为个人常用的两种串口通讯收发方式。

具体的串口通讯原理不赘述,SerialPort类具体使用也不介绍,有需要可以直接复制整个串口通讯类代码,在前台代码中调用即可。
两个方式在打开串口、关闭串口、发送数据都一样,只在接收数据部分有区别。

1.发送完数据立即读取缓冲区数据,并返回接受数据。

此方法使用数据传输安全性高的场景,当发送接收失败、通讯超时等问题会提示异常。

DataSendAndRead方法是同步方法,当数据接收和发送是按顺序完成的。

在DataSendAndRead方法中,接收的字节数组可根据实际情况解析成相应数据格式,当前格式已解析为十六进制。

internal class SerialPortComm
{
    private SerialPort serialPort = new SerialPort();
    private AutoResetEvent _receiveEvent = new AutoResetEvent(false);  //用于同步主线程和事件处理程序
    private byte[] _receivedData = null;  //存储接收到的数据
    private object _syncRoot = new object();  //用于线程同步的锁对象
    public SerialPortComm()
    {
        serialPort.DataReceived += SerialPort_DataReceived;  //订阅serialPort对象的DataReceived事件
    }

    #region 打开串口
    /// <summary>
    /// 打开串口
    /// </summary>
    /// <param name="PortName">串口号</param>
    /// <param name="Baud">波特率</param>
    /// <param name="Data">数据位</param>
    /// <param name="Parity">校验位</param>
    /// <param name="Stop">停止位</param>
    /// <returns>正确返回true,错误返回false和相应错误提示。</returns>
    public ReMsg OpenPort(string PortName, string Baud, string Data, string Parity, string Stop)
    {
        #region 检测通讯设置是否正确
        if (PortName == "")
        {
            return new ReMsg(false, "未检测到串口号数据!");
        }
        if (Baud == "")
        {
            return new ReMsg(false, "未检测到波特率数据!");
        }
        if (Data == "")
        {
            return new ReMsg(false, "未检测到数据位长度数据!");
        }
        if (Parity == "")
        {
            return new ReMsg(false, "未检测到校验位数据!");
        }
        if (Stop == "")
        {
            return new ReMsg(false, "未检测到停止位数据!");
        }
        #endregion

        #region 检测串口号是否存在
        string[] ArryPort = SerialPort.GetPortNames();
        int id = Array.IndexOf(ArryPort, PortName);
        if (id == -1)
        {
            return new ReMsg(false, "当前计算机未检测到指定串口" + PortName + "!");
        }
        #endregion

        #region 检测串口是否已打开
        if (serialPort.IsOpen)
        {
            serialPort.Close();
        }
        #endregion 

        try
        {
            serialPort.PortName = PortName;
            serialPort.BaudRate = Convert.ToInt32(Baud);
            serialPort.DataBits = Convert.ToInt32(Data);
            switch (Parity)
            {
                case "N":
                    serialPort.Parity = System.IO.Ports.Parity.None;
                    break;
                case "O":
                    serialPort.Parity = System.IO.Ports.Parity.Odd;
                    break;
                case "E":
                    serialPort.Parity = System.IO.Ports.Parity.Even;
                    break;
                case "M":
                    serialPort.Parity = System.IO.Ports.Parity.Mark;
                    break;
                case "S":
                    serialPort.Parity = System.IO.Ports.Parity.Space;
                    break;
            }
            switch (Stop)
            {
                case "0":
                    serialPort.StopBits = StopBits.None;
                    break;
                case "1":
                    serialPort.StopBits = StopBits.One;
                    break;
                case "1.5":
                    serialPort.StopBits = StopBits.OnePointFive;
                    break;
                case "2":
                    serialPort.StopBits = StopBits.Two;
                    break;
            }
            serialPort.Open();
            return new ReMsg();
        }
        catch (IOException)
        {
            return new ReMsg(false, "串口被占用!");
        }
        catch (UnauthorizedAccessException)
        {
            return new ReMsg(false, "串口被占用,但没有足够的权限访问!");
        }
        catch (Exception ex)
        {
            return new ReMsg(false, ex.ToString());
        }
    }
    #endregion

    #region 关闭串口
    /// <summary>
    /// 关闭串口
    /// </summary>
    /// <returns>正确返回true,错误返回false和相应错误提示。</returns>
    public ReMsg ClosePort()
    {
        try
        {
            serialPort.Close();
            return new ReMsg();
        }
        catch (Exception ex)
        {
            return new ReMsg(false, ex.ToString());
        }
    }
    #endregion

    #region 发送数据
    /// <summary>
    /// 发送数据,以原始字符串(ASCII或UTF-8)发送
    /// </summary>
    /// <param name="Data">要发送字符串</param>
    /// <returns></returns>
    public ReMsg DataSend(string Data)
    {
        try
        {
            serialPort.Write(Data);
            return new ReMsg();
        }
        catch (Exception ex)
        {
            return new ReMsg(false, ex.ToString());
        }
    }

    /// <summary>
    /// 发送数据,以字节发送十六进制数据
    /// </summary>
    /// <param name="Data">要发送字符串</param>
    /// <returns></returns>
    public ReMsg DataSendByHex(string Data)
    {
        try
        {
            // 去除可能存在的空格,确保字符串只包含十六进制字符
            Data = Data.Replace(" ", "");
            byte[] byteArray = HexStringToByteArray(Data);
            serialPort.Write(byteArray, 0, byteArray.Length);
            return new ReMsg();
        }
        catch (Exception ex)
        {
            return new ReMsg(false, ex.ToString());
        }
    }

    /// <summary>
    /// 十六进制字符串转字节数组
    /// </summary>
    /// <param name="hex">十六进制字符串</param>
    /// <returns></returns>
    /// <exception cref="ArgumentNullException"></exception>
    /// <exception cref="FormatException"></exception>
    public static byte[] HexStringToByteArray(string hex)
    {
        // 确保输入字符串不为空或null
        if (string.IsNullOrEmpty(hex))
        {
            throw new ArgumentNullException(nameof(hex), "输入的十六进制字符串不能为空。");
        }

        // 去除可能存在的空格,确保字符串只包含十六进制字符
        hex = hex.Replace(" ", "");

        // 检查字符串长度是否为偶数
        if (hex.Length % 2 != 0)
        {
            throw new FormatException("输入的十六进制字符串长度必须为偶数。");
        }

        int byteCount = hex.Length / 2;
        byte[] byteArray = new byte[byteCount];

        for (int i = 0; i < byteCount; i++)
        {
            // 每次取两个字符转换为一个字节
            string hexChunk = hex.Substring(i * 2, 2);
            byteArray[i] = Convert.ToByte(hexChunk, 16);
        }
        return byteArray;
    }
    #endregion

    #region 发送数据返回接收的数据
    /// <summary>
    /// 发送数据,返回接收数据
    /// </summary>
    /// <param name="data">要发送字符串</param>
    /// <returns></returns>
    public ReMsg DataSendAndRead(string data)
    {
        try
        {
            // 清空缓冲区 
            if (serialPort.IsOpen)
            {
                serialPort.DiscardInBuffer();  // 清空接收缓冲区 
                serialPort.DiscardOutBuffer(); // 清空发送缓冲区 
            }
            else
            {
                return new ReMsg(false, "串口未打开!");
            }

            ReMsg sendResult = DataSendByHex(data);
            if (!sendResult.Result)
            {
                return sendResult;
            }

            bool received = WaitForData(TimeSpan.FromSeconds(2));  //设置超时等待时间2s
            if (!received)
            {
                return new ReMsg(false, "接收超时!");
            }

            //将字节转换为十六进制字符串显示
            StringBuilder hexStringBuilder = new StringBuilder(_receivedData.Length * 2); // 每个字节两位十六进制,所以总长度是字节数组长度的两倍
            foreach (byte b in _receivedData)
            {
                hexStringBuilder.Append(b.ToString("X2"));
            }
            string hexString = hexStringBuilder.ToString();
            return new ReMsg(true, hexString);
        }
        catch (Exception ex)
        {
            return new ReMsg(false, ex.Message);
        }
    }

    /// <summary>
    /// 等待接收数据
    /// </summary>
    /// <param name="timeout">超时时间</param>
    /// <returns>是否接收到数据</returns>
    private bool WaitForData(TimeSpan timeout)
    {
        bool signaled = _receiveEvent.WaitOne(timeout);
        if (!signaled)
        {
            return false;
        }

        lock (_syncRoot)
        {
            if (_receivedData == null || _receivedData.Length == 0)
            {
                return false;
            }
        }
        return true;
    }
    #endregion

    #region 接收数据
    /// <summary>
    /// serialPort的DataReceived事件,负责串口数据接收。
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
    {
        SerialPort sp = (SerialPort)sender;
        int bytesToRead = sp.BytesToRead;
        byte[] readBuffer = new byte[bytesToRead];
        sp.Read(readBuffer, 0, bytesToRead);
        //使用lock关键字确保对_receivedData的操作是线程安全的。
        lock (_syncRoot)
        {
            _receivedData = readBuffer.Clone() as byte[];
        }
        _receiveEvent.Set();  //通知主线程数据已经接收完毕。
    }
    #endregion 
}

2.只发送数据,在datareceived事件中处理接收数据

此方法使用轮询指定命令等场景。

在封装的通讯类中创建一个DataReceivedEvent公共事件,通过DataReceivedEvent事件将类内部datareceived事件接收的数据发送到调用该类的前台进行处理。

 class SerialComm
 {
     SerialPort serialPort = new SerialPort();
     public event Action<byte[]> DataReceivedEvent; // 声明一个公共事件
     public SerialComm()
     {
         serialPort.DataReceived += SerialPort_DataReceived;  //订阅serialPort对象的DataReceived事件
     }

     #region 打开串口
     /// <summary>
     /// 打开串口
     /// </summary>
     /// <param name="PortName">串口号</param>
     /// <param name="Baud">波特率</param>
     /// <param name="Data">数据位</param>
     /// <param name="Parity">校验位</param>
     /// <param name="Stop">停止位</param>
     /// <returns>正确返回true,错误返回false和相应错误提示。</returns>
     public ReMsg OpenPort(string PortName,string Baud,string Data,string Parity,string Stop)
     {
         #region 检测通讯设置是否正确
         if (PortName == "")
         {
             return new ReMsg(false, "未检测到串口号数据!");
         }
         if (Baud == "")
         {
             return new ReMsg(false, "未检测到波特率数据!");
         }
         if (Data == "")
         {
             return new ReMsg(false, "未检测到数据位长度数据!");
         }
         if (Parity == "")
         {
             return new ReMsg(false, "未检测到校验位数据!");
         }
         if (Stop == "")
         {
             return new ReMsg(false, "未检测到停止位数据!");
         }
         #endregion

         #region 检测串口号是否存在
         string[] ArryPort = SerialPort.GetPortNames();
         int id = Array.IndexOf(ArryPort, PortName);
         if (id == -1)
         {
             return new ReMsg(false, "当前计算机未检测到指定串口" + PortName +"!");
         }
         #endregion

         #region 检测串口是否已打开
         if (serialPort.IsOpen)
         {
             serialPort.Close();
         }
         #endregion 

         try
         {
             serialPort.PortName = PortName;
             serialPort.BaudRate = Convert.ToInt32(Baud);
             serialPort.DataBits = Convert.ToInt32(Data);
             switch (Parity)
             {
                 case "N":
                     serialPort.Parity = System.IO.Ports.Parity.None;
                     break;
                 case "O":
                     serialPort.Parity = System.IO.Ports.Parity.Odd;
                     break;
                 case "E":
                     serialPort.Parity = System.IO.Ports.Parity.Even;
                     break;
                 case "M":
                     serialPort.Parity = System.IO.Ports.Parity.Mark;
                     break;
                 case "S":
                     serialPort.Parity = System.IO.Ports.Parity.Space;
                     break;
             }
             switch (Stop)
             {
                 case "0":
                     serialPort.StopBits = StopBits.None;
                     break;
                 case "1":
                     serialPort.StopBits = StopBits.One;
                     break;
                 case "1.5":
                     serialPort.StopBits = StopBits.OnePointFive;
                     break;
                 case "2":
                     serialPort.StopBits = StopBits.Two;
                     break;
             }
             serialPort.Open();
             return new ReMsg();
         }
         catch (IOException)
         {
             return new ReMsg(false, "串口被占用!");
         }
         catch (UnauthorizedAccessException)
         {
             return new ReMsg(false, "串口被占用,但没有足够的权限访问!");
         }
         catch(Exception ex)
         {
             return new ReMsg(false, ex.ToString());
         }
     }
     #endregion

     #region 关闭串口
     /// <summary>
     /// 关闭串口
     /// </summary>
     /// <returns>正确返回true,错误返回false和相应错误提示。</returns>
     public ReMsg ClosePort()
     {
         try
         {
             serialPort.Close();
             return new ReMsg();
         }
         catch (Exception ex)
         {
             return new ReMsg(false,ex.ToString());
         }
     }
     #endregion

     #region 发送数据
     /// <summary>
     /// 发送数据,以原始字符串(ASCII或UTF-8)发送
     /// </summary>
     /// <param name="Data">要发送字符串</param>
     /// <returns></returns>
     public ReMsg DataSend(string Data)
     {
         try
         {
             serialPort.Write(Data);
             return new ReMsg();
         }
         catch (Exception ex)
         {
             return new ReMsg(false,ex.ToString());
         }
     }

     private readonly object _serialPortLock = new object(); // 定义一个私有的锁对象

     /// <summary>
     /// 发送数据,以字节发送十六进制数据
     /// </summary>
     /// <param name="Data">要发送字符串</param>
     /// <returns></returns>
     public ReMsg DataSendByHex(string Data)
     {
         try
         {
             lock (_serialPortLock)
             {
                 // 去除可能存在的空格,确保字符串只包含十六进制字符
                 Data = Data.Replace(" ", "");
                 byte[] byteArray = HexStringToByteArray(Data);
                 serialPort.Write(byteArray, 0, byteArray.Length);
             }
             return new ReMsg();
         }
         catch (Exception ex)
         {
             return new ReMsg(false,ex.ToString());
         }
     }

     /// <summary>
     /// 十六进制字符串转字节数组
     /// </summary>
     /// <param name="hex">十六进制字符串</param>
     /// <returns></returns>
     /// <exception cref="ArgumentNullException"></exception>
     /// <exception cref="FormatException"></exception>
     public static byte[] HexStringToByteArray(string hex)
     {
         // 确保输入字符串不为空或null
         if (string.IsNullOrEmpty(hex))
         {
             throw new ArgumentNullException(nameof(hex), "输入的十六进制字符串不能为空。");
         }

         // 去除可能存在的空格,确保字符串只包含十六进制字符
         hex = hex.Replace(" ", "");

         // 检查字符串长度是否为偶数
         if (hex.Length % 2 != 0)
         {
             throw new FormatException("输入的十六进制字符串长度必须为偶数。");
         }

         int byteCount = hex.Length / 2;
         byte[] byteArray = new byte[byteCount];

         for (int i = 0; i < byteCount; i++)
         {
             // 每次取两个字符转换为一个字节
             string hexChunk = hex.Substring(i * 2, 2);
             byteArray[i] = Convert.ToByte(hexChunk, 16);
         }
         return byteArray;
     }
     #endregion

     #region 接收数据
     /// <summary>
     /// serialPort的DataReceived事件,负责串口数据接收。
     /// </summary>
     /// <param name="sender"></param>
     /// <param name="e"></param>
     private void SerialPort_DataReceived(object sender, SerialDataReceivedEventArgs e)
     {
         //接收默认ASCII码字符串
         //SerialPort sp = (SerialPort)sender;
         //string indata = sp.ReadExisting();
         触发DataReceivedEvent事件,传递接收到的数据
         //DataReceivedEvent?.Invoke(indata);

         //接收字节
         SerialPort sp = (SerialPort)sender;
         int bytesToRead = sp.BytesToRead;
         byte[] readBuffer = new byte[bytesToRead];
         sp.Read(readBuffer, 0, bytesToRead);
         DataReceivedEvent?.Invoke(readBuffer);
     }
     #endregion 
 }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值