【Unity】使用 C# SerialPort 进行串口通信_unity串口通信
索引
- 一、SerialPort串口通信
- 二、使用SerialPort
-
- 1.创建SerialPort对象,进行基本配置
- 2.写入串口数据
-
- ①.写入串口数据的方法
- ②.封装数据
- 3.读取串口数据
-
- ①.读取串口数据的方法
- ②.解析数据
- 4.读取串口数据的时机
-
- ①.DataReceived事件
- ②.多线程接收数据
- 5.粘包问题处理
一、SerialPort串口通信
C#中的SerialPort
类是.NET
框架提供的一个用于串口通信的强大工具,主要用于实现计算机与外部设备(如传感器、嵌入式设备、PLC等)之间的数据交换。
二、使用SerialPort
1.创建SerialPort对象,进行基本配置
使用SerialPort
时,直接创建一个SerialPort
对象即可,其基本配置项包括:
PortName:串口号,如COM1。
BaudRate:波特率,如9600或115200。
DataBits:数据位,通常为7或8。
StopBits:停止位,通常为1、1.5或2。
Parity:奇偶校验位,如None(无校验)、Odd(奇校验)或Even(偶校验)。
代码如下:
/// /// 串口号 /// [Label(\"串口号\")] public string portName; /// /// 波特率 /// [Label(\"波特率\")] public int baudRate; /// /// 数据位 /// [Label(\"数据位\")] public int dataBits; /// /// 停止位 /// [Label(\"停止位\")] public StopBits stopBits; /// /// 奇偶校验位 /// [Label(\"奇偶校验位\")] public Parity parity; private SerialPort _serialPort; protected override void Awake() { base.Awake(); _serialPort = new SerialPort(); _serialPort.PortName = portName; _serialPort.BaudRate = baudRate; _serialPort.DataBits = dataBits; _serialPort.StopBits = stopBits; _serialPort.Parity = parity; try { _serialPort.Open(); } catch (Exception e) { Log.Error(e.Message); } }
2.写入串口数据
①.写入串口数据的方法
写入串口数据的方法非常简单,如下:
//将数据封装为字节数组(可以直接强转字符串,也可以按16进制处理等) byte[] bytes = EncapsulatePackage(data); _serialPort.Write(bytes, 0, bytes.Length);
②.封装数据
如何封装数据
取决于数据交换的协议,比如直接强转字符串:
/// /// 单个数据包结束符(换行符) /// private const byte EndSign = 10; /// /// 封装数据包(这里要求所有字符必须为ASCII码,也即是一个字符只占一个字节) /// /// 原始数据 /// 数据包 private byte[] EncapsulatePackage(string data) { byte[] bytes = new byte[data.Length + 1]; for (int i = 0; i < data.Length; i++) { bytes[i] = (byte)data[i]; } //在每个数据包后面补充【结束符】,表明此数据包结束 bytes[bytes.Length - 1] = EndSign; return bytes; }
这里以简单协议进行讲解(每个数据包以【换行符】代表结束符
)。
3.读取串口数据
①.读取串口数据的方法
读取串口数据的方法非常简单,如下:
//建立数据缓冲区(大小为串口中可读取数据大小:_serialPort.BytesToRead) byte[] bytes = new byte[_serialPort.BytesToRead]; _serialPort.Read(bytes, 0, bytes.Length); //解析数据包 string data = AnalyzePackage(bytes);
②.解析数据
如何解析数据
取决于数据交换的协议,比如直接强转字符串:
/// /// 解析数据包 /// /// 数据包 /// 数据 private string AnalyzePackage(byte[] bytes) { return Encoding.Default.GetString(bytes); }
4.读取串口数据的时机
那么对如上的简单的写入、读取串口数据
有了基本的了解之后,接下来便是相对不那么简单
的部分了。
首先,写入串口数据的时机
由我们说了算,何时调用便何时写入,这点毋容置疑。
但是,读取串口数据的时机
该是何时?参考下面两种方式。
①.DataReceived事件
SerialPort的DataReceived
事件当对象对应的串口接收到了数据时便会触发,用他来读取数据简直不要太丝滑:
protected override void Awake() { base.Awake(); //...... _serialPort.DataReceived += OnDataReceived; //...... } private void OnDataReceived(object sender, SerialDataReceivedEventArgs e) { if (_serialPort.IsOpen && _serialPort.BytesToRead > 0) {//建立数据缓冲区(大小为串口中可读取数据大小:_serialPort.BytesToRead) byte[] bytes = new byte[_serialPort.BytesToRead]; _serialPort.Read(bytes, 0, bytes.Length); //解析数据包 string data = AnalyzePackage(bytes); } }
但是,这里必须得有一个但是,只要在Unity中用过SerialPort
的都会知道DataReceived
这玩意他不起作用,主要原因如下(别怀疑,就是问的AI):
1.Unity引擎对System.IO.Ports命名空间的支持有限,尤其是对DataReceived事件的支持存在缺陷。在Unity中,DataReceived事件通常不会像在常规C#项目中那样正常触发,这主要是因为Unity的运行环境与普通.NET应用有所不同,特别是在事件处理机制上存在差异。
2.DataReceived事件需要在一个独立的线程中监听串口数据,但在Unity中,主线程(用于游戏逻辑和渲染)和串口数据接收线程之间的同步机制存在问题。因此,DataReceived事件可能无法被正确触发。
3.Unity的Update和FixedUpdate等事件函数运行在主线程中,而串口数据接收通常需要多线程支持。当串口操作在主线程中执行时,可能会因为线程冲突导致DataReceived事件无法触发。
那么,我们不得不考虑更换其他方案了。
②.多线程接收数据
老样子,像处理Socket
通信那样,多线程永远是最强的利器:
private Thread _receiveThread; protected override void Awake() { base.Awake();//新建一个线程,启动数据接收方法ReceivedData _receiveThread = new Thread(new ThreadStart(ReceivedData)); _receiveThread.Start(); } protected override void OnDestroy() { base.OnDestroy(); _receiveThread.Abort(); _receiveThread = null; } /// /// 从串口接收数据 /// private void ReceivedData() { while (true) { if (_serialPort.IsOpen && _serialPort.BytesToRead > 0) { try { //建立数据缓冲区(大小为串口中可读取数据大小:_serialPort.BytesToRead) byte[] bytes = new byte[_serialPort.BytesToRead]; _serialPort.Read(bytes, 0, bytes.Length); //解析数据包 string data = AnalyzePackage(bytes); //...... } catch (Exception e) { Log.Error(e.Message); } } } }
5.粘包问题处理
当然,到此时我们并不能高枕无忧,还有另一个在数据交换领域普遍存在的问题需要我们解决,那就是数据的粘包
问题。
粘包问题是指多个数据包在接收端被错误地合并成一个数据包,导致接收方无法正确区分每个数据包的边界。这通常发生在连续发送多个数据包时,接收端来不及解析或缓冲区管理不当的情况下。
就像Socket
通信一样,发送方发出的数据是A03568
,但接收方可能会分多次接收到,比如2次才接收完,那就可能是A03
、568
,接收方只是一个机器不是人,自然不知道这2个包该连起来合成一个包,那么后续的处理自然就乱套了。
这就是通信协议
存在的必要了,以我们的简单通信协议为例(每个数据包以【换行符】代表结束符
),在读取数据的方法中进行防粘包处理:
private byte[] _buffer = new byte[16]; private List<byte> _receiveBuffer = new List<byte>(); /// /// 从串口接收数据 /// private void ReceivedData() { while (true) { if (_serialPort.IsOpen && _serialPort.BytesToRead > 0) { try { //读取一次数据(最大不超过缓冲区大小) int count = _serialPort.Read(_buffer, 0, Mathf.Min(_buffer.Length, _serialPort.BytesToRead)); for (int i = 0; i < count; i++) { //如果为结束符,则代表一个数据包接收完成,解析该包 if (_buffer[i] == EndSign) { string data = AnalyzePackage(_receiveBuffer.ToArray()); //清空缓冲区 _receiveBuffer.Clear(); //处理数据 HandlerData(data); Log.Info($\"接收串口数据:{data}\"); } //否则加入数据缓冲区 else { _receiveBuffer.Add(_buffer[i]); } } } catch (Exception e) { Log.Error(e.Message); } } } }
代码很简单,相信处理过字节流的人应该都能看懂,事实上在串口通信中如上的简单通信协议已能胜任大多数情况。