串口接收数据代码(如何写一个健壮且高效的串口接收程序)
串口接收数据代码(如何写一个健壮且高效的串口接收程序)为了更好的理解接下来的知识点,鱼鹰将设计一个串口框架,让道友心中有一个参考方向。内容很多,鱼鹰慢慢写,道友您也请慢慢看。3、 数据帧检查4、 串口空闲5、 通信吞吐量
学单片机的大概最先、最常写的通信程序应该就是串口程序了,但是如何写出一个健壮且高效的串口接收程序呢?接下来鱼鹰将根据多年的开发经验教你如何编写串口接收程序(可在公众号获取个人编写的串口接收源码)。
本篇文章包含以下内容,很长,但干货满满,就看你能吸收多少了:
1、 传入参数指针
2、 互斥锁释放顺序
3、 数据帧检查
4、 串口空闲
5、 通信吞吐量
内容很多,鱼鹰慢慢写,道友您也请慢慢看。
为了更好的理解接下来的知识点,鱼鹰将设计一个串口框架,让道友心中有一个参考方向。
本篇重点在于解决如何写一个健壮、高效的串口接收数据,发送与接收处理过程略讲。
帧格式
先聊聊帧格式,一般来说,一个数据帧有以下几部分内容:
帧头
帧头用于分辨一个数据帧的起始,这个帧头必须足够特殊才行,因为它是分辨一个帧的起始,那么什么样的帧头是足够特殊的数据呢?保证这个数据在一个帧内最好只出现一次的数据,那就是帧头,比如 0x55、0xAA 之类的。而且最好有两个字节以上,这样帧头才更加独一无二。
但是数据域内的数据你是没办法保障不包含和帧头一样的数据。
那么如果不凑巧,除了帧头外其他部分也有这样的两个字节的帧头,那会出现什么问题?
几乎不会出现问题。因为一般来说数据都是一帧一帧发送的,只要你前面的数据帧传输正确,那么即使下一帧的数据中有和帧头一样的数据(包括帧头)也没有问题,因为帧头判断已经在开始就判断成功了,就不会继续判断后面的数据是否是帧头了。
那么为什么说是几乎,因为如果上一帧数据接收错误,那么程序必须再找一次帧头才行(单字节接收时是如此,采用空闲中断的话就不需要这么麻烦),这就导致找帧头的时候在帧头数据之外寻找了,很可能这些数据就有帧头。
但是即使帧头数据之外的假帧头真的存在,也没关系,还有第二重保障,那就是校验,即使找到了一个错误的帧头,那么数据校验这一关也很难过去,所以放宽心。
如果校验也凑巧通过了,那还有第三重保障:帧尾。应该到不了这里吧,毕竟这比中还难。
又要上一帧数据接收错误,还要当前帧除了帧头之外还有帧头,另外你还能跳过校验的检查(还有功能字、长度信息的检查),太难了。所以只要通过了这些检查,你就可以认为这个数据帧是可用的了。所以一帧数据接收错误,导致的问题最多只是丢失了这帧数据,对后续接收是不会有影响的(前提是你这个接收程序设计的足够好),发送端在发送超时后再发送一次即可,所以重发机制很重要。
事实上,如果你采用串口空闲中断,帧头、帧尾都可以不用,但一般来说,帧头都会保留,帧尾可以不需要,这是为了当单片机没有串口空闲中断时考虑,当然也可能有其他考虑,所以帧头得保留。
功能字
功能字主要用于说明该数据帧的功能,当然也可以作为函数指针的索引,一个索引值代表了一个具体功能,据此可找到对应的功能函数。
比如,设计一个函数指针数组,通过功能字进行索引,进而跳转到对应的功能函数中处理。
特别注意的是,设计功能字的时候,要考虑兼容性,对数据帧的功能进行划分,不要想到一个算一个,功能字也不要随便安排,不然在以后增加数据帧的时候会很麻烦。
比如说,只有一个字节的功能字,前四位作为一个大类,后四位作为大类中具体类。这样就可以将系统数据通信帧分为 16 个大类,每个大类下有 16 个可用的具体类,当你增加功能字的时候,就可以根据你的设计来确定属于哪个大类了,然后再插入进去。这样在管理、维护这些通信数据时你会发现很方便。
这个思想其实在 ARM 内核的中断系统和设计 uCOS II 任务优先级的时候都有,而鱼鹰在设计项目的通信协议的时候就是运用了这些思想。
(图片来源于《权威指南》)
长度
长度信息也是一个非常关键的数据,别小看了它,因为它,鱼鹰用了将近一个星期的时间才把一个 HardFaul 问题解决了,虽然这个程序 bug 不是我写的(鱼鹰一直用的是串口空闲接收方式,这个 bug 自然而然就跳过了),但确实很容易出错。
因为它是决定了你这个数据域长度的关键信息(一般长度信息代表数据域的长度,而不包含其它部分长度),也是这个数据帧的长度信息(加上固定字节长度就是帧长度了),更是接收程序还要接收多少数据的关键信息(对于空闲中断接收方式不算关键,这里的不关键是指不会造成程序异常问题)。
比如说你的程序刚好将帧头、帧尾、功能字判断完毕,然后中断程序因为种种原因导致没有及时接收串口数据,那么你可能得到的就是错误的数据,然后这个错误的长度数据就可能导致你的栈帧或者全局变量被破坏(单字节接收情况下就可能出现,因为鱼鹰碰到过),这是很严重的事情。所以在接收数据域的数据之前一定一定要判断这个长度信息(空闲中断除外)是否合法,不合法的话及时扔掉这帧数据,开始下一帧的数据检查。
所以为了保证及时接收数据,最好采用 DMA 传输。
数据域
这个没啥好说的,就是整个帧你真正需要发送的数据。而为了让你的发送函数能接收各种类型的数据,那么把参数类型设置为 void * 会是不错的选择。
校验
一个数据在接收过程中可能会被干扰,导致接收到错误的数据,那么如何保证这帧数据的完整与准确性呢,就在校验这一关了。
校验有很多方式,和校验、CRC 校验等(奇偶校验是针对一个字节的,不是数据帧)。
和校验算法简单,CPU 运算量小,累加最后只取最低字节即可(注意不是高字节,想想为什么),或者保存累加和的变量就是一个字节空间,这样就不需要额外操作了。
CRC 校验,这个算法复杂,理解起来比较困难,但一般来说可以直接拿来用,因为它是对每一位(bit)进行校验,所以纠错率很高,几乎不存在发现不了的数据错误,但正因为对每一位进行检查,所以 CPU 运算量较大,但是有的单片机是可以硬件计算 CRC 校验值的(比如 stm32)。不过现在 CPU 运算速度都挺快的,软件运算也是可以接受的。
那么该怎么校验呢?是从帧头开始到数据域部分,还是说直接校验数据部分?其实都可以,区别就是运算量问题,不过问题不大(最好是从头开始校验,以保证整帧数据的准确性)。
帧尾
前面说了,帧尾在空闲中断中可以不用,RXNE 中断接收时其实也可以不用,当然也可以加上,好处就是当你用串口助手查看数据流时,可以观察出一帧数据是否发送完整了。
最后再说说为什么在数据域前面设计四个字节大小,除了协议本身需要外,还有一个原因就是强制类型转化需要,我们知道,一般来说,赋值时都有字节对齐的限制(实际上有的 CPU 可以不对齐进行赋值),stm32 是 32 位的,那么四字节对齐是最合适的,这样就可以直接将我们收到的数据转化为需要的数据类型了。
传输过程
聊完了帧格式,再从大的方向看串口的传输过程:
当发送端发送第一帧数据包时,接收端通过某种方式接收(串口接收非空 RXNE 中断、串口空闲 IDLE 中断),为了让串口能够触发空闲中断,必须在发送端两个发送帧之间插入一段空闲时间(就是在此时间内不发数据,红色部分),保证空闲中断的准确触发。
同理,为了让发送端也能正常接收接收端的数据,也需要控制接收端的发送,不能在返回一帧数据时立马发送下一帧数据,不然触发不了发送端的空闲中断。
事实上,有些程序员设计的发送、接收过程比这个简单一些。即只有当接收端接收到一帧数据并返回一帧数据之后,发送端才能继续发送数据,这样一来,我们只需要控制好接收端的频率,就可以控制整个通信过程,也能控制通信频率。
但为什么还要设计成第一种传输情况呢?这是为了充分利用串口,增大数据吞吐率(这个后面再说)。
另外,不知道你是否观察到图中的每个数据帧占用的时间是不一样的,这是因为每个数据帧不可能都是一样长的,它们是不定长的数据包,所以你的定时不能从发送开始定时,而是从发送完成后开始定时控制空闲时间。
下集精彩,串口的软件设计,喜欢的话记得关注鱼鹰哦!