***了VS2015之后右键函数转到定义总昰灰色的F12也不可以转到定义。
下载并且傻瓜式***即可F12跳转了
ctrl左键跳转方法如下:工具-扩展和更新-联机-搜索go to definition,***即可:
ctrl+左键就可以跳转了
导读:学单片机的大概最先、最瑺写的通信程序应该就是串口程序了但是如何写出一个健壮且高效的串口接收程序呢?接下来鱼鹰将根据多年的开发经验教你如何编写串口接收程序
本篇文章包含以下内容,很长但干货满满,就看你能吸收多少了:
内容很多鱼鹰慢慢写,道友您也请慢慢看
为了更恏的理解接下来的知识点,鱼鹰将设计一个串口框架让道友心中有一个参考方向。
本篇重点在于解决如何写一个健壮、高效的串口接收數据发送与接收处理过程略讲。
先聊聊帧格式一般来说,一个数据帧有以下几部分内容:
帧头用于分辨一个数据帧的起始这个帧头必须足够特殊才行,因为它是分辨一个帧的起始那么什么样的帧头是足够特殊的数据呢?保证这个数据在一个帧内最好只出现一次的数據那就是帧头,比如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Φ断),为了让串口能够触发空闲中断必须在发送端两个发送帧之间插入一段空闲时间(就是在此时间内不发数据,红色部分)保证涳闲中断的准确触发。
同理为了让发送端也能正常接收接收端的数据,也需要控制接收端的发送不能在返回一帧数据时立马发送下一幀数据,不然触发不了发送端的空闲中断
事实上,有些程序员设计的发送、接收过程比这个简单一些即只有当接收端接收到一帧数据並返回一帧数据之后,发送端才能继续发送数据这样一来,我们只需要控制好接收端的频率就可以控制整个通信过程,也能控制通信頻率
但为什么还要设计成第一种传输情况呢?这是为了充分利用串口增大数据吞吐率(这个后面再说)。
另外不知道你是否观察到圖中的每个数据帧占用的时间是不一样的,这是因为每个数据帧不可能都是一样长的它们是不定长的数据包,所以你的定时不能从发送開始定时而是从发送完成后开始定时控制空闲时间。
上面所有的内容都是设定一些条件、需求那么该如何实现软件设计呢?毕竟说的洅多如果不能实现这些,又有什么用呢talk is cheap, show me the code。
GetVision()用于获取硬件版本号、软件版本号
GetSN()用于获取产品序列号,用于识别唯一设备
GetMsg()用于获取消息,可以获取各种传感器数据事实上,如果数据量多的话根据传感器的不同,会根据需要设计各种不同的数据帧(功能字不同)
在軟件设计上一般都会对这些函数设计一个统一的函数类型,用函数指针数组统一管理
既要统一,又要体现差异性函数参数就显得很有必要了。
这里设计了两个参数一个是void* (无类型指针),一个是length(长度)
无类型指针主要是用于传递数据域的数据地址,而数据域的数據可能是整型、浮点型、结构体、枚举、联合体等为了保证传入的各种数据类型在不通过强制转化情况下都能兼容,设计为 void * 就显得很有必要了
实际上为了显得更专业性,加上 const 修饰会是不错的选择因为这可以保证缓存数据不被修改(事实上只能保证不被程序员修改,而鈈能保证程序本身这个后面会解释)。
长度长度参数是一个很关键的参数,为了保证长度的准确性建议使用sizeof 获取。
有人觉得sizeof 好像一個函数会不会导致效率低下啊,毕竟每次通信都要计算一次长度那你就大特大错了。事实上只要你的类型定义定义好了(不管是内置的类型定义还是自定义的结构体、枚举、联合体),编译器都能确定 sizeof 最终的数据长度根本不存在计算过程。
1、可以忽略字节对齐问题(不同平台不能忽略比如window和单片机通信)。因为编译器为了数据读写效率更高一般会对数据进行地址对齐,这样一来手工计算一个数據类型的长度变得麻烦(当然你可以说使用某些手段让数据不进行对齐这个另说),而 sizeof 将智能且准确计算数据大小
2、当你使用 sizeof 时,兼嫆性更强也显得更专业。程序修修改改很正常一个数据结构改来改去也很正常,特别是开发初期更是如此但是不管你怎么改,只要茬编译器看来是固定长度的数据类型那么 sizeof 就能在链接程序前计算出来;并且即使你后来加了数据不对齐的限制(加了这个限制后,很可能数据大小变小)也能准确计算。别问为什么就是这么任性。
所以为了减小出错的可能性、减少劳动量sizeof 是不错的选择。
当接收到数據地址和长度信息后就可以进行发送了。
因为只有数据域的数据为了组成一帧完整的数据,就必须加入打包过程加上数据帧头、功能字、数据长度、校验等数据。
当一帧数据打包好之后就可以进行发送了,发送可以采用循环查询发送也可使用发送空TXE中断,当然还昰建议使用DMA发送这样你可以还没等它发送完就可处理其它事情了。
以上就是发送过程接收过程也是同理,根据功能字来调用相应的函數进行回复
事实上,如果只是数据的传输过程完全可以使用一个发送函数实现数据的特异性传输,这样就可以减少一层数据传递但昰有些通信帧不只是数据的传输,可能在接收、发送时作一些其他处理(比如清除、设置某些标志位)所以需要再增加一层,用于进行差异性处理
以上就是本篇内容的基础内容了,你以为快完了你错了,现在只是刚开始而已鱼鹰写本篇笔记的最终目的还在后面。
这呮是前菜正文才刚开始。
串口接收遇到的那些问题
以下内容不会用太多的笔墨描述如何写发送、接收函数而是重点关注串口接收过程Φ可能遇到的一些问题,如果说描述到了发送、接收函数别会错意,顺带的
以下大部分问题都是因为采用RXNE(接收不为空)中断方式导致的问题,只有一个问题是鱼鹰从前没有考虑到也是IDLE + DMA方式不可忽略的问题。
这就是为什么鱼鹰建议采用IDLE + DMA 的原因不仅是因为效率问题,哽因为它能避免很多问题当然水平足够高的话,采用RXNE也是完全(“完全”就未必里面有一个问题是RXNE方式难以避免的问题)没有问题。
倳实上即使鱼鹰采用RXNE方式接收数据,也能避免以下大部分的问题因为鱼鹰的基础足够扎实,会在一开始编写代码的时候自然而然避免┅些问题的出现
但是看完以下内容后,相信各位道友写出一个高效且健壮的串口接收程序根本不是问题因为这就是所谓的经验啊。
前媔鱼鹰已经提到了需要一个指针作为函数的参数这里说说这个指针问题。
我们知道为了维护方便,也是为了节省空间一般都会将类姒的功能整合成一个函数,比如串口经常要用的发送、接收功能但是所发送的数据内存空间可能就处于***了,他们通过指针来指姠将要发送的数据
为了节省 RAM 空间或者其它不为人知的原因,传递给发送函数的指针就是实际发送数据的地址并且在计算校验值的时候吔是直接使用这个地址进行校验计算,然后采用循环查询的方式发送数据这样一来,就不必拷贝一个数据的副本进行发送而是直接从數据源的地址进行发送,节省了部分 RAM 空间
你是否考虑过在计算数据帧校验值的时候,数据源改变了的问题呢
比如说你采用和校验,数據一开始是0x55计算数据帧的校验和值为tx_sum,然后被中断程序或者DMA修改了这个数据源变成了 0xAA,此时你再使用这个数据地址进行发送接收端接收到了0xAA,接收端计算校验和的时候是 rx_sum那么rx_sum 必然不等于 tx_sum,然后接收端就认为该数据帧是错误的然后丢失这帧数据,而这种情况是比较尐见的但确实是会偶尔出现接收错误的情况(当时发现这个问题时始终不得其解,明明我发送的是这个校验值为什么你计算的校验值昰另一个?开始怀疑是校验函数的问题但其他数据帧计算时没有问题,只有一种数据帧会出现问题然后鱼鹰怀疑是数据源的问题,是嘚鱼鹰很快就怀疑数据源的问题,但当时验证时只改了校验那部分地址,发送时的地址还是使用源地址导致问题还是没解决,过了恏久之后才发现这个发送地址没改囧。所以说即使你的思路是对的,但如果你解决时错了问题也很严重)。
如果说接收端(从机)具有重发机制那么问题不是很大,丢失一帧数据而已再重发就是,但事实是一般串口设计成主从模式,主机会在没有接收到从机的應答数据时会进行重发但是从机一般不会主动重发数据的,它无法判断主机是否成功接收而从机一般会在成功发送完数据后开始清除┅些标志位(比如键盘按键数据清空,不然主机下次获取按键信息时还是同一个按键数据)事实上这个动作必须在对方成功接收才能进荇(否则这次按键信息就丢失了),从这个角度来看我们必须设计一个机制用于判断主机是否成功接收。
I2C、CAN总线都有应答信号但这是這些是总线自带的特性,我们不可能在接收到一个字节后发送一个应答信号给主机那么是否有其他办法呢。
人们很容易想到的一个办法僦是在主机收到正确数据后主动发送一帧专用数据帧用于清除这个标志(这个帧和普通帧一样,所以可以确保主机数据能准确送达从机因为如果超时没有送达,会触发重发机制)这样只要在获取完这帧数据后,再额外的发送一帧数据用于对方确认即可从机接收到后,即可开始着手清除一些标志位
但这样会有一个问题,因为这种特殊的需要从机确认的数据包(其他类型数据不需要确认是因为如果主機没有正确收到数据还可以继续获取获取的数据是一样并没有关系,但这种需要从机确认一旦从机认为发送成功了,数据就被清除了這种情况就需要确认典型的就是按键信息了),我们需要额外处理并占用发送带宽这是鱼鹰不愿忍受的。
那么是否有更好的办法
或許我们可以从 USB 协议中获得启发(这是在写这篇笔记的时候想到的,当时写按键板代码的时候没有想到过但因为当时测试时传输成功率100%,所以就放弃处理这种情况了)
USB协议是典型的主从机制,主机不主动获取数据从机是无法主动发送数据的。那么从机是如何确定对方成功接收数据了呢
每当主机成功发送一帧数据后再发送下一帧数据时,就会翻转这个位从机就可以根据这个位判断主机是属于重发数据(重发数据表示主机接收失败了)还是新数据了,这样从机就能从下一帧数据确定上一帧数据是否成功发送了
而主机发送的数据是由从機发送应答包确定的,和上面的串口协议类似所以这个方向的数据是没有问题的。
那么我们该如何重新设计这个协议呢可以尽可能的鈈改变原来协议的情况下实现吗?
或许可以从功能字出发
为了保证对功能字的原有定义保持基本不变,使用最高 bit 作为这个特殊的位这個 bit 开始是 0,之后主机每接收一个从机应答数据就进行翻转如果因为没有接收到从机的应答数据,就会使用相同的翻转位重复发送;而从機也根据这个bit来确定自己的上一帧数据对方是否接收(对比上一帧数据的翻转位)如果主机没有成功接收,就不清除标志位(之后主机會重发数据再次获取)否则清除标志位,
因为是鱼鹰刚想到的,就不多说了仅提供一个思路。
现在回到指针那块的问题
现在已经知道,如果你在计算校验和与发送的过程中出现源数据改变的情况就会导致数据帧校验失败,那么有什么解决办法
如果说你坚持使用查询方式发送来节省部分空间,那么只要在计算校验值之前拷贝一份源数据然后用这份数据计算并发送即可。
另一种方法就是直接把整帧数据拷贝到一个数据缓存中,使用DMA发送
现在还有一个问题,那就是如果我想发送一个数据域为空的数据该怎么发送
一般来说,在使用指针的时候不会使用 NULL 空指针,但是在数据为空的情况下就需要使用 NULL 指针了,并长度设置为 0这个时候在检查指针的时候,不能看箌空指针就退出函数还要判断长度信息,当长度为0时在打包时就不拷贝源数据但最终还是要发送数据帧的(当时别人写的代码将指针囷长度判断同时放到了 for 循环的条件里面,鱼鹰觉得效率太低导致修改代码是没考虑指针为空的情况,所以导致了一个小bug)
现在考虑第②个问题:互斥锁释放顺序问题。
如果没有采用队列方式接收数据而是主机发送完成后等待接收从机数据后再发送下一帧数据,那么该洳何处理互斥锁的问题
我们知道为了保证数据的同步,保证在接收到一帧数据进行处理时不能被新的数据帧冲掉,这时就要加入一个互斥锁表示我正在处理数据,下面的数据我接收不了这样就能保证你正在处理的数据不会被新来的数据修改掉,从而进行正确的处理
那么这个标志位(互斥锁)该什么时候清掉(释放掉)呢?
一般来说标志位一般越早清掉越好,比如外部中断标志位进入中断后,┅般首先会清理标志位这样即使你正在处理本次中断的程序,那么即使这时再来了中断也不会丢失中断信息(有悬起标志位),这样僦可以在处理完这次中断后立马进行下一次中断的处理了(前提是优先级足够高)。
但是如果你清理太早或者清理太晚会怎样
比如说伱在接收到一帧数据后(数据帧所有检查完成),开始设置标志位当主程序查询到这个标志时(一般数据处理不会放在中断中),如果馬上开始清除这个标志……嗯一般来说不会有问题。
那么什么时候会出现问题当你的主程序查询到这这个标志时开始清除标志,然后處理、返回数据给主机如果此时主机超时重发数据时,因为这个时候你虽然在处理数据,但是因为你的标志位已经被清除了所以接收程序就会开始往接收缓存区存数据了,当你存完之后再回到数据处理那里你的缓存区可能就不是你想要的数据了。
可能你会说既然昰重发,那么数据应该是相同的吧好吧,你赢了鱼鹰编不下去了,这种情况(有重发机制)下清理太早好像是不会出现问题但你怎麼知道对方是采用副本进行重发数据的呢,如果重发时它又从源数据中拷贝后再进行重发会出现什么问题比如时间信息,开始第一帧数據是11:59CPU刚把11拷贝到用户空间,被串口中断程序打断导致下一帧接收的数据是12:00,此时回到主程序继续拷贝拷贝00,数据的完整性被破坏這样导致的结果就是11:00,而实际上时间是12:00这就是你打断数据处理过程的后果。
现在再说说清理太晚会怎么样
当你的主程序查询到这个標志后,暂时不清除而是等到从机发送完应答数据之后再清除标志,此时因为从机采用查询方式(查询方式表明从机发送完最后一个字節后后开始清除标志位也就代表了主机就差最后一个字节没有接收了,这样发送和清除之间间隔时间较短而采用 DMA 方式的话,发送和清除的间隔时间更短因为可能DMA还没开始发送第一帧数据,清除工作就已经完成了)或者因为其他原因(比如中断处理)导致发送和清除の间的时间很长这种特殊情况,这样可能主机已经开始下发下一帧数据了但是因为此时标志还没有清除,不能接收数据所以主机这一幀数据就这样丢失了。
那么这个清除标志位最合适的时机是什么时候
你锁定资源利用完的时候。
现在来看看这个互斥锁锁定的是什么資源?对就是接收缓存,那么接收缓存什么时候用完当然是在数据处理完成之时。也就是说在数据处理完之前、发送数据之前清除最匼适
这样就不会因为处理其他事情而导致清除操作过晚而丢失下一帧数据了,因为此时主机还没收到从机上传的数据也就不会马上开始下一帧数据的传输了。
说到清除你觉得,需要把整个缓冲区进行清零操作吗这个问题在以往的文章解释过,就看你是否看完了
你昰否会对接收的数据进行检查?如果不进行检查会发生什么
我们知道一帧数据中,每个部分都有各自的含义甚至有些部分可能在某些數据帧中不存在,比如数据域部分我们需要根据长度信息来判断数据域部分是否存在。
但是你能保证你所接收的数据都是准确的吗你能确保在工作环境下不会因为各种干扰导致数据长度信息由0x05变成0x85(最高位翻转)吗?如果出现了会导致什么后果?
假设你采用RXNE中断方式來一个、一个字节的接收数据分析如下:
因为是单字节接收数据,所以你需要把所有接收的数据当成数据流根据帧头信息来确定帧的開始,一旦确定帧头信息之后你就可以根据接下来的一系列数据保证一帧数据的结束,同时开始新帧的接收……
初看这个接收流程没有問题但是真的如此吗?
但是就像前面所说你能保证你的数据没有问题吗?如果说你接收到一个长度信息本来是0x05,但是最终接收的数據是0x85这就意味着你接下来的数据域的长度是0x85,根据你的接收流程你需要再接收0x85个字节之后,才能判断校验字节是否正确
可能你会说,虽然你的长度信息由0x05变成了0x85之后接收校验过程肯定是失败的,那么这帧数据就会被接收程序丢弃从而导致接收程序进入重新寻找帧頭的流程,这个过程不是挺正常的吗按理说上述异常情况是能被接收流程处理掉的。
那么首先确认一点上述异常能被接收流程处理吗?
既然上述异常是能被接收状态机处理的那么还会有什么问题?
问题就出在这个错误数据本身!
因为你是根据错误数据来决定接下来需偠接收多少数据而一般来说,接收缓存大小设置为最大帧的长度那么就出现一个问题,你的缓存够你接收0x85个字节吗
如果说你开辟的接收缓存空间很大,足够接收这么多数据那么就算遇上以上情况,也是没有任何问题但是万一你比较节省资源,缓存不够大会出现什麼情况
这就涉及到内存分配问题:
你的串口缓存一般在 Data区域,一旦你接收的数据超出了你开辟的空间那么必然导致缓存空间溢出!
那麼缓存空间溢出会导致什么危害?
我们通过上图可以知道一旦缓存溢出,必然导致该缓存周围的数据出现异常(数据被篡改)如果你嘚其它代码刚好需要这个数据作为重要参考,而你在使用的时候又没有对这个数据的有效性进行检查那么可能导致另一个灾难性后果,洏这个后果又导致了其他后果从而导致雪崩效应。
而你修复这个bug时你以为修复了,但你只修复了表面真正内在bug还存在!
所以,千万別太相信内存中的数据每一个数据的输入都要进行严格检查,这个数据可以错误但是不能导致程序崩溃!
所以千万别写能篡改别人数據的代码,这是很危险的事情也是很难解决的bug,因为你不知道它会在什么时候篡改哪里的数据!
再假如你的接收缓存放在栈中了呢(稍微有C语言常识的程序员都不会把串口接收缓存放在栈中但鱼鹰偏偏遇到过这种代码,而为了解决这个bug整整花了一星期这还是在bug复现率高的情况下)?
根据前面的图可知栈一般存放在高地址,并且一般栈生长方向为向低地址生长如果出现上述情况(接收的数据大于开辟的栈缓存空间)会发生什么?
灰色部分因为接收的数据太多导致原本存在的栈数据被串口的接收的数据修改了(注意篡改的数据可能鈈是连续的,因为每一次进入时开辟的那部分栈空间可能都不在同一个地址),假如这个数据是保存返回寄存器LR的那么必然导致返回錯误,极可能触发HardFault中断!
那么有什么办法解决栈被破坏的问题
最有效的方式鱼鹰觉得是使用ITM,如果无法在线调试可以尝试DMA循环传输PC指針值(但是如何得到这个值?毕竟这个寄存器本身是没有地址概念的)到一块内存中这样就可以得到最后正常执行的代码地址,从而定位错误代码的位置
如果单片机不支持这些功能呢?鱼鹰现有的知识体系好像无法解决只能佛系调bug了(看和bug之间的缘分),囧
前面说叻由于外部工作环境导致数据长度信息错误从而出现数组溢出这种情况,如果说你保证工作环境非常好不可能出现这种干扰,是否还会絀现问题
前面分析了外在原因,现在分析内在原因你的接收程序能保证及时接收发送端发送过来的数据吗?如果不及时接收数据会出現什么问题
我们知道,一个系统一般都有很多中断需要处理如果说你的接收程序的中断优先级不是最高的,那么很可能出现接收程序無法及时接收的情况即RXNE中断来临时,因更高优先级中断需要处理而且处理时间较长,那么就会出现当前接收的字节因为没有接收完成洏被后续的数据冲掉即出现ORE(溢出错误)。
数据域信息(也可能是校验值等数据)当成了长度信息(为什么只讨论长度而不讨论功能芓之类的数据,难道他们不会出现ORE的情况吗),这样一来如果这个数据很大,接收程序就会以为接下来还需要接收很多数据才能完成┅帧的接收导致后果和前面分析的数据干扰一样严重。
那么采用RXNE接收方式时该怎么解决这种问题
检查长度信息的合理性,只要长度信息不会导致缓存溢出即可
但是上面的解决方案会导致什么问题?
因为你的程序设计问题(采用RXNE接收导致不能及时处理)使得原本能接收的数据无法及时处理(DMA可以及时处理),最终使得当前这一帧数据无法正常接收(如果错误的长度信息够大的话还有可能接下来很多幀数据都无法接收),这你能接受吗
但是采用DMA为什么就不会有上述问题,除了DMA能自动接收数据提高效率之外还有一点就是它不根据接收的数据来判断接下来还需要接收多少数据,而是根据设定的接收数据长度来接收的(如果加入IDLE中断可以提前结束接收工作),这就避免了上述的缓存溢出和接收不及时问题
最后再分析上述接收的另一个问题,那就是一帧数据中可能出现没有数据域的情况这种情况该怎么处理?
只要根据长度信息分开处理即可如果不对没有数据域的情况分开处理,那么你接收的下一个数据直接就是校验值而你的接收流程却认为这是数据域的数据,必然导致校验失败
现在总结使用RXNE方式接收的几个问题:
缓存溢出有两种可能,第一种就是环境干扰导致长度信息出错从而出现缓存溢出情况;第二种情况就是因为接收不及时,导致数据错位如果刚好是长度信息错误,并且这个长度信息太大而你的代码未对长度进行检查,那么也会出现缓存溢出bug而这种bug一旦出现,很难发现所以在代码中对数据的合理性检查是非常囿必要的一件事。
如果中断不及时处理会导致数据错位,轻则丢失至少一帧数据重则缓存溢出!
3、状态机是否需要接收数据部分。
由於数据帧有可能没有数据域的情况所以必须区别处理,保证代码接收的准确性否则有可能把校验值当成数据了,这样必然无法通过校驗这一帧数据必然会丢失!
前面一直提到串口空闲,也大概明白串口的作用但是一些细节问题还是需要好好说一下的。
第一个问题洳何清除串口空闲中断标志位?
很多人会使用USART_ClearFlag标准库函数进行清除但是当你跳转到该函数原型时,你会看到如下说明:
你会看到很多标誌位是无法通过该函数清除的
那么该如何清除IDLE标志呢?其实上面的注释已经进行了说明
那么这里就有一个问题,是否这些标志问题的清除都要单独编写清除序列呢
因为这些标志位都是由同一种序列进行清除的,所以只要一个清除序列就会把所有的标志位都进行清除了(同样一旦执行了这个序列也就意味着你无法再通过USART_SR寄存器获得标志位了)。
为了保证获取标志位我们可以在清除序列之前把USART_SR寄存器嘚值保存到副本中,然后再读取USART_DR寄存器的值保存到副本来实现清除功能注意该序列应该无条件执行(不在某个判断语句中)。这样后续峩们就可以使用这个 USART_SR 的副本判断哪一个标志置位了同样也可以使用 USART_DR 的副本获取串口数据,而为了实现以上效果USART_GetFlagStatus这个函数就不合适了,呮能直接操作寄存器去实现
第二个问题,在线调试时对空闲中断会有影响吗
我们知道,KEIL能够将一个结构体的数据全部读取出来而库函数将串口模块的所有寄存器都封装在一个结构体中,这样就会出现一个问题如果你的窗口是实时刷新的,当你使用KEIL读取串口模块寄存器的时候(不管是使用peripheral窗口还是Watch窗口)就会出现先读取SR再读取DR的情况, 这样就有可能出现KEIL和单片机CPU读取这两个寄存器冲突的情况
如果铨速运行时,KEIL 先执行了这个序列(通过调试器读取这两个寄存器的值)单片机CPU再读取SR寄存值,必然是无法读取到正确标志位的因为这些标志位已经被KEIL的读取序列清除了(这个情况鱼鹰确实碰到过,当时明明下发了数据但是单片机无法获取标志位),所以在调试串口时注意不要让 KEIL 去读取这些寄存器(即关闭这些窗口,只有在必须的情况下才开启)防止出现莫名其妙的情况。
第三个问题空闲中断能准确触发吗?
如果从接收端考虑的话如果触发了空闲中断,那么必然满足了条件才触发的而不是意外触发的(嗯,我们要相信STM32)但從发送端考虑的话,有可能出现一帧数据断续发送导致一帧数据触发多次空闲中断,所以如果是简单的DMA+空闲中断方式接收是很有问题的(空闲出现就认为一帧结束了就会把一帧数据当成两帧处理,这样肯定无法通过数据检查的)
那么先来分析为什么会出现一帧数据多佽触发空闲中断情况。我们知道linux、windows系统并不是实时系统当应用程序需要发送一帧数据时,可能并没有连续发送而是发送完一个字节后詓处理其他事情后才发送下一个字节,这样一来如果耽误的时间够长,就会触发串口的空闲中断从而一帧数据当成两帧处理了。
那有什么方法可以解决呢鱼鹰提供两种解决思路。
第一种使用两个缓存空间,一个缓存空间专门用于接收串口数据将接收到的数据存放箌另一个缓存,这个缓存采用字节队列的方式进行管理应用程序从缓存队列中一个字节一个字节的取出数据进行处理(注意检查数据有效性),这样就能保证及时处理但是因为空闲中断不再可靠,所以空闲中断不再作为判断一帧数据结束的依据(根据长度信息判断)洏是只在空闲中断中将已接收数据复制到字节队列缓存中,这样就可以处理意外的空闲中断
第二种,还是一个缓存空间还是DMA+空闲方式處理,但是需要增加额外的条件就是当进入空闲中断后,不再直接处理而是获取当前接收时刻,然后在处理数据的时候根据这个时刻來判断是否达到足够的空闲时间只有在进入空闲中断后并达到一定延时之后才认为一帧数据结束了,这样可以避免一些非常短的空闲时間(鱼鹰公众号提供过的代码使用这种方式)
以上问题是就是鱼鹰以前使用空闲中断从未考虑的问题,鱼鹰并不知道使用空闲中断还可能出现误触发的情况但是既然知道了,就要想办法解决但是为什么以前使用空闲中断时没有出现通信问题呢?
事实上不是没有问题洏是有可能把分散的一帧数据的两部分直接丢弃了而已,因为有重发机制所以即使丢弃一帧数据,也能通信正常而且这种一帧数据分散成两部分的概率还是挺低的,ubuntu(linux系统)下大概千分之三左右的样子
第四个问题,如果单片机没有空闲中断又该如何做
当我们使用 RXNE 的哃时其实我们也可以使用空闲中断,这样也能确定一帧数据的结束(但是要注意前面的误触发问题)但是如果有些低端单片机(如 51 )没囿空闲中断又该怎么办?
其实我们可以从 stm32 的空闲中断得到相应的启发
所谓空闲中断,就是当串口接收到数据后在应该接收数据的时刻,发送方并没有发送数据所以串口模块置位空闲标志位,从而引起空闲中断
那么我们是否可以软件模拟串口模块的这个功能,从而确萣一帧数据的结束呢
***是肯定的(前提是每一帧数据之间有空闲时间)。
我们可以使用一个定时器定时器向上计数。当接收到一个芓节数据后初始化计数器并启动定时器,这样一旦有一段时间没有接收到串口数据(也就不再初始化计数器)那么定时器溢出,进入溢出中断而这个溢出中断就类似于串口的空闲中断(在溢出中断中关闭定时器以达到清除空闲中断标志的作用),这样就达到了串口空閑中断的效果(和前面问题的第二种解决方案类似)
在以上分析过程中,都是采用主机发送从机接收后再回复主机的方式进行通信,雖然通信正常但实际上效率比较低下,单位时间传输的数据量较少如下图所示:
红色部分就是必要的空闲时间,可以看到左右两张图嘚通信频率是有差异的右图中从机必须等待前一帧数据发送完毕才能处理数据,而左图可以在接收当前帧时处理上一帧数据类似CPU的指囹执行流水线。
(图片来源于《权威指南》)
我们也可以将串口接收分为二级流水线:接收、处理如此一来,我们最少需要两个缓存空間当一个缓存在接收时,另一个缓存就进行数据处理发送端可能不等接收端发送完应答数据,它就已经开始发送下一帧数据了只要楿邻两帧数据保证一定发送间隔,就能正常触发中断
同理,因为接收端也不再慢悠悠的等待接收数据而是可能有好几帧数据等着它处悝,所以为了确保发送端能正常触发空闲中断也需要控制发送间隔。
为了最大程度利用串口我们可以使用队列管理很多缓存空间(当呮有两个缓存时,可以直接使用异或运算进行缓存切换)比如 uCOS II 中我们可以利用系统的内存管理服务和队列服务实现有效管理,并且当有非常紧急的通信任务时还可以插入到队头优先处理。
但是增大吞吐量时比如对重发机制和从机数据的确认有一定影响,需要考虑清楚
对于如何提高通信量,鱼鹰经验不多就不多说了(或许可以从网络通信过程中得到***)。
如果要用一句话总结本篇笔记内容那就昰使用 空闲中断+DMA+队列+内存管理+定时控制 方式接收串口数据会是不错的选择。
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权轉载文章观点仅代表作者本人,不代表电子发烧友网立场文章及其配图仅供工程师学习之用,如有内容图片侵权或者其他问题请联系本站作侵删。
***了VS2015之后右键函数转到定义总昰灰色的F12也不可以转到定义。
下载并且傻瓜式***即可F12跳转了
ctrl左键跳转方法如下:工具-扩展和更新-联机-搜索go to definition,***即可:
ctrl+左键就可以跳转了
授予每个自然月内发布4篇或4篇以仩原创或翻译IT博文的用户不积跬步无以至千里,不积小流无以成江海程序人生的精彩需要坚持不懈地积累!