串口学习
概述:
主要在unity环境使用system.io.ports库(当然也有很多优秀的第三方库),如果较高的unity版本(2020版本开始没有这个库,低于2020需要切换到.netframework)没有这个库,那么我们主动下载导入这个库(从nuget下载.netstandard2.0的dll放入plugins文件夹下),.下文主要介绍如何接受串口到来的数据和如何使用这些数据.
nuget gallery | system.io.ports 9.0.0-preview.6.24327.7
下载下来后在lib目录下找到dll
串口的基本概念
串口,也称为串行端口,是一种用于实现串行通信的接口。在计算机和其他设备之间,数据是按位顺序传输的。串口通信常用于连接如打印机、调制解调器、各种测量仪器和其他多种外部硬件到计算机上。
插入串口设备在设备管理器中可以发现
配置串口基本参数
在实际写代码之前我建议先阅读需要调试的串口设备的协议,然后在网上搜索一些串口工具来初步调试一下自己的串口设备.
serialport 类介绍
概述
在 c# 中,`system.io.ports.serialport` 类用于管理串行端口资源。这个类封装了对串口的操作,包括打开端口、设置参数、发送数据、接收数据和关闭端口。通过这个类的实例,可以和串口设备进行交互.
类的成员
一常用属性
readtimeout:
readtimeout 属性指定在串口对象尝试读取操作但尚未接收到数据时的超时时长(以毫秒为单位)。如果在指定的时间内没有读取到任何数据,读取操作将抛出一个 timeoutexception。
readbuffersize:
readbuffersize用来控制串口数据缓冲区大小,默认是4090字节
isopen:
检测串口是否已经被打开
bytestoread:
检测缓冲区字节数
二常用方法:
open()
打开串口连接
close()
关闭串口连接
write()
用途:向串口写入数据。
discardinbuffer() 和 discardoutbuffer()
用途:清除输入/输出缓冲区的数据。
读取数据方法
1 read() 通常使用这种办法
read() 方法提供了更灵活的数据读取方式,它可以从串口缓冲区读取指定数量的字符到字符数组中。你可以指定从缓冲区的哪个位置开始读取,以及读取多少个字符。当你指定的 buffer 大小(或 count 参数指定的数量)比实际在串口缓冲区中可用的数据多时,read 方法只会读取并返回当前可用的数据量。不会发生错误或异常,只是返回值会告诉你实际读取了多少字节。可以使用array.copy(),处理我们需要的数据
优点:提供了更精细的控制,能够指定读取的长度和起始位置,适用于二进制数据或特定格式的数据。
缺点:需要手动管理缓冲区和字符计数,实现复杂。
2 readline()
readline() 方法读取串口数据直到遇到换行符(默认是 \n)。这个方法非常适合于以行为单位发送的文本数据。使用 readline() 可以简化按行处理数据的逻辑。
优点:直接按行读取,简化处理流程,尤其是文本数据。
缺点:如果数据中不包含预期的换行符,readline() 会导致阻塞,直到读取到换行符或超时。
其他读取方法
readbyte():从串口读取单个字节并返回。这种方法适合于逐字节处理数据。
readchar():读取一个字符并返回。类似于 readbyte(),但返回的是字符。
readexisting():读取串口缓冲区中的所有现有数据为字符串。这个方法不会阻塞,只读取到目前为止缓冲区中已有的数据。
readto(string value):读取数据直到遇到指定的字符串 value。这可以用于读取到特定的标记或分隔符。
选择哪种读取方法取决于你的特定需求:
如果数据以行为单位发送,且每行以换行符结束,使用 readline()。
如果需要处理大量的二进制数据或对数据格式有特定的处理要求,使用 read() 更为合适。
如果需要实时抓取缓冲区中的所有数据,或在某些情况下需要非阻塞读取,可以选择 readexisting()。
三构造函数
`serialport` 类提供了几个构造函数,允许你在创建串口对象时指定不同的配置参数。常见的构造函数包括:
1. 无参数构造函数
serialport myserialport = new serialport();
这种方式创建的串口对象没有预设任何配置。你需要在使用前手动设置所有必要的属性,如端口名(`portname`)、波特率(`baudrate`)、奇偶校验(`parity`)、数据位(`databits`)和停止位(`stopbits`)。
2. 带端口名和波特率的构造函数
serialport myserialport = new serialport("com1", 9600);
这个构造函数允许你直接指定最常用的两个参数:端口名和波特率。其他参数(如奇偶校验、数据位和停止位)将使用默认值(通常是 `none`、`8`、`one`)。
3. 完全指定构造函数
serialport myserialport = new serialport("com1", 9600, parity.none, 8, stopbits.one);
这种方式可以在创建对象时设置所有主要的串口参数。这适用于需要精确控制串口配置的情况。
4.五个关键的配置参数
1. portname
描述:portname 指定要连接的串口的名称。在 windows 系统上,它通常表示为 "com1", "com2", 等等。
用途:用于确定你的应用程序将通过哪个物理或虚拟串口与外部设备通信。
2. baudrate
描述:波特率定义了在串行通信中每秒传输的比特数。常见的波特率包括 9600, 19200, 38400, 57600, 115200 等。
用途:波特率需要与连接设备的设置相匹配,以确保数据正确传输。设置不当可能导致数据丢失或通信错误。
3. parity
描述:奇偶校验是一种错误检测机制。它可以设置为 none(无校验)、odd(奇校验)、even(偶校验)、mark(标记校验)或 space(空格校验)。
用途:奇偶校验帮助检测数据在传输过程中的单比特错误。选择哪种奇偶校验取决于你的通信协议和设备要求。
4. databits
描述:数据位设置定义了串口数据包中的数据位数。常见的设置包括 7 或 8 位,尽管有些系统可能支持更少或更多的位数。
用途:数据位数决定了每个数据包的信息容量。大多数应用通常使用 8 数据位,但在某些情况下,如使用特定协议或设备时,可能需要不同的设置。
5. stopbits
描述:停止位用于标示每个数据包的结束,可以设置为 one(1 位停止位)、two(2 位停止位)或 onepointfive(1.5 位停止位)。
用途:停止位是数据包的结束标志,确保接收设备正确地同步和解析接收到的消息。不正确的停止位设置可能导致接收方解析错误。
示例
serialport myserialport = new serialport("com3", 115200, parity.none, 8, stopbits.one);
在这个示例中,我们连接到 "com3" 端口,设置波特率为 115200,无奇偶校验,8 个数据位,和 1 个停止位。
通常根据串口设备的协议进行配置即可,如果有些参数没有约定使用默认值即可.
世界观
一 线程安全
unity是单线程处理所有与游戏逻辑、渲染和用户界面相关的操作,通常在unity中为了不影响主线程,会选择再开一个线程处理串口数据(但是要注意不要在该线程访问unity的组件或api)
二unity帧更新和串口数据流
在 unity 中处理串口通信时,需要考虑两种不同的处理速度和单位:unity 的帧更新和串口的数据流。unity是以帧为单位进行周期性的计算,而串口是一个数据流,不停的存储到来的数据(在还有空间的情况下)
1 unity 的帧更新
unity 的更新循环基于帧,每一帧都会执行一次 update() 方法。帧的速度取决于游戏的帧率,通常设计为每秒 30 到 60 帧。这意味着每次 update() 方法的调用都是在大约 16.67 毫秒(60 fps)或 33.33 毫秒(30 fps)间隔。
2 串口数据流
概述
串口数据的处理则基于数据的到达。数据从串口到达的速度取决于多个因素,包括波特率和外部设备的数据发送频率。串口本身并不基于“帧”概念,而是连续的数据流。
注意:数据流到达后是被操作系统管理,代码中声明的字节数组和数据流缓冲区是两个概念,前者是少量多次的从后者读取数据,下面来详细介绍一下数据缓冲和读取机制的概念
1串口缓冲区:
串口通信中,硬件和操作系统通常提供一个缓冲区来暂存从硬件端口到达的数据。这个缓冲区的作用是在应用程序准备好读取数据之前,先存储这些数据。
大小可以通过readbuffersize属性来控制,默认是4096字节
2数据读取:
当你使用 serialport.read 方法时,这个方法会从缓冲区中读取指定数量的字节到你提供的数组中。如果你调用 read 方法时指定的字节数比缓冲区中的字节数少,那么它只会读取你请求的字节数;如果请求的字节数比缓冲区中的字节数多,它会读取缓冲区中的所有可用字节。
3数据丢失的可能情况
数据丢失可能发生在以下几种情况:
1缓冲区溢出:如果串口数据到达的速度超过了应用程序读取它们的速度,缓冲区可能会填满并溢出。这意味着新到达的数据会丢失,因为没有更多的空间来存储它们。
2不及时读取:如果你不频繁地从缓冲区读取数据,或者处理间隔过长,缓冲区可能会在下一次读取之前已经满了,导致新数据无法保存并丢失。
4如何防止数据丢失
1合理设置缓冲区大小:可以通过 serialport.readbuffersize 属性来调整读取缓冲区的大小,以适应数据流的需求。
2频繁读取数据:确保你的应用程序足够频繁地从缓冲区读取数据,以避免缓冲区满导致的数据丢失。
3使用多线程或异步读取:考虑在后台线程中处理数据读取,或使用异步i/o操作,这样可以更有效地管理数据流,尤其是在数据量大或数据到达速度快的情况下。
4监控数据流:监控数据流的到达速度和处理速度,调整你的读取策略,确保处理逻辑能跟上数据到达的速度。
3如何协调 unity 帧更新与串口数据流
要在 unity 中高效地处理串口数据,关键在于协调好 unity 的帧更新与连续的串口数据流。处理方法主要包括:
1使用后台线程读取数据:创建一个单独的线程来处理串口数据的读取,这可以防止数据读取过程中阻塞 unity 的主线程。后台线程可以持续检查串口,将读取的数据存储到线程安全的队列或缓冲区中。
2主线程中的数据处理:在 unity 的 update() 方法中,你可以从线程安全的队列或缓冲区中取出数据,并进行进一步的处理,如更新游戏状态、显示数据等。这样做可以确保即使数据以高速率到达,也能够被及时处理,而不会错过任何数据。
3使用适当的同步机制:为了防止数据竞争和确保数据一致性,在后台线程和主线程之间共享数据时,使用锁或其他线程安全的数据结构(如 concurrentqueue)是非常必要的。
4调整数据处理的频率:如果数据非常频繁地到达,可能需要在 update() 方法中实现一种机制,按一定的比例或条件选择性地处理数据,以避免在单个帧更新中过度处理数据导致性能问题。
通过以上方式,可以有效地将串口数据集成到 unity 的帧驱动模型中,确保游戏的流畅性和串口通信的高效性。这样的设计也有助于应对各种动态变化的需求,如可变的数据流速率和游戏帧率的调整。
方法论
using system;
using system.collections;
using system.collections.concurrent;
using system.collections.generic;
using system.io.ports;
using system.threading;
using unityengine;
public class testserialport : monobehaviour
{
public string portname;
public int baudrate;
serialport serialport;
//这里使用并行队列保证线程安全
private concurrentqueue<string> concurrentqueue = new concurrentqueue<string>();
private thread serialthread;
private bool isrunning = false;
void start()
{
//请注意这里有一个难点,就是如何获取portname
//串口设备的物理位置尽量避免被改变,但有的时候不得不改变位置
//我们可以通过读取配置来保证使用正确的portname
//通过设备管理器检测该串口设备的portname
//还有一个办法即使用:
//string[] ports = serialport.getportnames();
//该方法可以获取可以串口名数组,但是不保证被其他对象占用,
//还有一个缺点就是无法得知portname和串口设备的对应关系,
//所以还是使用配置的办法尽管该办法有些笨,
//但是可以确保我们有效的管理多个串口设备
portname = configportname();
openport(portname, baudrate);
serialthread = new thread(datareceiver);
serialthread.start();
}
//todo 实现配置方法
private string configportname()
{
return null;
}
private void update()
{
handledata();
}
private void ondestroy()
{
closeport();
}
private void onapplicationquit()
{
closeport();
}
private void openport(
string portname,
int baudrate,
parity parity = parity.none,
int databits = 8,
stopbits stopbits = stopbits.one)
{
serialport = new serialport(portname, baudrate, parity, databits, stopbits);
try
{
serialport.readtimeout = 400;
serialport.open();
if (serialport.isopen)
{
debug.log("the serialport has been opened");
}
isrunning = true;
}
catch (exception e)
{
debug.logerror(e.message);
}
}
private void datareceiver()
{
byte[] buffer = new byte[64]; //依照情况修改
while (isrunning)
{
try
{
if (serialport.isopen && serialport.bytestoread > 0)
{
int bytesread = serialport.read(buffer, 0, buffer.length);
//返回值bytesread可能会帮到你,
//因为有可能上面设置的临时缓冲区较大,
//这不会出错,但是多出的
//长度是无意义的默认值,可以使用array.copy()
//下面的代码根据实际业务逻辑自行修改,但是下面的方法可能会帮到你
//convert.tostring(byte,2),
//convert.toint(string,2),bitconvert等
var tempdata = handlebytearray(buffer);
concurrentqueue.enqueue(tempdata);
}
}
catch (exception e)
{
debug.logerror(e.message);
}
thread.sleep(10);
}
}
//todo 实现这个方法
private string handlebytearray(byte[] bytes)
{
return null;
}
//todo 实现该方法
private void handledata()
{
//concurrentqueue.trydequeue()
}
private void closeport()
{
isrunning = false;
if (serialport != null && serialport.isopen)
{
serialport.close();
}
if (serialthread != null && serialthread.isalive)
{
serialthread.join();
}
}
}
发表评论