赞
踩
最近在用nRF52832在开发一个项目,其中需要用到串口,为了提高通用质量与通信效率,很自然的就想到了使用DMA来进行接收,不过对于nRF52832的DMA来说,有一个硬伤就是不能实现不定长接收(其实好多芯片的DMA都不能实现硬件机制上的不定长接收),又不能像STM32一样有一个DMA空闲中断来判断数据是否接收完毕,所以这就让我们很头大,也就引出了第一个也是最重要的问题——如何快速有效的判断数据是否接收完毕。对于这种不能自己主动判断数据是否接收完毕的芯片来说,用定时器中断来判断是最直接有效的方式,那么解决的途径有了,具体怎么解决呢?接下来闲话少叙,接下来我们直接开始开发:
step1:串口UARTE初始化
nRF52832只有一个硬件串口,这个串口既可以做普通的串口UART,也可以做UART。同时只能使用一个,其中只有UARTE才能使用DMA功能,所以第一步我们就是要初始化UARTE与DMA。
对于配置一个串口来说,无非就是配置它的波特率,收发引脚,硬件流控引脚以及是否允许硬件流控,奇偶校验位等常见的特性,这对于一个玩单片机的盆友来说都不叫事。
从手册上我们可以看到以上几个寄存器可以用来配置引脚引脚和波特率以及奇偶校验位等内容。下面我直接贴出我的代码以作说明。
至于这些寄存器为什么是以这样的形式来使用的这里不做解释,因为我觉得既然能对这块芯片有兴趣,一定是有一定的嵌入式编程基础的,所以不过分解释这些太基础的东西。
看到这里有人会问你的UART_RXD,和UART_TXD分别是什么,其实很简单,对于nRF52832来说,把每个外设引脚都用数字编号了,所以所以我们在配置引脚的时候只需要将对应的数字填进去就行了。UART_RXD和UART_TXD的定义如下图所示,其实就是分别定义在8号引脚和6号引脚上的。
有细心的朋友可能看到了我这里把波特率的定义也贴出来了,那么我是怎么知道115200这个波特率对应的就是这个数据的呢,当然离不开万能的开发手册了。
通过以上查表我们就可以知道我们想要的比特率的对应的配置数据了。
配置完收发引脚和波特率,接下来就该配置硬件流控功能了,首先我们打开寄存器手册。
我在程序中给的全部是0,也就是说我没有用到硬件流控功能,没有用到硬件流控功能自然就不用配置相应的引脚了。到目前为止我们就把有关UARTE的一些基本的硬件配置完成了。
接下来我们要做的就是配置相应的触发中断和使能UARTE了。
看到以上代码还是不够明显,我把对应的寄存器说明贴出来以供讲解。
有的朋友看到这里可能会有疑问,手册上不是说使能UART是用0X04吗?为什么我做的是0X08呢,一开始我也是这么做的配置成0X04,后来我发现不管我怎么做,串口都用不了,后来一翻原厂的芯片手册才发现,应该是配置成0X08才有戏,所以必要的时候还是要靠原厂手册啊。
对于INTEN寄存器我就使能了RXDRDY,ENDRX,ENDTX这三位,当然串口中断也就只能响应这三个事件了。
RXDRDY:从接受线路上成功接收到一个字节数据标志位,(但是这个数据可能并未存储到缓存中).
ENDRX:接收缓存区已满(接收DMA的缓存空间已满)标志位
ENDTX:已发送完发送(DMA)缓存区的最后一个数据。
把这三个事件标志位弄清楚是非常有必要,不然后面你就根本无法下手。讲到这里终于降到了DMA的头上了,那么,究竟什么是DMA呢?
上面是百度的说法,很官方,很绕口。对于初学者可能不是那么容易理解,对于UARTE的DMA来说,简单的说,就是芯片可以自动的将接收到的数据放到一段缓存中,可以自动的将一段缓存中的数据通过串口发送出去,就是这么简单,DMA是纯粹的硬件机制,也就是说使用DMA可以大大的提高通信效率和通信质量,减少程序的时间开销。既然DMA这么好,那么我们当然要式样它。至于其中的FIFO机制就不用过分的区理解他,说到底就是要个进栈出栈的操作,对于目前的来说无关紧要。
那么通过上面的程序我们已经打开了UARTE的DMA的接收和发送中断了,但是仅仅依靠上述的105和106行程序还是不行的,这样不足以打开中断服务,所以才有了第107行程序,这行程序很复杂,是用来配置NVIC中断向量的,要知道nRF52832是基于CM4内核的,所以使用中断的时候也要向使用其他的CM4内核的芯片一样配置中断向量,这个具体的有点复杂,如果有需要我会另开一篇博客讲解,这里不做深入了解。有了这三行程序,我们的UARTE和DMA的中断都可以打开了,当然只是针对我们使能的那三个服务而言。有人可能会疑惑,讲了半天DMA也没有看到寄存器中有出现有关DMA的字符啊,是的,就是这么神奇,nrf在做UARTE的时候就已经把配置DMA的寄存器一起做了进来,所以不必太纠结。
上面说到了,DMA需要有缓存空间,说到底就是两个数据存储空间,一般来说我们会接收缓冲区和发送缓冲区各做一个,用来保存接收到的数据和需要发送的数据。
从上面的代码可以看出,我对发送和接收各做了一个1024个字节长度的缓冲区,还分别定义了发送数据的长度和接收数据的长度。
那么缓存既然有了,我们怎么和UARTE的DMA关联起来呢?
我们根据这四个寄存器就可以看出他们分别是接收数据指针,接收缓存区最大字节数,发送数据指针,发送数据缓存区最大字节数。
那么我能就可以有入如下代码。
第102行的程序是告诉单片机接收到的数据要压入uarteStruct.rxBuf这个数据栈中,103行程序是告诉单片机,这次我要接收255个字节的数据。(发送的暂且不讲)。
看似好像串口应该是配置完了,实际上并没有,我们还需要打开串口接收任务,串口才能接收数据。
到这里,UARTE的DMA配置基本上已经完成了。那么这里有我们开发遇到的两个大坑。稍不留神就得翻船。
坑一:不管是DMA的一次接收的数据长度还是一次发送数据长度都不得大于255,这是因为RXD.MAXCNT和TXD.MAXCNT都只有一个字节。
坑二:不管是DMA的接收缓冲区还是发送缓冲区都只能在idata里,因为RXD.PTR和TXD.PTR只能指向idata里的数据,也就是说不能用DMA来发送const的数据或者把接收的数据放到非idata的空间里。
在开发的时候一定要注意这两个问题,切记!切记!。
step2:中断服务程序
对于UARTE的中断服务程序来说,由于我们前面只开了三个中断,那么我们现在也只能得到三个中断,分别是EVENTS_ENDRX,EVENTS_RXDRDY,EVENTX_ENDTX。这三个事件寄存器的说明如下图所示。
所以说有了这三个事件中断我们就可以很轻松的判断数据有没有接收完,发送完,当然这只是针对255个字节一下的内容,对于接收来说,这是只能指定接收多少长度才能产生ENDRX事件中断。
讲到这里终于铺垫完了,如果您真的看到了这里,恭喜您,接下来我们终于可以开始讲干货了,前面讲的其实都是定长接收的内容作为铺垫和导入,我得跟您交代明白了才能讲接下来的内容:
那么我们具体怎么实现的DMA不定长接收的呢?
1、准备接收缓冲区,由于不知道接收到的数据有多长,所以默认接收长度是255,以此来减少进入事件EVENTS_ENDRX中断的次数。
2、我们需要开启接收任务,并且开启接收字节中断,也就是允许产生EVENTS_RXDRDY事件中断,用来判断接收一串连续数据的第一个字节的数据。
3、在接收完一个字节的数据后,失能EVENTS_RXDRDY事件中断,这样就可以避免每接收一个字节进入一次中断了,开启循环定时器。
4、在定时器服务程序中检测EVENTS_RXDRDY事件,如果该标志位为0,也就是在上次定时器结束到本次定时器没有接收到数据,可以认为接收数据完毕,发送数据接收完毕事件(将产生EVENTS_ENDRX事件),否则EVENTS_RXDRDY事情清0,继续检测。
5、EVENTS_ENDRX事件中断产生后判断接收到的数据长度是否超过缓冲区的长度,如果超过则强制结束接收,如果改事件是有定时器主动产生的则结束接收,如果不是则继续接收。
以上就是实现DMA不定长接收的一个简要的流程。其中就涉及到了我们前面讲的定时器,这里的定时器的工作主要就是为了检测两次定时器中断中的EVENTS_RXDRDY事件,以此来判断数据是否接收完毕,有人可能会疑问,不是在接收完一个字节的数据后产生该中断后屏蔽了EVENTS_RXDRDY事件中断吗?是的,是屏蔽了,但是并不影响它产生事件啊,只是不触发中断罢了。那么实际上就是检测两次定时器中断间有没有接收到数据。如果没有就认为数据已经接收完了,要主动发送EVENTS_ENDRX事件。那么如何主动发送该事件呢?查阅手册可知置位TASKS_STOPRX任务可以产生EVENTS_ENDRX事件。所以我们的定时器中断服务程序可以如下所示:
我这里为了方便,用的是APP定时器,至于APP定时器怎么用,这里不做讲解。
既然使用了APP定时器,那么自然要对它进行初始化了:
那么关于定时器中的一些数据需要定义和APP定时器启动停止函数如下:
现在好了,定时器也有了,基本思路也有了,串口也初始化好了,现在就只剩下UARTE中断服务程序中的内容了,我们一个个事件来讲,
第一个讲一下EVENTS_RXDRDY事件:直接贴出代码如下
按照上述思路,在产生第一个字节接收中断后需要禁止字节接收中断,以防止每接收一个字节就产生一次中断,这样子我们的DMA就没有了意义,其中154行就是实现这个功能的,第155行清除接收字节中断,接下来解释清除接收结束标志位,清除超时标志位,清除接收长度,开启APP定时器来判断是否接收完毕,因为一切才刚刚开始,所以这些数据都要清零来做接收准备。
接下来就是EVENTS_ENDRX事件中断了:
这部分内容稍微麻烦点,我们一步步来讲,进入事件中断第一步当然是清除事件标志位喽。130行的作用就是讲接收指针移到下一个接收地址入口,为什么这样做呢?我们首先看一下RXD.AMOUT这个寄存器的内容:
这是什么意思呢,就是说RXD.AMOUT这个寄存器可以记录你这一次接收了多少个字节的数据,我们把上一次接收数据的入口地址加上这一次的接收的数据不就正好是接下来要接收的数据的入口地址了吗?
同理131行就是累计本次接收数据的长度。
134行是用来判断的数据是否接收完毕,我们前面的思路就是接收数据长度超过接收缓冲区的大小或者定时器超时没有接收到数据就认为数据接收完毕。接收完数据后我们就不需要定时器了,当然要把他关掉,所以就有了第135行程序,第136行程序是为接收的入口地址重新指向我们事先定义好的缓存地址,为下一次接收做好准备。137行程序是通知应用程序一次接收完毕,可以来处理数据了,为了避免错误的再次进入接收字节中断,我们需要想把接收字节事件标志位清0再开启接收字节中断。
第142行到148行都是更新下一次接收的数据长度。
第149行是重新启动接收任务。
有了以上程序,我们就可以实现一个不定长的UARTE DMA接收功能了。
这里需要补充两条指令,相信你们在阅读的时候已经发现了,开启字节接收中断和关闭字节接收中断这两条指令出现得很突兀,这里补充一下这两条指令的定义。
而我们上面说的第三条事件中断是用来干嘛的呢,实际上是用来实现不定长发送的功能的。
首先EVENTS_ENDTX这个事件表示DMA发送缓冲区里的内容已经发送完毕了,第164行表示清除中断事件标志位,第165行程序表示计算发送剩余的字节长度,接下来就是如果还有数据需要发送则判断剩余的长度是不是小于255,如果是就将剩下的发送完毕,不是的就再发送255个字节。
同样的需要启动发送任务。
其中第120行就是启动发送任务的。
在即将结束的时候,有必要再排除一些坑。
第一:为什么我不在定时中断里直接通过RXD.AMOUT获取最后的接收字节来通知应用程序接收完毕,而是要通过手动置位TASKS_STOPRX来获取EVENTS_ENDRX进而来获取最后的接收字节长度通知应用程序接收完毕呢?
这里有一个坑就是,获取RXD.AMOUT必须要要是产生EVENTS_ENDRX事件后才能获取,不然获取到的就是上一次产生EVENTS_ENDRX事件后的值。还有就是必须产生EVENTS_ENDRX事件后才能确保接收到的数据搬运到了DMA接收缓存中。
第二:RXD.AMOUT的获取也是必须要在EVENTS_ENDTX产生后才能得到。
第三:在重新准备下一次接收的时候,一定要先清除EVENTS_RXDRDY标志位,再重新开启EVENTS_RXDRDY中断,不然有可能一开中断就进入中断,影响接收。
最后贴上我的效果图:
水平有限,能力一般,如有不对,万望指正,万谢,万谢。。。。。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。