C#实现高效稳定的串口通讯
前言
串口通讯是嵌入式开发、工业自动化、物联网等领域中非常重要的一种通信方式。在C#开发中,虽然.NET框架提供了System.IO.Ports.SerialPort类来实现串口通信,但直接使用它往往需要处理很多底层细节,如多线程、缓存管理、错误处理等。本文将分享如何在C#中实现高效稳定的串口通讯。
串口通讯的挑战
在实现串口通讯时,我们通常会面临以下挑战:
- 主线程阻塞:串口操作是IO操作,如果在主线程中直接执行,可能会导致UI卡顿
- 数据丢失:串口数据传输速度与应用程序处理速度不匹配时,容易导致数据丢失
- 错误处理:串口连接不稳定时,需要妥善处理各种异常情况
- 参数配置:串口参数(波特率、数据位、停止位、校验位等)的管理和配置
- 动态检测:需要实时检测串口的连接状态和列表变化
设计思路
为了应对上述挑战,主要包括以下设计思路:
1. 多线程架构
采用多线程架构,将串口操作分为三个独立线程:
- 接收缓存线程:负责从串口读取原始数据并缓存
- 接收数据线程:负责处理缓存数据并触发事件
- 发送数据线程:负责将待发送数据写入串口
这种设计有效避免了串口操作阻塞主线程,提高了应用程序的响应速度和稳定性。
2. 队列缓存机制
使用线程安全的队列实现数据的缓存和处理,确保数据在多线程环境下的安全传输:
queueSerialCacheReceived:缓存从串口读取的原始数据queueSerialDataReceived:缓存处理后的完整数据帧queueSerialDataWrite:缓存待发送的数据
3. 事件驱动模型
采用事件驱动模型处理串口数据和错误:
EventSerialPortDataReceivedProcess:数据接收事件EventSerialPortError:错误处理事件
这种设计使得库的使用更加灵活,用户可以根据需要自定义数据处理和错误处理逻辑。
4. 动态串口检测
提供实时串口检测功能,支持两种检测模式:
- 线程检测模式:使用后台线程定期检测串口列表变化
- 定时器检测模式:使用定时器定期检测串口列表变化
当串口列表发生变化时,会自动触发事件通知应用程序。
5. 丰富的数据转换工具
提供多种数据格式转换方法:
- 字节数组与字符串之间的转换
- 16进制字符串与字节数组之间的转换
- ASCII编码与字符串之间的转换
- 数据有效性校验
核心实现
1. 多线程串口操作
// 初始化线程
private void InitThread()
{
//初始化接收缓存线程
threadSerialCacheReceived = new Thread(new ThreadStart(ThreadSerialCacheReceivedFunction));
threadSerialCacheReceived.IsBackground = true;
//初始化接收数据线程
threadSerialDataReceived = new Thread(new ThreadStart(ThreadSerialDataReceivedFunction));
threadSerialDataReceived.IsBackground = true;
//初始化发送数据线程
threadSerialDataWrite = new Thread(new ThreadStart(ThreadSerialDataWriteFunction));
threadSerialDataWrite.IsBackground = true;
}
// 打开线程
private void OpenThread()
{
InitThread();
threadSerialCacheReceived.Start();
threadSerialDataReceived.Start();
threadSerialDataWrite.Start();
}
2. 数据接收处理
// 串口接收事件函数
private void SerialDataReceivedEventFunction(object sender, SerialDataReceivedEventArgs e)
{
try
{
byte[] readBuffer = new byte[sPort.BytesToRead];
int count = sPort.Read(readBuffer, 0, readBuffer.Length);
lock (queueSerialCacheReceived.SyncRoot)
{
for (int i = 0; i < count; i++)
{
queueSerialCacheReceived.Enqueue(readBuffer[i]);
}
}
}
catch (Exception error)
{
EventSerialPortError(this, enumSerialError.ReceivedError, error.Message);
}
}
// 接收缓存线程函数
private void ThreadSerialCacheReceivedFunction()
{
List<byte> listData = new List<byte>();
while (sPort.IsOpen)
{
int cacheLength = queueSerialCacheReceived.Count;
if (cacheLength > 0)
{
for (int i = 0; i < cacheLength; i++)
{
listData.Add((byte)queueSerialCacheReceived.Dequeue());
if (listData.Count >= SerialReceviedLengthMax)
{
lock (queueSerialCacheReceived.SyncRoot)
{
queueSerialDataReceived.Enqueue(listData.ToArray()); //转为帧队列
}
listData.Clear();
}
}
}
else if (listData.Count > 0)
{
lock (queueSerialCacheReceived.SyncRoot)
{
queueSerialDataReceived.Enqueue(listData.ToArray()); //转为帧队列
}
listData.Clear();
}
Thread.Sleep(SerialReceviedTimeInterval);
}
EventSerialPortError(this, enumSerialError.LinkError, "端口连接断开或未开启端口!");
queueSerialCacheReceived.Clear();
}
// 接收数据线程函数
private void ThreadSerialDataReceivedFunction()
{
while (sPort.IsOpen)
{
lock (queueSerialDataReceived.SyncRoot)
{
if (queueSerialDataReceived.Count > 0)
{
byte[] byteData = (byte[])queueSerialDataReceived.Dequeue();
EventSerialPortDataReceivedProcess(this, byteData); //触发事件
continue;
}
}
Thread.Sleep(SerialReceviedTimeInterval);
}
lock (queueSerialDataReceived.SyncRoot)
{
queueSerialDataReceived.Clear();
}
}
3. 数据发送处理
// 发送数据线程函数
private void ThreadSerialDataWriteFunction()
{
byte[] tempbyte=null;
while (sPort.IsOpen)
{
lock (queueSerialDataWrite.SyncRoot)
{
if (queueSerialDataWrite.Count > 0)
{
byte[] byteData = (byte[])queueSerialDataWrite.Dequeue();
try
{
sPort.Write(byteData, 0, byteData.Length);
tempbyte = byteData;
}
catch (Exception e)
{
EventSerialPortError(this, enumSerialError.WriteError, e.Message);
return;
}
}
}
Thread.Sleep(SerialWriteTimeInterval);
}
lock (queueSerialDataWrite.SyncRoot)
{
queueSerialDataWrite.Clear();
}
}
// 串口发送数据
public void Write(byte[] arrData)
{
if (arrData.Length > 0)
{
if (sPort.IsOpen)
{
lock (queueSerialDataWrite.SyncRoot)
{
queueSerialDataWrite.Enqueue((byte[])arrData.Clone());
}
}
else
{
EventSerialPortError(this, enumSerialError.LinkError, "端口未开启!");
}
}
}
4. 动态串口检测
// 检测串口列表并处理
private void DetectSerialPortListProcess()
{
//获取当前串口列表
nowSerialPortList.Clear();
foreach (string item in SerialPort.GetPortNames())
{
nowSerialPortList.Add(item.ToString());
}
//串口列表比对
if (CompareArray(nowSerialPortList.ToArray(), bakSerialPortList.ToArray()) == false)
{
//获取串口信息
GetSerialPortInfo(DicSerialPortInfo);
//更新备份列表
bakSerialPortList.Clear();
foreach (string item in nowSerialPortList)
{
int index = StrSerialPortDefaultInfo.ToList().IndexOf(DicSerialPortInfo[item]);
if (DicSerialPortInfo.ContainsKey(item) && (index >= 0))
{
bakSerialPortList.Insert(0, item);
}
else
{
bakSerialPortList.Add(item);
}
}
//触发事件
EventSerialPortList(bakSerialPortList.ToArray());
}
}
5. 数据转换工具
// byte[]转成string
public static string ToString(byte[] byteData)
{
return System.Text.Encoding.Default.GetString(byteData);
}
// string类型转成byte[]
public static byte[] ToByteArray(string strData)
{
return System.Text.Encoding.Default.GetBytes(strData);
}
// byte[]转16进制格式string
public static string ToHexString(byte[] bytes)
{
string returnStr = "";
if (bytes != null)
{
for (int i = 0; i < bytes.Length; i++)
{
returnStr += bytes[i].ToString("X2") + " ";
}
}
return returnStr;
}
// 字符串转16进制字节数组
public static byte[] ToHexByteArray(string hexString)
{
hexString = CheakHexString(hexString);
hexString = hexString.Replace(" ", "");
byte[] returnBytes = new byte[hexString.Length / 2];
for (int i = 0; i < returnBytes.Length; i++)
returnBytes[i] = Convert.ToByte(hexString.Substring(i * 2, 2), 16);
return returnBytes;
}
完整实现示例
1. 核心类设计
SerialPortHelper类
public class SerialPortHelper
{
#region 常量
public const Int32 SERIAL_RECEIVED_TIME_INTERVAL = 20; //接收数据间隔
public const Int32 SERIAL_RECEIVED_LENGTH_MAX = 128; //接收帧最大长度
public const Int32 SERIAL_WRITE_TIME_INTERVAL = 100; //发送数据帧时间间隔
#endregion
#region 内部变量
private SerialPort sPort;
private Thread threadSerialCacheReceived;
private Thread threadSerialDataReceived;
private Thread threadSerialDataWrite;
private Queue queueSerialCacheReceived;
private Queue queueSerialDataReceived;
private Queue queueSerialDataWrite;
#endregion
#region 字段
private ConfigComType _configSerialPort;
private Int32 _serialReceviedTimeInterval = SERIAL_RECEIVED_TIME_INTERVAL;
private Int32 _serialReceviedLengthMax = SERIAL_RECEIVED_LENGTH_MAX;
private Int32 _serialWriteTimeInterval = SERIAL_WRITE_TIME_INTERVAL;
private String _serialMark = "";
#endregion
#region 属性
public int SerialReceviedTimeInterval
{
get { return _serialReceviedTimeInterval; }
set { _serialReceviedTimeInterval = value; }
}
public int SerialReceviedLengthMax
{
get { return _serialReceviedLengthMax; }
set { _serialReceviedLengthMax = value; }
}
public int SerialWriteTimeInterval
{
get { return _serialWriteTimeInterval; }
set { _serialWriteTimeInterval = value; } }
public string SerialMark
{
get { return _serialMark; }
set { _serialMark = value; }
}
public bool IsOpen
{
get { return sPort.IsOpen; }
}
public ConfigComType ConfigSerialPort
{
get {return _configSerialPort; }
set {_configSerialPort = value;}
}
/// <summary>
/// 提供一个外部可访问串口接口
/// </summary>
public SerialPort SPort
{
get {return sPort; }
set {sPort = value; }
}
#endregion
#region 构造函数
public SerialPortHelper()
{
//Control.CheckForIllegalCrossThreadCalls = false; //不检测跨线程访问
InitQueue();
InitSerialPort();
InitThread();
}
public SerialPortHelper(ConfigComType config) : this()
{
ConfigSerialPort = config;
}
public SerialPortHelper(ConfigComType config, DelegateSerialPortDataReceivedProcessEvent e) : this(config)
{
BindSerialPortDataReceivedProcessEvent(e);
}
#endregion
#region 初始化函数
/// <summary>
/// 初始化串口
/// </summary>
private void InitSerialPort()
{
//初始化串口
sPort = new SerialPort();
sPort.DataReceived += SerialDataReceivedEventFunction;
sPort.DtrEnable = true;
sPort.RtsEnable = true;
sPort.Close();
}
/// <summary>
/// 初始化线程
/// </summary>
private void InitThread()
{
//初始化接收缓存线程
threadSerialCacheReceived = new Thread(new ThreadStart(ThreadSerialCacheReceivedFunction));
threadSerialCacheReceived.IsBackground = true;
//初始化接收数据线程
threadSerialDataReceived = new Thread(new ThreadStart(ThreadSerialDataReceivedFunction));
threadSerialDataReceived.IsBackground = true;
//初始化发送数据线程
threadSerialDataWrite = new Thread(new ThreadStart(ThreadSerialDataWriteFunction));
threadSerialDataWrite.IsBackground = true;
}
/// <summary>
/// 初始化队列(线程安全)
/// </summary>
private void InitQueue()
{
//初始化接收缓存队列
queueSerialCacheReceived = Queue.Synchronized(new Queue());
//初始化接收数据队列
queueSerialDataReceived = Queue.Synchronized(new Queue());
//初始化发送数据队列
queueSerialDataWrite = Queue.Synchronized(new Queue());
}
#endregion
#region 串口接收事件函数
private void SerialDataReceivedEventFunction(object sender, SerialDataReceivedEventArgs e)
{
try
{
byte[] readBuffer = new byte[sPort.BytesToRead];
int count = sPort.Read(readBuffer, 0, readBuffer.Length);
lock (queueSerialCacheReceived.SyncRoot)
{
for (int i = 0; i < count; i++)
{
queueSerialCacheReceived.Enqueue(readBuffer[i]);
}
}
}
catch (Exception error)
{
EventSerialPortError(this, enumSerialError.ReceivedError, error.Message);
}
}
#endregion
#region 线程处理函数
/// <summary>
/// 打开线程
/// </summary>
private void OpenThread()
{
InitThread();
threadSerialCacheReceived.Start();
threadSerialDataReceived.Start();
threadSerialDataWrite.Start();
}
/// <summary>
/// 接收缓存线程函数
/// </summary>
private void ThreadSerialCacheReceivedFunction()
{
List<byte> listData = new List<byte>();
while (sPort.IsOpen)
{
int cacheLength = queueSerialCacheReceived.Count;
if (cacheLength > 0)
{
for (int i = 0; i < cacheLength; i++)
{
listData.Add((byte)queueSerialCacheReceived.Dequeue());
if (listData.Count >= SerialReceviedLengthMax)
{
lock (queueSerialCacheReceived.SyncRoot)
{
queueSerialDataReceived.Enqueue(listData.ToArray()); //转为帧队列
}
listData.Clear();
}
}
}
else if (listData.Count > 0)
{
lock (queueSerialCacheReceived.SyncRoot)
{
queueSerialDataReceived.Enqueue(listData.ToArray()); //转为帧队列
}
listData.Clear();
}
Thread.Sleep(SerialReceviedTimeInterval);
}
EventSerialPortError(this, enumSerialError.LinkError, "端口连接断开或未开启端口!");
queueSerialCacheReceived.Clear();
}
/// <summary>
/// 接收数据线程函数
/// </summary>
private void ThreadSerialDataReceivedFunction()
{
while (sPort.IsOpen)
{
lock (queueSerialDataReceived.SyncRoot)
{
if (queueSerialDataReceived.Count > 0)
{
byte[] byteData = (byte[])queueSerialDataReceived.Dequeue();
EventSerialPortDataReceivedProcess(this, byteData); //触发事件
continue;
}
}
Thread.Sleep(SerialReceviedTimeInterval);
}
lock (queueSerialDataReceived.SyncRoot)
{
queueSerialDataReceived.Clear();
}
}
/// <summary>
/// 发送数据线程函数
/// </summary>
private void ThreadSerialDataWriteFunction()
{
byte[] tempbyte=null;
while (sPort.IsOpen)
{
lock (queueSerialDataWrite.SyncRoot)
{
if (queueSerialDataWrite.Count > 0)
{
byte[] byteData = (byte[])queueSerialDataWrite.Dequeue();
try
{
sPort.Write(byteData, 0, byteData.Length);
tempbyte = byteData;
//Console.WriteLine("发送数据:" + SerialData.ToHexString(byteData) + "\t时间:" + DateTime.Now.ToString("yyyy-MM-dd hh:mm:ss fff"));
}
catch (Exception e)
{
EventSerialPortError(this, enumSerialError.WriteError, e.Message);
return;
}
}
}
Thread.Sleep(SerialWriteTimeInterval);
}
lock (queueSerialDataWrite.SyncRoot)
{
queueSerialDataWrite.Clear();
}
}
/// <summary>
/// 比较两个字节数组是否相等
/// </summary>
/// <param name="b1">byte数组1</param>
/// <param name="b2">byte数组2</param>
/// <returns>是否相等</returns>
private bool ByteEquals(byte[] b1, byte[] b2)
{
if (b1 == null || b2 == null) return true;
//if (b1.Last != b2.Length) return false;
for (int i = 0; i < 3; i++)
if (b1[i] != b2[i])
return true;
return false;
}
#endregion
#region 数据接收处理事件
/// <summary>
/// 定义委托
/// </summary>
/// <param name="arrDataReceived">接收到的数据帧</param>
public delegate void DelegateSerialPortDataReceivedProcessEvent(object sender, byte[] arrDataReceived);
/// <summary>
/// 定义事件
/// </summary>
public event DelegateSerialPortDataReceivedProcessEvent EventSerialPortDataReceivedProcess;
/// <summary>
/// 绑定接收事件
/// </summary>
/// <param name="e">接收处理函数</param>
public void BindSerialPortDataReceivedProcessEvent(DelegateSerialPortDataReceivedProcessEvent e)
{
EventSerialPortDataReceivedProcess = e;
}
#endregion
#region 串口异常事件
/// <summary>
/// 定义委托
/// </summary>
/// <param name="numError">错误代号</param>
/// <param name="strError">错误信息</param>
public delegate void DelegateSerialPortErrorEvent(object sender, enumSerialError enumError, string strError);
/// <summary>
/// 定义事件
/// </summary>
public event DelegateSerialPortErrorEvent EventSerialPortError;
/// <summary>
/// 绑定串口错误事件
/// </summary>
/// <param name="e">事件处理函数</param>
public void BindSerialPortErrorEvent(DelegateSerialPortErrorEvent e)
{
EventSerialPortError = e;
}
#endregion
#region 数据发送处理
/// <summary>
/// 串口发送数据
/// </summary>
/// <param name="arrData">数据内容</param>
public void Write(byte[] arrData)
{
if (arrData.Length > 0)
{
if (sPort.IsOpen)
{
lock (queueSerialDataWrite.SyncRoot)
{
queueSerialDataWrite.Enqueue((byte[])arrData.Clone());
}
}
else
{
EventSerialPortError(this, enumSerialError.LinkError, "端口未开启!");
}
}
}
/// <summary>
/// 清空缓存
/// </summary>
public void DiscardInBuffer() {
if (sPort.IsOpen)
{
//sPort.DiscardInBuffer();
//queueSerialDataWrite.Clear();
//queueSerialCacheReceived.Clear();
//queueSerialDataReceived.Clear();
}
}
/// <summary>
/// 串口发送数据
/// </summary>
/// <param name="strData">数据内容</param>
public void Write(string strData)
{
if (strData.Length > 0)
{
Write(SerialData.ToByteArray(strData));
}
}
/// <summary>
/// 串口发送Byte数据
/// </summary>
/// <param name="arrData">Byte数组数据</param>
public void WriteByte(byte[] arrData)
{
Write(arrData);
}
/// <summary>
/// 串口发送Char数据
/// </summary>
/// <param name="arrData">字符数组数据</param>
public void WriteChar(char[] arrData)
{
Write(Encoding.Default.GetBytes(arrData));
}
/// <summary>
/// 串口发送字符串数据
/// </summary>
/// <param name="strData">字符串数据</param>
public void WriteString(string strData)
{
Write(strData);
}
/// <summary>
/// 串口发送16进制字符串数据
/// </summary>
/// <param name="hexData">16进制字符串数据</param>
public void WriteHexString(string hexData)
{
if (hexData.Length > 0)
{
Write(SerialData.ToHexByteArray(hexData));
}
}
#endregion
#region 公共函数
/// <summary>
/// 开启端口
/// </summary>
/// <param name="strError">错误信息</param>
/// <returns>是否开启成功</returns>
public bool OpenCom(out string strError)
{
//检测端口合法性
if (DetectCom.IsCom(ConfigSerialPort.PortName) == false)
{
strError = "端口号不存在,无法开启端口。";
return false;
}
else if (EventSerialPortDataReceivedProcess == null)
{
strError = "没有绑定接收数据事件";
return false;
}
else
{
//开启端口
try
{
//写入串口配置
sPort.PortName = ConfigSerialPort.PortName;
sPort.BaudRate = ConfigSerialPort.BaudRate;
sPort.DataBits = ConfigSerialPort.DataBits;
sPort.StopBits = (System.IO.Ports.StopBits)ConfigSerialPort.StopBits;
sPort.Parity = (System.IO.Ports.Parity)ConfigSerialPort.Parity;
//尝试关闭串口
this.CloseCom(out strError);
sPort.Open();
OpenThread();
strError = string.Format("打开{0}串口", sPort.PortName);
return sPort.IsOpen;
}
catch (Exception e)
{
strError = e.Message;
return false;
}
}
}
/// <summary>
/// 开启指定配置的端口
/// </summary>
/// <param name="config">配置</param>
/// <param name="strError">错误信息</param>
/// <returns>是否开启成功</returns>
public bool OpenCom(ConfigComType config, out string strError)
{
ConfigSerialPort = config;
string str = string.Empty;
bool ret = OpenCom(out str);
strError = str;
return ret;
}
/// <summary>
/// 关闭串口
/// </summary>
/// <param name="strError">错误信息</param>
public void CloseCom(out string strError)
{
try
{
if (SPort.IsOpen)
{
sPort.Close();
strError = "串口关闭成功!";
}
else {
strError = "串口未打开!";
}
}
catch (Exception e)
{
strError = e.Message;
}
}
#endregion
}
#region 枚举
/// <summary>
/// 串口错误枚举
/// </summary>
public enum enumSerialError
{
LinkError,
WriteError,
ReceivedError
}
#endregion
2. 使用示例
// 创建SerialPortHelper实例
SerialPortHelper serialPort = new SerialPortHelper();
// 绑定事件处理函数
serialPort.BindSerialPortDataReceivedProcessEvent(new SerialPortHelper.DelegateSerialPortDataReceivedProcessEvent(SerialPortDataReceivedProcess));
serialPort.BindSerialPortErrorEvent(new SerialPortHelper.DelegateSerialPortErrorEvent(SerialPortErrorProcess));
// 配置串口参数
ConfigComType config = new ConfigComType();
config.PortName = "COM1";
config.BaudRate = 9600;
config.DataBits = 8;
config.StopBits = StopBits.One;
config.Parity = Parity.None;
serialPort.ConfigSerialPort = config;
// 打开串口
string errorMessage;
bool result = serialPort.OpenCom(out errorMessage);
if (result)
{
Console.WriteLine("串口打开成功!");
}
else
{
Console.WriteLine("串口打开失败:" + errorMessage);
}
// 发送数据
byte[] data = new byte[] { 0x01, 0x02, 0x03 };
serialPort.Write(data);
// 接收数据处理
private void SerialPortDataReceivedProcess(object sender, byte[] arrDataReceived)
{
string receivedData = SerialData.ToString(arrDataReceived);
Console.WriteLine("接收到数据:" + receivedData);
}
// 错误处理
private void SerialPortErrorProcess(object sender, enumSerialError enumError, string strError)
{
switch (enumError)
{
case enumSerialError.LinkError:
Console.WriteLine("连接错误:" + strError);
break;
case enumSerialError.WriteError:
Console.WriteLine("写入错误:" + strError);
break;
case enumSerialError.ReceivedError:
Console.WriteLine("接收错误:" + strError);
break;
}
}
// 关闭串口
string closeError;
serialPort.CloseCom(out closeError);
Console.WriteLine(closeError);

浙公网安备 33010602011771号