近日有粉丝朋友留言,希望介绍一下nRF24L01这款无线收发芯片,正巧前不久的电赛有些涉及,因此将自己的一些经验写在这里,希望能有所收获。
前面我们介绍过单片机的几种通信协议,并且初步了解如何操作寄存器进而控制芯片的工作等等。那么,今天我们将利用之前的知识来对无线收发模块编写驱动程序。
首先,介绍我们今天用到的无线收发模块——NRF24L01芯片
nRF24L01简介:nRF24L01是由NORDIC生产的工作在2.4GHz~2.5GHz的ISM 频段的单片无线收发器芯片。无线收发器包括:频率发生器、增强型“SchockBurst”模式控制器、功率放大器、晶体振荡器、调制器和解调器。输出功率频道选择和协议的设置可以通过SPI 接口进行设置。几乎可以连接到各种单片机芯片,并完成无线数据传送工作。
管脚图:
各个管脚的具体功能可自行参考数据手册,由于高频信号对线路设计要求十分严格,我们没有必要去手焊电路,直接采用现成模块即可。
接线图:
从它的接线图也很容易可以看出它是利用SPI与单片机进行通信的,而且有5根线需要与单片机I/O口连接。
以上为硬件部分连接,接下来具体讲一讲软件设计。
我们拿到一块芯片,肯定要先看它的数据手册,了解它是如何工作的,然后还得重点了解一下这块芯片到底有哪些寄存器,这些寄存器的地址是什么,控制字是什么,时序图怎么样。所以我们从寄存器开始入手。
nRF24L01有多达25个寄存器,其中大部分是8位的,有两个数据缓冲器是32字节的,还有3个是40位的(发送地址和通道0、通道1接收地址),我们选择其中几个寄存器来进行说明。
名称:CONFIG 地址:00H
作用:可以用来决定是否屏蔽发送完成中断、接收完成中断、最大重发次数中断,还有芯片是上电还是掉电,工作在发射模式还是接收模式等。
名称:EN_AA 地址:01H
作用:用来使能各个通道的应答
名称:RX_ADDR 地址:02H
作用:接收地址允许位
名称:SETUP_AW 地址:03H
作用:用来设置地址宽度(3、4、5字节)
名称:SETUP_RETR 地址:04H
作用:建立自动重发,高四位决定自动重发的延时,低四位决定重发次数
名称:STATUS 地址:07H
作用:这是一个状态寄存器,从名字可以看出这个寄存器是描述状态的。高4位可读可写,最高位保留,其中bit6~bit4分别可以产生接收数据中断、发送数据中断、最大重发次数中断。bit3~bit1可以判断是哪个通道获得数据,只能读不能写,最低位保留。
名称:RX_ADDR_P0 地址:0AH
作用:它是数据通道0的接收地址,最多5个字节,从低字节开始,字节数量由前面的SETUP_AW决定,还有10H的发送地址,需要与这个地址一致
还有一些其他的寄存器就不多说了。这些寄存器大部分是比较重要的,我们在写芯片的初始化程序的时候,就是对这些寄存器进行配置。(注意:我们在写芯片的驱动程序,基本上是从芯片的初始化开始的,而芯片要初始化,当然要搞清楚它的各个寄存器地址、作用,才能进行配置)
上面提到的自动重发可能会不理解,原来一个发送过程并不只是发送机独自一个人的事情。一开始, 发送机发送连同身份地址在内的数据包给接收机,接收机收到数据并确认正确后,就以刚才发送机发来的身份地址回发一个确认信号,而此时发送机会自动切换到接收状态,当收到确认信号与通道0的地址进行比较,地址相吻合,就知道对方收到数据,这样一次发送数据才算完毕。当然,这个过程是模块内部自动完成的,不用人为干预。因此这就叫做增强模式。在这个过程中发送双方都最少各有一次发送和接收的过程。这样就保证了数据可靠收到。如果发送一次,接收机没有收到,模块具有允许重发,和重发次数设置寄存器,当你设置了允许重发,并设置好重发次数。在发送机发送完-一个数据包,等待会儿, 如果没有接收到应答信号, 发送机就会重发一次,如此直到重发完你设定的次数,再收不到它就产生一个中断信号给IRQ,告诉模块没有发送成功,由程序根据情况再决定是否重发。
除了寄存器之外,还有芯片的一些命令格式也要注意
那么要如何来理解上面这张表呢?我们可以看一下。指令R_REGISTER和指令W_REGISTER的前三位可以确定是读还是写,如果是000就是读,如果是001就是写,后面的5位是寄存器的地址。合起来就构成了一个8位字节,就可以确定对哪个寄存器进行读或写了。我们可以将R_REGISTER宏定义为0x00,如果某个寄存器A的地址为0x12,那么指令(R_REGISTER+A)不就是对寄存器A进行读的操作吗?那有人可能会问,万一寄存器地址很大,他们加起来不就会让高三位超过000和001吗?在这里是不可能的,后面的5位地址可以有32个寄存器,而这块芯片没有这么多寄存器,因此不会超过。前两个指令也就很好理解了。
后面的几个指令都是专用指令,只要发送相应的代码,就会执行右边说明的操作,比如发送指令W_RX_PAYLOAD,那么就是往TX FIFO寄存器里写数据。也很好理解。
指令和寄存器都介绍完了,因此,我们可以先建一个头文件,把各个寄存器的地址和指令先用宏定义好。(部分截图如下)
这些寄存器的名称、作用和地址都是从数据手册上可以直接获取的,我们以宏的形式将它们封装好。
然后是它的时序图
上图就是NRF24L01模块与单片机建立通讯的时序图。看懂它,是用单片机控制这个模块,与它建立通讯的基础条件。很多串行通讯设备都采用这种方式,也叫SPI.它一般有四根连线:第一根是片选信号线,单片机可以同时连接多个功能相同或不同的模块,为了节省输入输出(I/0)信号线的资源,一般都将多个模块连接在相同的SCK、MOSI、 MIS0线 上,这叫串行总线。那单片机发出指令后,模块怎么知道指令是发给自己的而不是发给其它模块的呢?这就靠每个模块上和单片机专线连接的CSN片选信号线,平时CSN是 高电位的,当把这根线的电位拉低那一时刻,模块就知道单片机要给自己发指令了。
从时序图上我们可以看出,读操作的时候,主机先发送指令位,同时接收从机的状态寄存器数据,然后再接收从机发送的数据。写操作时,一样是主机先发送指令位,同时接收从机的状态寄存器数据,然后再发送你要的数据。
而且,根据经验,在利用SPI通信时,我们至少要写这两部分的程序,一个是发送/接收一个字节,这一部分的写法是固定的,完全可以照抄。另一个是根据时序,对芯片的读写操作。
但是在这里,稍稍有点不同的是,因为有两个寄存器是32个字节的,还有3个是5个字节的,其他的都是8位的寄存器,因此,这些寄存器的读写程序要单独写。根据以上分析,我们可以写出如下4个程序。
uint SPI_RW(uint dat) //利用SPI单独写一个字节,并返回状态值
/*这是最基本的一个函数,几乎所有函数都要调用这个函数,它就是将一个8位字节从高位开始 ,一位一位的放到数据总线上,由于左移位后会补0,因此低位也可以顺便接收来自MISO总线上的数据,作为返回值。*/
当然,执行一次这个函数并不算是一次通信,一次通信至少要发送两个字节,地址和指令,于是有了下面的函数。
uchar NRF24L01_Read(uchar reg) //读出一个寄存器的数据,发送寄存器地址(包括读写位+地址)用以选择寄存器,然后执行空操作NOP,目的是读出数据。(空操作是一种指令格式,上面有提到)
uint NRF24L01_write_Reg(uchar reg, ucharvalue) //往一个寄存器里写一个字节,并且返回寄存器状态值
uint SPI_Read_Buf(uchar reg, uchar *pBuf,uchar uchars)
/*读出缓冲寄存器里的数据,最多32个字节,这个与上面的读其实是类似的,只不过这个加了个for循环,目的是读出多个字节*/
uint SPI_Write_Buf(uchar reg, uchar *pBuf,uchar uchars)
/*往缓冲寄存器里写数据,至多32个字节*/
以上几个程序的编程思路是比较经典的编程思路,从发送字节开始,到芯片发送寄存器地址和命令。(源码将在文末附上)
写完这几个函数,也就完成了工作的一半了。然后我们还要编写一个发射成功的判断函数和一个接收成功的判断函数,可以通过这个函数的返回值来判断是否接收或者发送成功。
在main函数的第一句,我们还可以编写一个函数,用来检测24L01是否存在。当然,不写这个也可以。
前面部分的编程都只能算是准备阶段,从芯片的初始化开始才能算是真正的编程。
所以,接下来,我们得利用前面写好的这些函数,来对芯片进行初始化。
初始化主要有以下几个方面
(1)设置接收数据长度
(2)写TX节点地址
(3)设置RX节点地址,主要为了使能ACK
(4)使能通道0的自动应答(多通道暂不考虑)
(5)使能通道0的接收地址
(6)设置自动重发间隔时间和最大自动重发次数
(7)设置RF通道为0 收发必须一致,0为2.4GHz
(8)设置TX发射参数,0db增益,1Mbps,低噪声增益开启
(9)配置基本工作模式的参数;PWR_UP,EN_CRC,16BIT_CRC,接收模式,开启所有中断
这些可以参考数据手册的寄存器部分。
到这里,我们就可以在main函数里开始调用前面编写的程序了。
进入main函数,首先是调用初始化程序,然后就可以读取缓冲寄存器里的值或者是写入数据。main函数部分根据用户需要编写。比如发射机可以用按键按下来发送数据,接收机则通过查看IRQ管脚来判断是否接收到数据,接收到数据后再执行你要的程序,比如控制小灯亮等等。
最后,我们来理一下编程思路
①首先,我们先明确了它是利用SPI的通信方式来对芯片控制的。不管是什么通信,无非就是读和写操作而已。读和写无非就是发送地址+命令。通常有一位是读写方向位的,这个查看手册就可以知道。因此,肯定要先编写一个“发送函数”,把字节发送出去,其实也就是单片机通过MOMI输出高低电平而已。然后,前面也提到过,发送一个字节是没有意义的,一次通信至少要包含两个字节,第一个字节包含了读写的方向位以及待操作寄存器的地址,第二个字节是具体的命令。我们将发送这两个字节的语句,结合时序“打包”在一起,就是对芯片寄存器的读写函数了。写可以理解成先发送地址(同时返回状态值),再发送命令。读可以理解成先发送地址,再执行空语句(执行空语句的目的是为了读出寄存器值)。
②上一步是实现了对寄存器读写一个字节,但是我们知道有的寄存器不止一个字节,比如发送地址和接收地址都有5个字节,发射接收缓冲器有32个字节等。因此我们可以编写一个函数,连续读出或者写入多个字节,实现也很简单,就是定义一个数组,读的话就把数据读出放入到这个数组中,写的话就是把你数组里的内容写进去。
③以上两步是如何实现读和写。这还不够,我们还得搞清楚具体读写什么内容。这就得回到它的各个寄存器上去了。我们要根据手册的说明,看各个寄存器有什么用,这在前面已经讲过了。主要是在初始化程序中比较重要。
④光有读和写不够,我们还得知道每一次通信是否成功。因此,还得编写两个函数。比如接收,我们要先读出状态寄存器STATUS的值,来判断是否接收成功,接收成功是会产生接收中断的,我们看相应的位就能知道有没有接收成功了。如果接收成功,我们把数据转移到我们自己定义的一个数组中,方便使用。如果是发送,我们只要把要发送的数据放在数组里,然后发送出去即可,然后还要读取状态寄存器的值,看是否发送成功,或者是否达到最大重发次数,返回相应的值。
⑤有了上面的这些基础,我们就可以在main函数里先进行初始化,然后写我们的发射程序和接收程序了。在发射函数里,每次先配置一下寄存器CONFIG,然后再利用我们前面的程序发射。接收比较简单,只要看IRQ是否产生中断即可。
以上就是整个编程思路。
最后说一下,如果大家对硬件和编程感兴趣,可以点击牛客网这个连接看看,是个不错的学习网站,是学长推荐给我的,因为之前找工作的时候没有太多经验,也不知道怎么准备,去哪里搜资源,走了很多弯路,所以推荐给大家,当初在上面大量刷题还看了好多面试经验等,反正上面有很多课程+刷题+面经+求职+讨论区等资源,关键里面的资源全部公开免费,不用花钱,希望能帮助家!