当前位置: 代码网 > it编程>游戏开发>unity > 在Unity环境中进行串口通讯

在Unity环境中进行串口通讯

2024年08月01日 unity 我要评论
在Unity中读取串口数据

串口学习

概述:

主要在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();

        }

    }

}

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com