- 07.串口通信数据收发原理
- 7.1 接收数据场景和分析
- 7.2 为什么设计桢数据
- 7.3 如何及时处理数据
- 7.4 完整桢包含桢头桢尾
- 7.5 发送数据场景和分析
- 串口接收是指,开发板将数据发送给设备,设备读取数据并进行数据分析处理的过程。
- 试想这样的应用场景,我的开发板上安装了一个温度传感器,温度传感器采集的数据长度是3字节(24比特);我需要将开发板采集到的温度信息实时显示在屏幕上,我需要怎么做?
- 这其中需要注意的有下面几点:
- 问题1: 温度传感器是3字节的,如何确定接收到的某一个字节位于三个字节中的哪个位置?
- 问题2: 实时显示要求我需要对每次发送过来的数据做出响应,这种响应需要怎么做?
- 先看问题1,如何确定接收某个字节在数据中的位置?
- 其实这也是初学者常遇到的问题,有时候串口发送的数据就像一个堵不住的水管,完全不知道要怎么处理。
- 由于串口协议的限定,导致其每次发送的只能是一个字节,对于多字节的数据【ABC】来说,就只能通过三个字节【A】、【B】、【C】来发送,如下图所示(最左端为最先接收到的字节):
image1- 这就显然会遇到问题,在任意一个时刻,我没办法确定接收到的数据到底处于【ABC】的哪一个位置;更致命的是,由于物理介质的影响,甚至可能会造成数据的丢失,这就更给数据的接受造成了影响。
- 如何解决这一问题呢?人们开始想到了“打包”的方式,也可以理解为我们常说的“帧”的概念。
- 只要在每组数据的开头加一些标志,表示出这是数据的最开始位置不就好了,即为下图所示(最左端为最先接收到的字节):
image2- 假设我们设置的这个标志为【FF、FF】,当上位机检测到连续的两个【FF】时,就表示之后的三个字节分别为【A】、【B】、【C】。
- 这其实就解决了这个问题1,实现了对一帧中每一个字节的位置确定。
- 问题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】。
- 因此必须通过固定的帧头来确定此时传输的是否是完整的数据。
- 在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码,我们可以使用开发板让他们发出对应的十六进制数据来表示。
- 串口发送是指,设备将需要发送的数据(一般是指令或者参数设置信息)整合好,发送给开发板的过程。
- 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