在开发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
}