编程进阶网编程进阶网
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 基础组成体系
  • 程序编程原理
  • 异常和IO系统
  • 六大设计原则
  • 设计模式导读
  • 创建型设计模式
  • 结构型设计模式
  • 行为型设计模式
  • 设计模式案例
  • 面向对象思想
  • 基础入门
  • 高级进阶
  • JVM虚拟机
  • 数据集合
  • Java面试题
  • C语言入门
  • C综合案例
  • C标准库
  • C语言专栏
  • C++入门
  • C++综合案例
  • C++专栏
  • HTML
  • CSS
  • JavaScript
  • 前端专栏
  • Swift
  • iOS入门
  • 基础入门
  • 开源库解读
  • 性能优化
  • Framework
  • 方案设计
  • 媒体音视频
  • 硬件开发
  • Groovy
  • 常用工具
  • 大厂面试题
  • 综合案例
  • 网络底层
  • Https
  • 网络请求
  • 故障排查
  • 专栏
  • 数组
  • 链表
  • 栈
  • 队列
  • 树
  • 递归
  • 哈希
  • 排序
  • 查找
  • 字符串
  • 其他
  • Bash脚本
  • Linux入门
  • 嵌入式开发
  • 代码规范
  • Markdown
  • 开发理论
  • 开发工具
  • Git管理
  • 百宝箱
  • 开源协议
  • 技术招聘
  • 测试经验
  • 职场提升
  • 技术模版
  • 关于我
  • 目标清单
  • 学习框架
  • 育儿经验
  • 我的专栏
  • 底层能力
  • 读书心得
  • 随笔笔记
  • 职场思考
  • 中华历史
  • 经济学故事
  • 01.IC实体卡介绍说明
  • 02.M1实体卡读写技术
  • 03.CPU实体卡读写技术
  • 04.PSAM卡读写技术
  • 05.键盘按键监听实践
  • 06.USB开发设计实践
  • 08.设备串口通信实践

09.串口数据发送原理

目录介绍

  • 07.串口通信数据收发原理
    • 7.1 接收数据场景和分析
    • 7.2 为什么设计桢数据
    • 7.3 如何及时处理数据
    • 7.4 完整桢包含桢头桢尾
    • 7.5 发送数据场景和分析

07.串口通信数据收发原理

7.1 接收数据场景和分析

  • 串口接收是指,开发板将数据发送给设备,设备读取数据并进行数据分析处理的过程。
    • 试想这样的应用场景,我的开发板上安装了一个温度传感器,温度传感器采集的数据长度是3字节(24比特);我需要将开发板采集到的温度信息实时显示在屏幕上,我需要怎么做?
  • 这其中需要注意的有下面几点:
    • 问题1: 温度传感器是3字节的,如何确定接收到的某一个字节位于三个字节中的哪个位置?
    • 问题2: 实时显示要求我需要对每次发送过来的数据做出响应,这种响应需要怎么做?

7.2 为什么设计桢数据

  • 先看问题1,如何确定接收某个字节在数据中的位置?
    • 其实这也是初学者常遇到的问题,有时候串口发送的数据就像一个堵不住的水管,完全不知道要怎么处理。
    • 由于串口协议的限定,导致其每次发送的只能是一个字节,对于多字节的数据【ABC】来说,就只能通过三个字节【A】、【B】、【C】来发送,如下图所示(最左端为最先接收到的字节):
    • image1
      image1
    • 这就显然会遇到问题,在任意一个时刻,我没办法确定接收到的数据到底处于【ABC】的哪一个位置;更致命的是,由于物理介质的影响,甚至可能会造成数据的丢失,这就更给数据的接受造成了影响。
  • 如何解决这一问题呢?人们开始想到了“打包”的方式,也可以理解为我们常说的“帧”的概念。
    • 只要在每组数据的开头加一些标志,表示出这是数据的最开始位置不就好了,即为下图所示(最左端为最先接收到的字节):
    • image2
      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
      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
      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
      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
贡献者: yangchong211