08.串口通信方案建设设计
目录介绍
- 01.串口通信的概念
- 1.1 什么是串口通信
- 1.2 串口通信特点
- 1.3 串口通信场景
- 1.4 串口通信优缺点
- 1.5 串口硬件说明
- 1.6 串口通信问题思考
- 02.串口通信实践流程
- 2.1 打开串口
- 2.2 发送数据
- 2.3 接收数据
- 2.4 错误检测和处理
- 2.5 关闭串口
- 2.6 串口数据校验
- 03.串口通信方案设计
- 3.1 使用谷歌c实现方案
- 3.2 使用c++实现方案
- 3.3 两种方案对比分析
- 04.SerialPort实现原理
- 4.1 打开串口通信
- 4.2 串口参数介绍
- 4.3 串口数据传输
- 4.4 串口传输流程
- 4.5 错误检查纠正
- 4.6 串口通信传输编码
- 05.使用c++实现串口通信
- 5.1 c++实现打开串口
- 5.2 c++实现串口参数
- 5.3 c++实现读数据
- 5.4 c++实现写数据
- 5.5 c++实现关闭串口
- 06.串口通信稳定性
- 6.1 数据缓冲区管理
- 6.2 错误处理和重试
- 6.3 串口设备问题排查
- 07.串口通信数据收发原理
- 7.1 接收数据场景和分析
- 7.2 为什么设计桢数据
- 7.3 如何及时处理数据
- 7.4 完整桢包含桢头桢尾
- 7.5 发送数据场景和分析
01.串口通信的概念
1.1 什么是串口通信
- 串口通信是一种通过串行接口(串口)在计算机或其他设备之间传输数据的通信方式。
- 它是一种常见的数据传输方式,用于连接计算机与外部设备,如传感器、打印机、调制解调器、嵌入式系统等。
- 串口通信使用串行接口来逐位地传输数据。
- 在串口通信中,数据被分成一个个位(bit)进行传输,通常使用一对数据线(发送线和接收线)来进行双向通信。
- 数据按照一定的协议和规则进行传输,通常包括起始位、数据位、校验位和停止位等。
- 串行端口 (SerialPort)简称:串口
- 主要用于数据被逐位按顺序传送的通讯方式称为串口通讯(简单来讲就是按顺序一位一位地传输数据)。
1.2 串口通信特点
- 串口通信具有以下特点
- 逐位传输:数据按位传输,一次只传输一个位。
- 双向通信:串口通信可以实现双向数据传输,即可以同时发送和接收数据。
- 相对较低的传输速率:串口通信的传输速率相对较低,通常以波特率(bps)来表示。
- 硬件控制:串口通信可以通过硬件信号进行流量控制,如使用RTS(请求发送)和CTS(清除发送)信号进行流量控制。
1.3 串口通信场景
- 串口通信提供了一种可靠、简单和经济的数据传输方式,适用于各种场景和设备。
- 在许多应用中被广泛使用,特别是在嵌入式系统、物联网设备、工业自动化和通信领域。
- 很多硬件设备都可以通过串口进行通讯,比如:打印机、ATM吐卡机、IC/ID卡读卡等,以及物联网相关的设备。
1.4 串口通信优缺点
- 串口通信优点说明一下:
- 串口通信具有一些优点,如简单、可靠、适用于长距离传输和适应各种设备。
- 串口通信缺点说明一下:
- 1.传输速率较低:相比于其他通信方式(如以太网或无线通信),串口通信的传输速率相对较低【通信速率由波特率决定】。
- 2.有限的距离和连接数:串口通信的距离受限于串口线缆的长度和信号衰减。
- 3.不支持一对多:串口通信通常只支持点对点连接,即一对一的连接方式。
- 4.传输数据限制:数据帧的大小限制为最多 9 位,常用与ASCII编码,最多可以给256个字符(包括字母、数字、标点符号、控制字符及其他符号)分配(或指定)数值。
- 总结一下
- 然而,由于串口通信的传输速率相对较低,不适合高速数据传输。在实际应用中,需要根据具体需求和设备特性选择合适的通信方式。
1.5 串口硬件说明
- 串口通信涉及到两个主要的硬件组件:发送器和接收器。
- 发送器将数据位逐位地发送到串口的发送线上。
- 接收器从串口的接收线上逐位地接收数据位。
- 串口通信(Serial Communications)的概念非常简单,串口按位(bit)发送和接收字节。
- 串口通讯(Serial Communication),设备与设备之间,通过输入线(RXD),输出线(TXD),地线(GND),按位进行传输数据的一种通讯方式。
- 由于串口通信是异步的,端口能够在一根线上发送数据同时在另一根线上接收数据。其他线用于握手,但不是必须的。
- 串口通信最重要的参数是波特率、数据位、停止位和奇偶校验。对于两个进行通信的端口,这些参数必须匹配。
- Android SDK并没有在Framework层实现封装关于串口通信的类库。
- Android是基于Linux kernel 2.6上的,所以我们可以像在Linux系统上一样来使用串口。
- 因为Framework层中并没有封装关于串口通信的类库,所以我们需要通过Android NDK来实现打开、读写串口,然后提供接口供JAVA本地调用。
1.6 串口通信问题思考
- 基础问题思考
- 什么叫做串口通信?串口通信硬件组成有什么?它的概念是什么,通信是串行还是并行的?有何特点?
- 串口通信优缺点是什么?串口通信传输的数据最大是多大?如何传递数据,并检验数据的完整性?
- 高级一点的问题思考
- 如何理解文件传输符?串口数据传输的帧数据是如何组成的,如何理解?
02.串口通信实践流程
2.1 打开串口
- 要打开要使用的串口。
- 这可以通过调用操作系统提供的串口API或使用第三方库来实现。
- 在打开串口之前,需要指定串口的设备路径(如/dev/ttyUSB0)和相应的参数(如波特率、数据位、校验位和停止位等)。
- 参数的设置应与通信对方设备的参数相匹配。以确保发送和接收端的数据传输一致。
/** * @param 1 串口路径,根据你的设备不同,路径也不同 * @param 2 波特率 * @param 3 flags 给0就好 */ SerialPort serialPort = new SerialPort(new File("/dev/ttyS1"), 9600, 0);
- 注意波特率匹配
- 发送端和接收端的波特率必须匹配,否则会导致数据传输错误。确保在通信双方使用相同的波特率设置。
- 打开串口的流程
- 1.通过Runtime,获取当串口进程。
- 2.向Linux内核发送一个"chmod 666 "指令设置串口进程的权限。
- 3.通过JNI方法获取文件描述符对象。
- 4.通过文件描述符对象获取输入输出流。
- 设备文件是什么
- 在Android系统中,串口设备对应着一个设备文件,一般位于/dev目录下。打开串口时,我们需要指定串口设备的路径,例如/dev/ttyS0。
- 打开了这个设备文件,并与其建立了一条通信通道,以实现串口的读写操作。
2.2 发送数据
- 使用串口发送数据时,需要将要发送的数据按照一定的协议和格式进行封装。
- 通常,数据会被分成一个个数据帧,每个数据帧包含起始位、数据位、校验位和停止位等。将封装好的数据通过串口发送出去。
- 往串口中写入数据。注意这个数据是字节数组。
//从串口对象中获取输出流 OutputStream outputStream = serialPort.getOutputStream(); //需要写入的数据 byte[] data = new byte[x]; data[0] = ...; data[1] = ...; data[x] = ...; //写入数据 outputStream.write(data); outputStream.flush();
- 发送端和接收端的数据位、校验位和停止位设置必须一致。这些参数的不匹配可能导致数据解析错误或丢失。
2.3 接收数据
- 在接收端,需要设置好串口的接收缓冲区,并等待接收数据。
- 当有数据到达时,接收器会逐位接收数据,并根据协议和参数进行解析和处理。接收到的数据可以存储在接收缓冲区中,供后续处理使用。
- 读取数据时很可能会遇到分包的情况,即不能一次性读取正确的完整的数据
- 解决办法:可以在读取到数据时,让读取数据的线程sleep一段时间,等待数据全部接收完,再一次性读取出来。这样应该可以避免大部分的分包情况
- 只接收一条数据的情况下,以上方法可以应对数据分包,数据量多的情况下需要考虑是否会因为sleep导致接收多条数据,可以根据通信协议核对包头包尾等参数。
//从串口对象中获取输入流 InputStream inputStream = serialPort.getInputStream(); //使用循环读取数据,建议放到子线程去 while (true) { if (inputStream.available() > 0) { //当接收到数据时,sleep 500毫秒(sleep时间自己把握) Thread.sleep(500); //sleep过后,再读取数据,基本上都是完整的数据 byte[] buffer = new byte[inputStream.available()]; int size = inputStream.read(buffer); } }
2.4 错误检测和处理
- 在串口通信中,通常使用校验位来检测传输错误。
- 接收端会根据校验位检查数据的完整性,并在检测到错误时进行纠正或丢弃。如果发生错误,可以根据具体情况进行相应的处理,如重新发送数据或进行错误处理。
- 常见的校验位包括奇校验、偶校验和无校验。正确的校验位设置可以提高数据传输的可靠性。
2.5 关闭串口
- 在完成串口通信后,需要关闭串口以释放资源。这可以通过调用相应的关闭函数或方法来实现。
serialPort.close();
2.6 串口数据校验
- 一般情况下串口通讯协议都会在数据帧或者说命令格式里定义一个校验方式,常用的有:异或校验、和校验、CRC校验和LRC校验。
- 注意:这里说的校验和校验位是不同的,校验位针对的是单个字节,校验类型针对的是单个数据帧。
- 校验方式一般放在命令最后,可以是一个byte,也可以是两个byte或者其他,具体看协议设计。
03.串口通信方案设计
3.1 使用谷歌c实现方案
- 当前的Android SDK不提供任何用于读写Linux TTY串口的API。您可能会在 HTC Android 手机的连接器上找到此类串行端口。
- 该项目希望提供一个简单的API来通过这些串行端口连接、读取和写入数据。
- 支持的功能有:
- 列出设备上可用的串行端口,包括 USB 转串行适配器
- 配置串行端口(波特率、停止位、权限等)
- 提供标准的 InputStream 和 OutputStream Java 接口
- 该项目不可能实现的功能:
- 通过 USB 从机接口接收/发送数据
3.2 使用c++实现方案
- 核心原理和官方demo一样
- 其底层原理都是通过调用open函数打开设备文件来进行读写操作。同样是那几个步骤,设置串口参数,通过调用open方法开启串口,再进行数据的读写操作。
- 支持的功能有:
- 列出设备上可用的串行端口,包括 USB 转串行适配器
- 配置串行端口(波特率、停止位、权限、数据位、校验类型等)
- 针对读写,没有提供Java层的stream接口,而是直接采用c++层的读写数据操作
3.3 两种方案对比分析
- 两种方案的相同点说明
- 设置串口波特率、数据位、停止位、校验位主要操作的就是termios 结构体,对应的头文件是termios.h。
- 两种方案的差异点说明
- 谷歌官方串口通信c语言实现方案,针对读写操作是提供Stream标准Java接口;
- 而我这边使用c++实现串口通信方案,针对读写是采用unistd.h中的read和write接口;
04.SerialPort实现原理
4.1 打开串口通信
- FileDescriptor(文件描述符)是一个整数值,用于标识打开的文件或设备。在操作系统中,每个打开的文件或设备都会被分配一个唯一的文件描述符。
- 在C/C++编程中,文件描述符通常用于进行底层的I/O操作,包括读取和写入数据。
- 对于串口通信,文件描述符用于表示打开的串口设备,通过该文件描述符可以进行串口数据的读取和写入。
- 通过使用文件描述符,我们可以使用操作系统提供的底层API函数(如read()和write())来进行串口数据的读取和写入。
- 例如,在Linux系统中,可以使用read(fileDescriptor, buffer, size)函数从串口设备中读取数据,其中fileDescriptor是串口设备的文件描述符,buffer是用于存储读取数据的缓冲区,size是要读取的字节数。
4.2 串口参数介绍
- 在进行串口通信之前,需要设置一些串口参数
- 如波特率(传输速率)、数据位、校验位和停止位等。这些参数用于确保发送和接收端的数据传输一致。
- 每个参数的名词解释
- 1.波特率:用来表示通信速度的参数,它表示每秒钟传送的 bit 的个数,用来衡量数据传输的快慢。如每秒钟传送240个字符,而每个字符格式包含10位(1个起始位,1个停止位,8个数据位),这时的波特率为240Bd,比特率为10位*240个/秒=2400bps
- 2.数据位:通信中数据位的参数。衡量通信中实际数据位的参数,当计算机发送一个信息包,实际的数据往往不会是8位的,标准的值是6、7和8位。
- 3.停止位:表示单个包的最后一位,停止位不仅仅是表示传输的结束,并且提供上位机正时钟同步的机会;
- 4.校验位:串口通信中一种简单的检错方式(偶校验[2]、奇校验[1]、无校验[0])。然没有校验位也是可以的。对于偶和奇校验的情况,串口会设置校验位(数据位后面的一位),用一个值确保传输的数据有偶个或者奇个逻辑高位。
4.3 串口数据传输
- 在串口通信中,数据按照一定的协议和规则进行传输。
- 通常,每个数据帧由起始位、数据位、校验位和停止位组成。
- 起始位标识数据帧的开始,数据位包含要传输的实际数据,校验位用于检测传输错误,停止位标识数据帧的结束。
- 来理解一下数据帧,字符帧由四个部分构成,分别是起始位、数据位、校验位以及停止位。
- 起始位占1位,为逻辑0。数据位占5 ~ 8位,可配置。
- 校验位占1位,可配置为奇校验、偶校验、无校验;配置为无校验时字符帧不包含校验位;配置为奇校验时,数据位中逻辑1的个数为奇数时,校验位的值为逻辑0,否则为逻辑1;配置为偶校验时,数据位中逻辑1的个数为偶数时,校验位的值为逻辑0,否则为逻辑1。
- 停止位占1/1.5/2位,可配置,停止位的值为逻辑1。
- 常用的字符帧格式如下图所示,1位起始位、8位数据位、1位校验位、1位停止位。
image
4.4 串口传输流程
- 串口传输流程
- 在发送端,数据被分成一个个位(bit)进行传输,从起始位开始,逐位发送到串口的发送线上。
- 在接收端,接收器逐位接收数据位,并根据协议和参数进行解析和处理。
4.5 错误检查纠正
- 为了确保数据的可靠传输,串口通信通常使用校验位来检测传输错误。
- 校验位可以是奇校验、偶校验或无校验。接收端会根据校验位检查数据的完整性,并在检测到错误时进行纠正或丢弃。
4.6 串口通信传输编码
- 串口通信本身并不限制数据的编码方式。
- 串口通信只是一种物理接口和传输方式,用于在计算机或设备之间传输数据。数据的编码方式取决于应用程序或协议的设计。
- 通常情况下,串口通信可以传输任何类型的数据,包括2字节编码的数据。
- 例如,可以使用Unicode编码或其他编码方案来表示2字节字符,并通过串口进行传输。在发送端和接收端,需要确保使用相同的编码方式来解析和处理数据。
- 串口通信的传输速率相对较低,特别是在较低的波特率下。对于大量的2字节编码数据,可能会导致传输延迟和效率降低。
- 串口通信中的数据编码是指将要传输的数据转换为适合串口传输的格式。
- ASCII编码:ASCII编码是一种基于英文字母和常用符号的字符编码方式。在串口通信中,可以将数据转换为ASCII码表示,每个字符占用一个字节进行传输。
- 二进制编码:二进制编码是将数据直接转换为二进制形式进行传输。在串口通信中,可以将数据按照位(bit)进行分割,并逐位传输。这种编码方式可以实现更高的传输效率,但需要在接收端进行相应的解码处理。
- Unicode编码:在串口通信中,可以使用Unicode编码来传输多字节字符。
- 根据具体的应用需求和通信协议来选择合适的数据编码方式。在进行数据编码和解码时,发送端和接收端需要使用相同的编码方式和解码算法,以确保数据的正确传输和解析。
05.使用c++实现串口通信
5.1 c++实现打开串口
- 在C++中,可以使用open函数来打开文件或设备。open函数是fcntl.h头文件中的一部分,用于打开文件或设备并返回一个文件描述符。
- open函数的第一个参数是文件名,第二个参数是打开模式和选项的组合。
- 如果open函数返回-1,表示打开文件失败。否则,返回的整数值是一个文件描述符,可以用于后续的文件操作。
// 打开/dev/tty*节点。这个是核心api int fd = open("/dev/ttyMT1", O_RDWR | flags); if (fd == -1) { std::cerr << "无法打开文件" << std::endl; return 1; }
- 关于串口文件打开方式,可采用下面的文件打开模式,具体说明如下:
O_RDONLY:以只读方式打开文件 O_WRONLY:以只写方式打开文件 O_RDWR:以读写方式打开文件 O_APPEND:写入数据时添加到文件末尾 O_CREATE:如果文件不存在则产生该文件,使用该标志需要设置访问权限位mode_t O_EXCL:指定该标志,并且指定了O_CREATE标志,如果打开的文件存在则会产生一个错误 O_TRUNC:如果文件存在并且成功以写或者只写方式打开,则清除文件所有内容,使得文件长度变为0 O_NOCTTY:如果打开的是一个终端设备,这个程序不会成为对应这个端口的控制终端,如果没有该标志,任何一个输入,例如键盘中止信号等,都将影响进程。 O_NONBLOCK:该标志与早期使用的O_NDELAY标志作用差不多。程序不关心DCD信号线的状态,如果指定该标志,进程将一直在休眠状态,直到DCD信号线为0。
5.2 c++实现串口参数
- 在C++中,获取串口的波特率(baud rate)可以使用串口库或操作系统提供的相关函数。
- 使用操作系统函数:如果你直接使用操作系统提供的串口通信函数(如open、tcgetattr等),可以使用termios.h头文件中的函数来获取串口的波特率设置。
#include <iostream> #include <fcntl.h> #include <termios.h> void test() { // 打开串口设备 int fd = open("/dev/ttyUSB0", O_RDWR); if (fd == -1) { std::cerr << "无法打开串口设备" << std::endl; return 1; } // 获取串口属性 struct termios options; if (tcgetattr(fd, &options) == -1) { std::cerr << "无法获取串口属性" << std::endl; close(fd); return 1; } // 获取波特率 speed_t baudrate = cfgetospeed(&options); std::cout << "当前串口波特率: " << baudrate << std::endl; }
- 使用操作系统函数:如果你直接使用操作系统提供的串口通信函数(如open、tcgetattr等),可以使用termios.h头文件中的函数来获取串口的波特率设置。
- 在C++中,可以使用cfmakeraw函数来配置串口为原始模式(raw mode)。cfmakeraw函数是termios.h头文件中的一部分,用于设置串口的属性为原始模式。
- cfmakeraw函数的参数是指向属性结构体的指针,它会将属性设置为典型的原始模式配置,包括禁用输入/输出处理、禁用软件流控制等。
// 配置为原始模式 cfmakeraw(&options);
- cfmakeraw函数的参数是指向属性结构体的指针,它会将属性设置为典型的原始模式配置,包括禁用输入/输出处理、禁用软件流控制等。
- 在C++中,可以使用cfsetispeed函数来设置串口的输入波特率。cfsetispeed函数是termios.h头文件中的一部分,用于设置串口的输入波特率。
- cfsetispeed函数的第一个参数是指向属性结构体的指针,第二个参数是要设置的输入波特率。
//使用cfsetispeed函数来设置串口的输入波特率。 int setISpeed = cfsetispeed(&cfg, b_speed); LOGE("设置输入波特率是:%i",b_speed); if (setISpeed == -1) { LOGE("无法设置输入波特率"); close(fd); return FALSE; }
- cfsetispeed函数的第一个参数是指向属性结构体的指针,第二个参数是要设置的输入波特率。
- 在C++中,可以使用cfsetospeed函数来设置串口的输出波特率。cfsetospeed函数是termios.h头文件中的一部分,用于设置串口的输出波特率。
- cfsetospeed函数的第一个参数是指向属性结构体的指针,第二个参数是要设置的输出波特率。
//可以使用cfsetospeed函数来设置串口的输出波特率 int setOSpeed = cfsetospeed(&cfg, b_speed); LOGE("设置输出波特率是:%i",b_speed); if (setOSpeed == -1) { LOGE("无法设置输出波特率"); close(fd); return FALSE; }
- cfsetospeed函数的第一个参数是指向属性结构体的指针,第二个参数是要设置的输出波特率。
- 在C++中,可以使用tcsetattr函数来设置串口的属性。tcsetattr函数是termios.h头文件中的一部分,用于设置串口的属性。
- tcsetattr函数的第一个参数是文件描述符,第二个参数是操作标志,第三个参数是属性结构体。函数返回值为0表示成功设置属性。
// 使用tcsetattr函数将修改后的属性设置回串口。 // tcsetattr函数的第一个参数是文件描述符,第二个参数是操作标志,第三个参数是属性结构体。函数返回值为0表示成功设置属性。 if (tcsetattr(fd, TCSANOW, &cfg)) { LOGE("无法设置串口属性"); close(fd); return FALSE; }
- tcsetattr函数的第一个参数是文件描述符,第二个参数是操作标志,第三个参数是属性结构体。函数返回值为0表示成功设置属性。
5.3 c++实现读数据
- 在C++中,unistd.h是一个头文件,提供了对POSIX操作系统API的访问。
- 其中包含了一些与文件描述符相关的函数,包括read()函数,用于从文件描述符中读取数据。
- read函数的第一个参数是文件描述符,第二个参数是用于存储读取数据的缓冲区,第三个参数是要读取的最大字节数。函数返回值是实际读取的字节数。
int fd = open("/dev/ttyUSB0", O_RDONLY); // 替换为你的串口设备路径 if (fd == -1) { std::cerr << "Failed to open the serial port." << std::endl; return 1; } char data[1024]; // 用于存储读取的数据 while (true) { ssize_t bytesRead = read(fileDescriptor, data, sizeof(data)); // 读取数据 if (bytesRead == -1) { close(fd); return 1; } std::string receivedData(data, bytesRead); // 将读取的数据转换为字符串 std::cout << "Received data: " << receivedData << std::endl; } close(fd);
5.4 c++实现写数据
- 在C++中,unistd.h是一个头文件,提供了对POSIX操作系统API的访问。
- 其中包含了一些与文件描述符相关的函数,包括write()函数,用于向文件描述符写入数据。
- 使用write()函数将数据写入文件描述符。write()函数的第一个参数是文件描述符,第二个参数是要写入的数据的指针,第三个参数是要写入的数据的长度。write()函数返回实际写入的字节数。
int fd = open("/dev/ttyUSB0", O_WRONLY); // 替换为你的串口设备路径 if (fd == -1) { std::cerr << "Failed to open the serial port." << std::endl; return 1; } std::string data = "Hello, yc, serial port!"; // 要写入的数据 ssize_t bytesWritten = write(fileDescriptor, data.c_str(), data.length()); // 写入数据 if (bytesWritten == -1) { std::cerr << "Failed to write data to the serial port." << std::endl; close(fd); return 1; } std::cout << "Data written successfully." << std::endl; close(fd);
5.5 c++实现关闭串口
- 在C++中,unistd.h是一个头文件,提供了对POSIX操作系统API的访问。
- 其中包含了一些与文件描述符相关的函数,包括close()函数,用于关闭文件描述符。
- 使用close()函数关闭文件描述符。close()函数的参数是要关闭的文件描述符。如果关闭成功,close()函数返回0;如果关闭失败,返回-1。
if (close(fd) == -1) { std::cerr << "Failed to close the serial port." << std::endl; return 1; } std::cout << "Serial port closed successfully." << std::endl;
06.串口通信稳定性
6.1 数据缓冲区管理
- 在接收端,需要合理管理接收缓冲区,以避免数据溢出或丢失。
- 确保接收缓冲区具有足够的容量来存储接收到的数据,并及时处理接收到的数据。
6.2 错误处理和重试
- 错误处理和重传机制:
- 在发生传输错误或丢失数据时,需要有相应的错误处理和重传机制。这可以包括重新发送数据、请求重传或进行错误处理等。
- 延时和超时处理:
- 在串口通信中,可能会出现延时或超时的情况。合理设置延时和超时参数,以确保及时处理数据和错误。
07.串口通信数据收发原理
7.1 接收数据场景和分析
- 串口接收是指,开发板将数据发送给设备,设备读取数据并进行数据分析处理的过程。
- 试想这样的应用场景,我的开发板上安装了一个温度传感器,温度传感器采集的数据长度是3字节(24比特);我需要将开发板采集到的温度信息实时显示在屏幕上,我需要怎么做?
- 这其中需要注意的有下面几点:
- 问题1: 温度传感器是3字节的,如何确定接收到的某一个字节位于三个字节中的哪个位置?
- 问题2: 实时显示要求我需要对每次发送过来的数据做出响应,这种响应需要怎么做?
7.2 为什么设计桢数据
- 先看问题1,如何确定接收某个字节在数据中的位置?
- 其实这也是初学者常遇到的问题,有时候串口发送的数据就像一个堵不住的水管,完全不知道要怎么处理。
- 由于串口协议的限定,导致其每次发送的只能是一个字节,对于多字节的数据【ABC】来说,就只能通过三个字节【A】、【B】、【C】来发送,如下图所示(最左端为最先接收到的字节):
image1 - 这就显然会遇到问题,在任意一个时刻,我没办法确定接收到的数据到底处于【ABC】的哪一个位置;更致命的是,由于物理介质的影响,甚至可能会造成数据的丢失,这就更给数据的接受造成了影响。
- 如何解决这一问题呢?人们开始想到了“打包”的方式,也可以理解为我们常说的“帧”的概念。
- 只要在每组数据的开头加一些标志,表示出这是数据的最开始位置不就好了,即为下图所示(最左端为最先接收到的字节):
image2 - 假设我们设置的这个标志为【FF、FF】,当上位机检测到连续的两个【FF】时,就表示之后的三个字节分别为【A】、【B】、【C】。
- 这其实就解决了这个问题1,实现了对一帧中每一个字节的位置确定。
7.3 如何及时处理数据
- 问题2: 实时显示要求我需要对每次发送过来的数据做出响应,这种响应需要怎么做?
- 先思考一下,如果是你,你会如何设计和实践。解决问题1后,我们当然可以利用顺序执行的方式,来实现对串口数据的一次读取以及数据处理。
- 如何实现当每一次检测到特定信号,就调用一次数据处理函数呢?
- 这就要先理解一下MATLAB中serialportlist的使用逻辑了,整体来说serialportlist是对serial的升级版本(在帮助页面也有提到),其通过构建SerialObject对象的方式,来实现串口参数的设置以及读写。
- 具体细节可以参考MATLAB文档serialport,太全面的参数设置过于冗余,不在本文讨论范围内。这里主要介绍两个比较重要的概念缓冲区以及回调函数。
- 为什么要设计缓冲区?
- 在serialPort中,缓冲区是自动存在于SerialObject对象中,但是有时使用时不需要针对性设置缓冲区的大小。
- 可以理解为一个长度固定的FIFO队列,当检测到特定信号的时候,将串口传入的每一个字节的数据,按顺序保存在里面,当长度满了之后,就不再继续在里面添加新的数据了。
- flush(SerialObj)可以用来清空缓冲区,常常用在串口对象初始化的时候。
- 如何理解回调函数?
- 这个是解决问题2的关键,回调函数可以理解为一个开关被触发后需要进行的操作(或者简单理解为单片机的中断处理函数)。
- 我们可以通过SerialObject的对象设置,来设置检测到什么信号(这个信号是作为一帧的结尾)的时候,执行回调函数。
- 举个方案A作为例子,我们可以设置检测到【FF FF】信号的时候,执行三个字节的数据读取。(尽管不这样用,后面会说为什么)
image3 - 如上图所示,当我们按照上面方案A的方式,设置回调函数的触发条件,有什么问题呢?每当检测到【FF FF】的时候,就会触发回调函数。
- 看似没问题,但是此时一帧的组合已经从【FF FF A B C】变成了【A B C FF FF】,因为我们提到回调函数敏感的是一帧的结尾。
- 检测到【FF FF】时,下一个字节显然就是【A】。这其实是不规范的,我们不能理所当然地认为每一帧都是传输正确的。
- 举个例子:【 A B C FF FF】【 A B C FF FF】【 FF FF】【 A B C FF FF】【 A B C FF FF】
- 中间红色的ABC表示因为数据线接触不良导致的传输错误,如果具有固定帧头的话,或许帧头也会出现错误,从而直接跳过这一帧错误的信号【 FF FF】。
- 因此必须通过固定的帧头来确定此时传输的是否是完整的数据。
7.4 完整桢包含桢头桢尾
- 在MATLAB中给出了configureTerminator的方法,可以编辑SerialObject需要检测到的帧尾信号。详细解释可以看configureTerminator官方文档,其中有这样的介绍:
- configureTerminator(t,terminator) defines the terminator for both read and write communications with the remote host specified by the TCP/IP client t. Allowed terminator values are "LF" (default), "CR", "CR/LF", and integer values from 0 to 255. The syntax sets the Terminator property of t.
- 这里提到,我们可以设置需要检测帧尾信号为“LF”、“CR”、“CR/LF”或一个0-255的整数(刚好对应了8位无符号数,也就是一字节)。
- 按照上面的说明,我们可以对之前的帧进行下图的修改,加上帧尾(最左端为最先接收到的字节):
image4 - 这样,我们就可以利用检测帧尾(橙色部分),来实现对回调函数的调用啦。但是新的疑问又诞生了:我理解0-255的数字怎么发送,但是这毕竟是单字节的,会不会造成数据读取混乱?
- 上文提到的“LF”、“CR”、“CR/LF”这三个又是什么?(这也是困扰了我一段时间的问题)
- 在计算机还没有出现之前,有一种叫做电传打字机(Teletype Model 33,Linux/Unix下的tty概念也来自于此)的玩意,每秒钟可以打10个字符。
- 但是它有一个问题,就是打完一行换行的时候,要用去0.2秒,正好可以打两个字符。要是在这0.2秒里面,又有新的字符传过来,那么这个字符将丢失。
- 研制人员想了个办法解决这个问题,就是在每行后面加两个表示结束的字符。一个叫做“回车”,告诉打字机把打印头定位在左边界;另一个叫做“换行”,告诉打字机把纸向下移一行。这就是“换行”和“回车”的来历。
- Unix/Linux系统里,每行结尾只有“<换行>”,即"\n"。这个叫做LF,对应ASCII码是0x0A
- Windows系统里面,每行结尾是“<换行><回车 >”,即“\n\r” 。这个叫做CR/LF,对应ASCII码是0x0A
- Mac系统里,每行结尾是“<回车>”,即"\r"。这个叫做CR,对应ASCII码是【0x0D 0x0A】
- 是不是感觉豁然开朗?那我们就可以理所当然的将之前的图改为下面的样子(最左端为最先接收到的字节):
image5
- 再次回过头来总结一下为何需要桢头和桢尾这种设计?
- 回想一下我们的思路,因为要实现多字节读取,所以需要给一个固定的帧头用来确定每个字节的位置;为了提供一个可以激活回调函数的信号,并且不影响帧头的存在,我们需要添加一个帧尾。
- 结合configureTerminator中的设置信号,我们发现可以使用“LF”、“CR”、“CR/LF”或者0-255的数字作为帧尾让回调函数激活。
- 通过查阅原来前面的三个“LF”、“CR”、“CR/LF”说的是换行符的ASCII码,我们可以使用开发板让他们发出对应的十六进制数据来表示。
7.5 发送数据场景和分析
- 串口发送是指,设备将需要发送的数据(一般是指令或者参数设置信息)整合好,发送给开发板的过程。
- 问题1: 如何将数据进行分割并发送。
参考博客
- MATLAB :【11】一文带你读懂serialport串口收发原理与实现
- https://blog.csdn.net/Alex497259/article/details/125922427
- Android端串口通讯开发整理
- https://juejin.cn/post/6844903892208058381#heading-5
- 深入理解串口通信原理及应用
- https://blog.csdn.net/qq_58288010/article/details/135186421