原创文章,欢迎转载,转载请注明出处
这次花了10多天了才再次写blog,一是中秋优点小活动,二是这次完成了不少东西。。
终于接近完成了,这次完成了NRF的通讯,并且用了改进的环形缓冲和简单的通讯协议规划
看着做的东西挺少,实际工作量不小。。哈。。我们先要在遥控上写个简易的UI,显示一些数据,然后读取手柄数据,通过NRF传给飞控,看看数据是否正确传输,而且还要解决数据传输中的各种问题,虽然以前用过NRF905,但是NRF24L01是没有用过的,不过大同小异。
1:NRF24L01P模块的使用
2:改进型环形缓冲的实现方法
3:通讯协议
1:NRF24L01P模块
NRF24L01的资料很多了,就不细说了,怎么配置,看看手册就好了,对了,关于手册,网上很多,中文版的也各种各样,但是最好的还是官方的原版英文的,不会有歧义,很多细节你都能在这个手册上找到。我们使用的是Enhanced ShockBurst模式,可以自动ACK,ACK带payload模式没有使用,是用了ACK模式,因为ACK带payload模式的花,PRX端(接收端)发送数据是被动发送了,只有对方发送数据来了,我们在ACK的包里面将我们要的数据顺带发给对方,如果对方没数据来,我们就无法将ACK里面的payload发过去了,这种只能在某些特定场合。那我们发送数据就需要自己控制NRF转换为TX模式了,发送完数据马上回到RX模式。。各个模式的转化如下图所示,摘自英文原版手册
可以看到TX Mode和RX Mode之间转换需要经过StandBy-1模式,中间需要让CE=0回到StandBy-1模式,然后在通过PRIM位来控制RX和TX模式,再置CE=1,虽然我们直接改变PRIM能到TX模式,但是还是按照手册上面执行比较安全靠谱。
讲到Enhanced ShockBurst下的ACK,不得不再上一张图,图下图:
其实上图的英文部分对于ACK和地址的设置已经比较清楚了哈。。不过我们这里还是稍微说下,PRX接收端收数据有6个通道,但是通道0比较特殊,是用来接收ACK包的数据。
对于这个图我看了下,很多人都有不同的理解。。。不同的理解对TX_ADDR和RX_ADDR的理解会完全不一样。先讲解下上面的图,再讲讲网上的对TX_ADDR、RX_ADDR一两种理解和我们自己的理解。
对于发送端:发送的时候需要设置两个地址,一个是TX_ADDR,还有一个是RX_ADDR_P0,这两个设定程一样的地址,我们假设为地址1,
对于接收端:需要设定一个RX_ADDR,而这个RX_ADDR地址需要和上面设定的地址一样,就是地址1,P0还是P几无所谓,接收端会根据监测到的空中的数据包中的Address和自己的RX_ADDR对比,如果哪个对上了,就会接收数据,空中数据发送的数据格式(可以理解为链路层的数据,物理层就是空气,哈,这样理解不知道对不对)如图:
发送端接收到数据后,会发送ACK数据包,这个数据包发给谁呢?发给地址1,对,这就是为什么发送端需要把RX_ADDR_P0设置为地址1的原因,这个就是用来接收ACK包的,接收到ACK包后,会产生TX_DS中断,通知MCU发送完成。
对这个地址可能会有点混,怎么发送接收都是地址1。。。。。。对,开始我也优点晕。。我们下面来看看对这两个地址的理解
网上的理解:大概意思是信息是在空中传播的,NRF会监测到在频道上的所有信息,NRF的RX_ADDR的意义是要接收的目标的地址,返回的时候也通过这个地址返回ACK,TX_ADDR是自己的地址,标志自己,好像我看到一个理解是这样说的,这怎么说呢,点对点没问题,多对多那就是错误了。。这样理解多对多会发生非常混乱的情况。
我们的理解:TX_ADDR就是目标地址,和TCP/IP里面一样,就是目标的地址,而RX_ADDR就是我自己的地址。会理解混的主要原因在于ACK返回的时候是通过自己的RX_ADDR_P0这个地址返回(我们可以把P0到P5理解为TCP/IP里面的端口,实际上用的时候P1到P5的前四个地址要求是一样的,只有最后一个自己可以改,这样理解为端口也更合情合理)。不是有六个接收地址么,我们用P0不干其他事情,就只用来接收ACK包,用P1来接收数据。这样就好理解了,RX就是本机地址,放在P1里面,TX就是目标地址,放在TX里面和P0里面。
如果需要开通更多的接收地址或者收端口,再开RX_ADDR,每个RX_ADDR给不同的线程处理,不就实现了类似余TCP/IP里面的端口功能了么。
发送端的发送流程这样的,发送数据,然后等待,等待TX_DS或者MAX_TX,TX_DS说明发送完成,并且收到了ACK数据,MAT_TX说明发送了设定的重发次数后还是没有收到ACK包,那就是发送失败了意味着。
接收端当监测到RX_DR信息的时候,说明接受到数据,我们读取数据处理数据就行了。
将上面的都实现了,其实我们的通讯已经完成了, 调用发送命令发送数据,发送完转换到RX模式,等待接收数据,有数据就接收,没有一直等,如果有数据要发送,转换到发送模式发送数据,发送完再转换到RX模式,等待接收数据,如此无限循环下去。
没错,我们的通讯完成了,但是这是极其不稳定不可靠的。一个完整的系统我想通讯也不会做到这里为止的,必须有个高速缓存,为什么要高速缓存?想象一下:当发送数据的频率非常多非常快的时候会出现什么情况,接收端收到RX_DR然后读取信息,然后处理,处理的过程是要时间的,当我们还没处理完,又一个RX_DR来了怎么办,这时候就会丢包,并且如果我们是收到RX_DR然后处理数据,然后再清楚RX_DR标志,这时候就不是丢包问题了,而是直接漏了一个RX_DR信号,也就是说有个包在RX_BUFF里面,我们根本就不知道,就算收到RX_DR立马清掉,然后处理数据,这时候不丢第二个RX_DR,但是第三个第四个RX_DR呢?虽然我们可以在每次RX_DR都判断status寄存器,看RX_BUFF里面有没有数据,有就读出来,全部读出来在清,但是处理时间在那,跟不上的话RX_BUFFER会满(我们用的动态数据长度模式),满了就没办法接收新的数据,发送端就会收到MAX_TX信号,表示数据没有发送成功。。这一切的根源就在于我们收到数据要处理,处理需要时间,当大量的数据高频率的过来的时候,我们无法保证数据处理的速度足够快来保证每个接收到的包能的到即时的响应。这时候要怎么解决?没错,需要一个高速缓存。有了高速缓存就好说了,来了数据就放入缓存,放入缓存不管了,让其他任务去处理,这样能够最大限度的保证处理数据的时间很短,能够尽可能快的响应到来的数据包。下面讲讲缓存的那些事。
2:改进型环形缓冲的实现方法
看下面之前,可以先看看维基百科上对环形缓冲的讲解:http://zh.wikipedia.org/wiki/環形緩衝區
我们使用的是环形缓存,这里主要讲讲改进型环形缓冲到底改进了哪些东西
传统环形缓冲一种是字节字节压入的,一种是块数据压入的,块数据就是固定长度的数据,例如每次都压入八个字节。
字节压入型的环形缓存应用在,例如串口。这是数据流方式的输入,来一个字节就写一个字节,来一个字节就写一个字节,读的时候根据头指针和尾指针,从头读到为,碰到尾部调头就是了,是个很简单的环形缓存形式,注意,读数据的时候外面需要一个内存空间来存这些数据,每次读数据,都需要从缓存中将数据复制到一个连续的内存空间去,要复制一次,然后再处理,很多串口都是这样实现的。对,问题处在这,处理的时候需要复制出去,要多复制一次,这就浪费时间了,要怎么处理呢?下面的块字节压入方式的环形缓冲就解决了这个问题。
块数据压入能够很好的解决上面读数据的时候需要复制出来的问题,其实我们需要读数据的时候,只需要返回数据的指针,然后我们去读就可以了,上面需要赋值出来再处理的最根本原因就是,数据存在缓存中没有保证是连续的。例如一个数据包来了,10个字节长,前5个字节存在尾部,后5个字节存在头部,那么如果你返回数据头指针,读到的前5个数据是对的,后5个数据已经越界,读到的不知道什么数据,所以需要读出来到另外内存中(可以理解为我们的变量中或者数组中),读出来后就可以保证数据是连续的,之后才可以处理。而块数据压入方式就不存在这样的问题了,例如我们设定了每次都是压入10个字节,那我们缓冲区的大小设定为10的倍数,这样就绝对不回出现数据一部分在尾部一部分在头部的情况,就可以通过返回数据指针来操作数据了,操作完成后,再POP掉就解决了,这样节省了内存空间,而且还少复制了一次,提升了处理速度,唯一的缺陷就是压入的数据长度必须一样,这个让我们有点小完美主义也可以说有点小强迫症的用起来觉得是非常的不爽。于是我们就自己动工,写两个个可以压入任意,并且读的时候只要返回指针,不需要复制数据的环形缓冲。
改进型环形缓冲:简单的说一下实现的思路,需要实现压入任意长度并且读数据只需要返回指针的环形缓冲需要解决两个问题,一个是你压入任意长度,假如里面有好几个包,你怎么直到读出来怎么读?第一个数据包多长?第二个数据包多长?二就是保证单个数据包在缓存中的数据在内存中是连续存放的。第一个问题:每个数据包的前面我们加一个长度信息,用来标识接下来多长为一个数据包。第二个问题就是我们监测当尾部不能够容下这个数据包的时候,我们就开启跳列模式,跳到头部,从头部再写,这样就可以保证数据在内存中存取是连续的,读取的时候需要判断跳列标志,并且需要直到跳到哪里去了。具体细节就不多说了,大体上实现方式就是这样的。这个也有缺点,就是每个包我都会占用一定的空间用来存取长度信息,然后当最后长度不够放的时候,我会跳到前面去,后面的空间就浪费掉了。这个没有办法。。哈。。有利有弊。。不过这种更通用的环形缓冲方式,我等有小强迫症的还是比较喜欢的。
3:通讯协议
待续。。。明天加上。。