关于Unity在Windows平台使用串口,基础内容百度一下,看一下别人的博客就能学习到,这里说几点,避免自己忘记:
1、Unity2019要使用串口的类SerialPort,需要将Api Copatibility Level选项换成.NET 4.x 。
2、Unity编辑器里面可以直接调试串口,不用导出exe,如果串口打开失败,看看此串口是否可用,以及有没有被占用。
3、如果电脑硬件上没有串口,可以使用串口模拟工具模拟出两个串口,让Unity程序与串口调试助手各连接一个,实现互通。
我的相关代码(有参考别人的代码):(完整工程下载地址在最下面)
以下假设下位机发送来的一次完整消息是以"START"开头,以"END"结尾。(我这是给自己挖坑了,有点不太好处理,不如直接用"\r\n"或者'\0'作为消息结束的标志,这样还好处理一些)。
水平有限,代码写得比较乱(尤其是接收数据那一块),凑合着看吧。
using UnityEngine;
using System.Collections;
using System.IO.Ports;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Text;
/// <summary>
/// 控制串口接收和发送消息
/// </summary>
public class SerialPortControl : MonoBehaviour
{
//定义基本信息
[SerializeField] private string _portName = "COM1";//串口名
[SerializeField] private int _baudRate = 9600;//波特率
[SerializeField] private Parity _parity = Parity.None;//效验位
[SerializeField] private int _dataBits = 8;//数据位
[SerializeField] private StopBits _stopBits = StopBits.One;//停止位
[SerializeField] private Handshake _handshake = Handshake.None;//握手协议
[SerializeField] private bool _dtrEnable = false;
[SerializeField] private bool _rtsEnable = false;
private string START_FLAG = "START";//一条完整消息的起始标志
private string END_FLAG = "END";//一条完整消息的结束标志
/// <summary>
/// 接收数据后的回调,参数是对方传来的消息
/// </summary>
private Action<string> _onRecv;
private SerialPort _serialPort = null;
private Thread recvThread;//数据接收线程
private Thread sendThread;//数据发送线程
private bool _bRunning = false;//线程是否需要继续运行
/// <summary>
/// 消息发送队列
/// </summary>
private Queue<string> _msgSendQueue = new Queue<string>();
/// <summary>
/// 消息接收队列
/// </summary>
private Queue<string> _msgRecvQueue = new Queue<string>();
public void Init(SerialPortConfig config)
{
Init(config.portName, config.baudRate, config.parity, config.dataBits, config.stopBits, config.handshake, config.dtrEnable, config.rtsEnable);
}
/// <summary>
/// 如果不调用初始化,就默认使用Inspector面板中的设置。
/// </summary>
/// <param name="portName"></param>
/// <param name="baudRate"></param>
/// <param name="parity"></param>
/// <param name="dataBits"></param>
/// <param name="stopBits"></param>
/// <param name="handshake"></param>
/// <param name="dtrEnable"></param>
/// <param name="rtsEnable"></param>
public void Init(string portName, int baudRate, Parity parity, int dataBits, StopBits stopBits, Handshake handshake = Handshake.None, bool dtrEnable = false, bool rtsEnable = false)
{
this._portName = portName;
this._baudRate = baudRate;
this._parity = parity;
this._dataBits = dataBits;
this._stopBits = stopBits;
this._handshake = handshake;
this._dtrEnable = dtrEnable;
this._rtsEnable = rtsEnable;
}
/// <summary>
/// 打开串口以及开启读写数据的线程。返回值表示是否成功。
/// </summary>
/// <returns></returns>
public bool Open()
{
if (OpenPort())
{
_msgSendQueue.Clear();
_msgRecvQueue.Clear();
_bRunning = true;
recvThread = new Thread(new ThreadStart(ReceiveDataLoop));
recvThread.Start();
sendThread = new Thread(new ThreadStart(SendDataLoop));
sendThread.Start();
return true;
}
return false;
}
public void Close()
{
if (IsOpen())
{
ClosePort();
}
_bRunning = false;//让线程结束循环,自动结束线程
}
public bool IsOpen()
{
if (_serialPort != null)
{
return _serialPort.IsOpen;
}
return false;
}
/// <summary>
/// 打开串口,返回值表示是否打开成功
/// </summary>
/// <returns></returns>
private bool OpenPort()
{
//创建串口
_serialPort = new SerialPort();
_serialPort.PortName = _portName;
_serialPort.BaudRate = _baudRate;
_serialPort.Parity = _parity;
_serialPort.DataBits = _dataBits;
_serialPort.StopBits = _stopBits;
_serialPort.Handshake = _handshake;
_serialPort.DtrEnable = _dtrEnable;
_serialPort.RtsEnable = _rtsEnable;
//设置读写超时,单位毫秒
_serialPort.ReadTimeout = 500;
_serialPort.WriteTimeout = 500;
//下位机数据一般是ASCII
_serialPort.Encoding = Encoding.ASCII;//其实默认就是这个,不设置也行。ASCII就是只能使用英文字符,数值为0-127里面的字符。
bool success = true;
try
{
_serialPort.Open();
Debug.Log($"打开串口成功:{_portName}");
}
catch (Exception ex)
{
success = false;
Debug.LogError("打开串口失败,原因:" + ex.Message);
}
return success;
}
private bool ClosePort()
{
bool success = true;
try
{
_serialPort.Close();
_serialPort = null;
Debug.Log($"关闭串口成功: {_portName}");
}
catch (Exception ex)
{
success = false;
Debug.LogError("关闭串口失败,原因:" + ex.Message);
}
return success;
}
public void AddReceiveListener(Action<string> listener)
{
_onRecv -= listener;
_onRecv += listener;
}
public void RemoveReceiveListener(Action<string> listener)
{
_onRecv -= listener;
}
private void Update()
{
//这样,接收数据后就在Unity主线程中处理,如果涉及UI显示,就不会出问题了。
HandleReceivedData();
}
/// <summary>
/// 处理已经接收的数据
/// </summary>
private void HandleReceivedData()
{
List<string> msgList = null;
lock (_msgRecvQueue)
{
if (_msgRecvQueue.Count > 0)
{
msgList = new List<string>();
while (_msgRecvQueue.Count > 0)
{
msgList.Add(_msgRecvQueue.Dequeue());
}
}
}
if (msgList != null && msgList.Count > 0)
{
for (int i = 0; i < msgList.Count; i++)
{
_onRecv?.Invoke(msgList[i]);
}
}
}
/// <summary>
/// 接收数据的线程
/// </summary>
private void ReceiveDataLoop()
{
Debug.Log("进入串口接收数据线程");
byte[] recvBuffer = new byte[8 * 1024];
int bytesToRead = 0;//需要读取多少字节
int countTemp = 0;
int totalCount = 0;//已经读取多少字节
bool maybeNoMoreData = false;//“也许”,串口接收缓存区里已经没有数据了
bool noMoreData = false;//确实,串口接收缓存区里已经没有数据了
int sleepTime_Waiting = 30;//没有数据的时候的睡眠时间,单位毫秒
int sleepTime_Receiving = 10;//接收数据的时候的睡眠时间,单位毫秒
int sleepTime = sleepTime_Waiting;
bool receiving = false;//是否接收中
//没有数据时以5毫秒为间隔查询,有数据时以2毫秒为间隔查询。比特率为9600的情况下,大概1点几毫秒传输1个字节,
//当间隔5毫秒的时间没有接到新数据的时候,则说明此次的数据传输完成并读取完成了。
//这种方式,如果下位机发送数据的间隔低于5毫秒,可能会出现连续两次或多次的数据被合在一起作为一次读取,因此就需要规定好数据之间的间隔符或起始结束标志,
//处理数据的时候,按照这些标志将数据隔开。同时也可以将查询间隔缩短。但是,如果下位机一直高频发送数据,并且巧妙地导致上位机一直判断到有数据而没有捕捉到
//数据发送的间隔时间,会导致上位机一直没判断到接收结束,因此导致问题。所以我就在接收数据的中途就处理数据,判断起始/结束标志,将完整消息分离出来。
//以上5毫秒、2毫秒是本来这样想的,但是后来想想也没必要这么短间隔,而且间隔越短可能越容易出事,不该认为上位机代码里面接收到数据的时序天衣无缝,
//尤其使用虚拟串口调试的时候更容易时序对不上,于是改成了几十毫秒这样子,时间上留点余裕。
while (_bRunning)
{
if (IsOpen())
{
try
{
bytesToRead = _serialPort.BytesToRead;
if (bytesToRead > 0)
{
//如果缓存空间不够,则扩充缓存空间并将数据Copy过去。
//其实串口默认的接收缓存区大小是4K,更多的数据不接收了,设置_serialPort.ReadBufferSize的值可改变接收缓存区的大小。
if (bytesToRead > recvBuffer.Length - totalCount)
{
bool extend = true;//是否扩大buffer
if (recvBuffer.Length >= 128 * 1024)
{
totalCount = 0;//清空现存数据,避免无限扩大buffer。能运行到这,说明下位机一直高频率发送上位机处理不了的数据,或者就是串口电平受到持续扰动。
extend = bytesToRead > recvBuffer.Length;//_serialPort.ReadBufferSize默认是4k,所以bytesToRead实际上不会超过4k。
Debug.LogError($"串口接收buffer的尺寸过大!!! recvBuffer.Length:{recvBuffer.Length}。自动清空现存数据!");
}
if (extend)
{
byte[] newBuffer = new byte[totalCount + bytesToRead];
recvBuffer.CopyTo(newBuffer, 0);
Debug.Log($"串口接收buffer大小扩展:{recvBuffer.Length} -> {newBuffer.Length}");
recvBuffer = newBuffer;
}
}
countTemp = _serialPort.Read(recvBuffer, totalCount, bytesToRead);//从串口接收缓存区取出一些数据
Debug.Log($"串口接收数量:{countTemp}");
totalCount += countTemp;
//对数据进行处理。正常情况下,在这里就能把数据处理完了。除非数据出错,或者出现了奇怪的扰动。
HandleRecvData(recvBuffer, totalCount, out int restCount, reserveRest: true);
totalCount = restCount;
maybeNoMoreData = false;//也许还有数据可以读
noMoreData = false;
sleepTime = sleepTime_Receiving;//抓紧查询
receiving = true;
}
else
{
if (receiving)
{
if (maybeNoMoreData == true)//连续第二次判断到缓存里面没数据了(第一次与第二次之间睡眠了5毫秒),因此可以确认接收完成了。
{
noMoreData = true;
receiving = false;
}
if (maybeNoMoreData == false)//第一次判断到缓存里面没数据了,说明2毫秒之前就把数据读完了,之后睡眠3毫秒再看,如果还没数据,说明确实没了。
{
maybeNoMoreData = true;
sleepTime = sleepTime_Waiting - sleepTime_Receiving;
}
}
}
if (noMoreData)
{
if (totalCount > 0)
{
string dataStr = _serialPort.Encoding.GetString(recvBuffer, 0, totalCount);
Debug.Log($"处理剩余数据:{dataStr}");
//如果数据出错,或者出现了奇怪的扰动,就可能执行到这里,把数据清空。
HandleRecvData(recvBuffer, totalCount, out int restCount, reserveRest: false);
}
totalCount = 0;
maybeNoMoreData = false;
noMoreData = false;
sleepTime = sleepTime_Waiting;
receiving = false;
}
}
catch (Exception e)
{
totalCount = 0;//舍弃已经读取的数据
maybeNoMoreData = false;
noMoreData = false;
sleepTime = sleepTime_Waiting;
receiving = false;
Debug.LogError("串口数据接收异常:" + e.Message + " StackTrace: " + e.StackTrace);
}
}
Thread.Sleep(sleepTime);
}
Debug.Log("退出串口接收数据线程");
}
/// <summary>
/// 处理接收到的数据,从其中获取消息放进接收到的消息处理队列,并将剩下的数据放回buffer(也可以选择不放回,从而清空buffer).
/// 返回值表示是否添加了一个或多个消息进队列。
/// </summary>
/// <param name="buffer"></param>
/// <param name="totalCount">buffer里面的数据的字节数</param>
/// <param name="restCount">剩下的数据的字节数,其实代表方法返回后buffer中留存的数据的字节数</param>
/// <param name="reserveRest">剩下的数据是否放回buffer开头</param>
/// <returns></returns>
private bool HandleRecvData(byte[] buffer, int totalCount, out int restCount, bool reserveRest)
{
if (SeparateRecvData(buffer, totalCount, out restCount, out List<string> msgList, reserveRest))
{
//放进接收的消息处理队列
lock (_msgRecvQueue)
{
for (int i = 0; i < msgList.Count; i++)
{
_msgRecvQueue.Enqueue(msgList[i]);
Debug.Log("串口接收到数据:" + msgList[i]);
}
}
return true;
}
return false;
}
/// <summary>
/// 从接收到的数据中分离出单条消息存到列表中。并将剩余的数据重新放回到数组中。返回值表示是否分离出一条或多条数据。
/// </summary>
/// <param name="buffer">保存接收的数据的数组</param>
/// <param name="totalCount">数组中保存的数据的字节数</param>
/// <param name="restCount">分离后,剩余的字节数,其实代表方法返回后buffer中留存的数据的字节数</param>
/// <param name="msgList">保存消息的列表</param>
/// <param name="reserveRest">是否保留剩余的数据(指后面的暂时分析不出来的数据,可能数据还没接收完全)。true的话,就把剩余的数据拷贝到buffer开头,false的话就直接丢弃(在确认数据接收完了,暂时(几毫秒内)没有新数据的时候,应该使用false,以清空buffer,以免错误数据干扰)。</param>
/// <returns></returns>
private bool SeparateRecvData(byte[] buffer, int totalCount, out int restCount, out List<string> msgList, bool reserveRest)
{
msgList = new List<string>();
if (buffer == null || buffer.Length == 0 || totalCount == 0)
{
restCount = 0;
return false;
}
//以下假设单条数据是以字符串 START_FLAG 开始,以字符串 END_FLAG 结束。可以自己根据情况更改。
//这种以字符串 START_FLAG 开始,以字符串 END_FLAG 结束的方式,处理起来比较复杂,不如直接使用"\r\n"或者使用'\0'等单个字符隔开的方式处理起来更加简单。
string dataStr = _serialPort.Encoding.GetString(buffer, 0, totalCount);
if (!string.IsNullOrEmpty(dataStr))
{
string fullMsgStr = null, restStr = null;
SeparateRecvData2FullAndRest(dataStr, out fullMsgStr, out restStr);
if (!string.IsNullOrEmpty(fullMsgStr))
{
SeparateMsgsFromFullMsgStr(fullMsgStr, out msgList);
}
if (reserveRest && !string.IsNullOrEmpty(restStr))
{
byte[] restBytes = _serialPort.Encoding.GetBytes(restStr);
restBytes.CopyTo(buffer, 0);
restCount = restBytes.Length;
}
else
{
restCount = 0;
}
}
else
{
restCount = 0;
}
return msgList.Count > 0;
}
/// <summary>
/// 将接收到的数据拆分成具有一个或多个完整消息的字符串(每个消息以起始标志开头,以结束标志结尾,因此fullMsgStr也以起始标志开头,以结束标志结尾),
/// 以及剩余的部分(最后一个结束标志后面的部分)。
/// 方法返回后,fullMsgStr的状态是:"START????????????????END",问号表示不知道是什么,中间的问号中也许也有START、END标志。这个字符串中包含一个或多个完整的消息。
/// 或者fullMsgStr的状态是:null。
/// restStr的状态是"???????????"或者null。
/// </summary>
/// <param name="buffer"></param>
/// <param name="fullMsgStr"></param>
/// <param name="restStr"></param>
private void SeparateRecvData2FullAndRest(string buffer, out string fullMsgStr, out string restStr)
{
if (string.IsNullOrEmpty(buffer))
{
fullMsgStr = null;
restStr = null;
}
else
{
int lastEndIndex = buffer.LastIndexOf(END_FLAG);
if (lastEndIndex != -1)//有最后一个结束标志,说明“可能”有完整的消息,"STARTxxxxENDSTARTxx"有完整消息,"xxENDSTARTxx"则没有完整消息。xxx表示消息具体内容
{
string inFrontOfRest = null;//在剩余的部分的前面,也就是除去了Rest部分的数据后所剩下的数据,也就是前面的数据,应该是这样"?????????END"
int restIndex = lastEndIndex + END_FLAG.Length;//剩余的部分的索引
if (restIndex < buffer.Length)//最后一个结束标志后还有数据,也就是说有剩余的部分
{
inFrontOfRest = buffer.Substring(0, restIndex);
restStr = buffer.Substring(restIndex);
}
else//最后一个结束标志后没有数据了,说明没有剩余的部分
{
inFrontOfRest = buffer;
restStr = null;
}
//到这里,inFrontOfRest的状态是:"???????????????????END"
int firstStartIndex = inFrontOfRest.IndexOf(START_FLAG);//找到第一个开始标志
if (firstStartIndex != -1)//有第一个开始标志,说明有完整消息
{
fullMsgStr = inFrontOfRest.Substring(firstStartIndex);//firstStartIndex之前的数据丢弃。
}
else
{
fullMsgStr = null;//没有第一个开始标志,说明前面数据不完整、或者START标志损坏,那么直接丢弃掉就行了。(正常处理情况下不会出现这种情况)
}
//到这里,fullMsgStr的状态是:"START????????????????END",问号表示不知道是什么,中间的问号中也许也有START、END标志。这个字符串中包含一个或多个完整的消息。
//或者fullMsgStr的状态是:null
//此时,restStr的状态是"???????????"或者null
}
else//没有最后一个结束标志,说明没有完整的消息。或者结束标志损坏(这种情况也没事,因为在以后处理的时候,会将损坏的剔除)
{
fullMsgStr = null;
restStr = buffer;
}
}
}
/// <summary>
/// 从具有完整消息的字符串中分离出一个或多个完整消息。
/// 参数要保证,fullMsgStr要以起始标志开头,以结束标志结尾。
/// 返回值表示是否分离出了一个有效的消息,即msgList中是否有元素。
/// msgList中保存的将是一条一条的单独的完整消息,如果传输的数据的起始结束标志不出错的话,确实是这样。如果出错,则只能保证元素以 起始标志 开始,以 结束标志 结束,
/// 只是以后在处理这里面的元素的时候,要注意处理数据可能出错的情况。
/// </summary>
/// <param name="fullMsgStr"></param>
/// <param name="msgList"></param>
/// <returns></returns>
private bool SeparateMsgsFromFullMsgStr(string fullMsgStr, out List<string> msgList)
{
msgList = new List<string>();
string restFullMsgStr = fullMsgStr;
while (!string.IsNullOrEmpty(restFullMsgStr))
{
int firstStartIndex = restFullMsgStr.IndexOf(START_FLAG);
if (firstStartIndex == -1)
{
break;
}
else
{
//如果前一条消息的起始标志或结束标志损坏了,希望不影响下一条消息。
int firstEndIndex = restFullMsgStr.IndexOf(END_FLAG);//这个值一定不会是-1,因为方法参数里面已经保证fullMsgStr的最后是结束标志。
if (firstEndIndex < firstStartIndex)//说明位于firstStartIndex之前的起始标志损坏了,则此时firstStartIndex是下一条消息的起始标志。
{
restFullMsgStr = restFullMsgStr.Substring(firstStartIndex);//舍弃掉前面的消息
}
else
{
int secondStartIndex = restFullMsgStr.IndexOf(START_FLAG, firstStartIndex + START_FLAG.Length);
if (secondStartIndex != -1 && secondStartIndex < firstEndIndex)//这种情况说明此条消息的结束标志损坏了。
{
restFullMsgStr = restFullMsgStr.Substring(secondStartIndex);//因为此条消息的结束标志损坏了,所以将这条消息直接舍弃,而留下剩下的。
}
else
{
int firstMsgLength = firstEndIndex + END_FLAG.Length - firstStartIndex;
string msg = restFullMsgStr.Substring(firstStartIndex, firstMsgLength);
msgList.Add(msg);
//如果secondStartIndex为-1,则表示没有下一条消息了,或者后面都是无效消息。
restFullMsgStr = secondStartIndex == -1 ? string.Empty : restFullMsgStr.Substring(firstEndIndex + END_FLAG.Length);
}
}
}
}
return msgList.Count > 0;
}
/// <summary>
/// 发送数据。其实只是将数据放进消息队列里面,发送线程会将消息队列里的消息一个个发送出去。
/// </summary>
/// <param name="msg"></param>
public void Send(string msg)
{
if (!string.IsNullOrEmpty(msg))
{
lock (_msgSendQueue)
{
_msgSendQueue.Enqueue(msg);
}
}
}
/// <summary>
/// 发送数据的线程
/// </summary>
private void SendDataLoop()
{
Debug.Log("进入串口发送数据线程");
while (_bRunning)
{
if (IsOpen())
{
if (_serialPort.BytesToWrite <= 0)//输出缓存区里面没有数据了,等几毫秒再将数据写进去,保证两次数据之间时间上是隔断的
{
Thread.Sleep(10);
lock (_msgSendQueue)
{
if (_msgSendQueue.Count > 0)
{
string msg = _msgSendQueue.Peek();
if (!string.IsNullOrEmpty(msg))
{
try
{
_serialPort.Write(msg);
_msgSendQueue.Dequeue();
}
catch (Exception e)
{
Debug.LogError("串口数据发送异常:" + e.Message);
}
}
else
{
_msgSendQueue.Dequeue();
}
}
}
}
}
}
Debug.Log("退出串口发送数据线程");
}
}
完整工程下载:https://download.csdn.net/download/Thechuang/21487378
使用Unity版本:2019.4.15f1c1
工程是东拼西凑的,所以有一些用不到的代码。水平有限,请多担待。