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