找回密码
 立即注册

QQ登录

只需一步,快速开始

搜索
查看: 43741|回复: 17
打印 上一主题 下一主题
收起左侧

NRF24l01无线模块调试心得!STM32_SPI驱动NRF24L01+

  [复制链接]
跳转到指定楼层
楼主
ID:82083 发表于 2015-6-9 02:30 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式
经过两日的调试nRF24L01终于调试成功。耐心,细心。有想法。资料。数据手册。总就会成功。


   下面是调试的基本方法:           
   
24L01是收发双方都需要编程的器件,这就对调试方法产生了一定的要求,如果两块一起调,那么通讯不成功,根本不知道是发的问题还是收的问题,不好意思的说,我当时也是没理清调试思路才浪费了一天时间看着模块干瞪眼。并且烧坏了两个模块。(还好这个模块可以》。。。。。)。

所以正确的调试方法应该是先调试发送方,能保证发送正确,再去调接收,这样就可以有针对性的解决问题。
至于怎么去调发送方,先说下发送方的工作流程:

??配置寄存器使芯片工作于发送模式后拉高CE端至少10us
??读状态寄存器STATUS
??判断是否是发送完成标志位置位
??清标志
??清数据缓冲

网上的程序我也看过,大多都是成品,发送方发送-等应答-(自动重发)-触发中断。可是这样的流程就已经把接收方给牵涉进来了,就是说一定要接收方正确收到数据并且回送应答信号之后发送方才能触发中断,结束一次完整的发送。可是这跟我们的初衷不相符,我们想单独调试发送,完全抛开接收,这样就要去配置一些参数来取消自动应答,取消自动重发,让发送方达到发出数据就算成功的目的。
SPI_RW_Reg(WRITE_REG + EN_AA, 0x00);        // 失能通道0自动应答
SPI_RW_Reg(WRITE_REG + EN_RXADDR, 0x00);    // 失能接收通道0
SPI_RW_Reg(WRITE_REG + SETUP_RETR, 0x00);  // 失能自动重发
1有了以上这三个配置,发送方的流程就变成了发送-触发中断。这样就抛开了接收方,可以专心去调试发送,可是怎么样才知道发送是否成功呢,要用到另外两个寄存器,STATUS和FIFO_STATUS。


这样就很清晰了,我们可以通过读取STATUS的值来判断是哪个事件触发了中断,寄存器4、5、6位分别对应自动重发完成中断,数据发送完成中断,数据接收完成中断。也就是说,在之前的配置下,如果数据成功发送,那么STATUS的值应该为0x2e。这样就可以作为一个检测标准,另外一个标准可以看FIFO_STATUS寄存器,第5位的描述:发送缓冲器满标志,1为满,0为有可用空间;第4位的描述:发送缓冲器空标志,1为空,0为有数据;同样可以看到接收缓冲器的对应标志。这样在数据发送成功后,发送寄存器当然应该是空的,接收缓冲因为在之前已经失能,所以也应该是空,也就是说成功发送之后的FIFO_STATUS寄存器值应该是0x11。
有了这两个检测标准,我们即使不用接收方也可以确定发送方是否成功发送。当发送方调试成功之后,在程序里让它一直发送,然后我们就可以去调试接收方,思路是一样的,同样说下接收方工作流程先。

??配置寄存器使芯片工作于接收模式后拉高CE端至少130us
??读状态寄存器STATUS
??判断是否是接收完成标志位置位
??清标志
??读取数据缓冲区的数据
??清数据缓冲

然后在初始化配置寄存器的时候要和发送方保持一致,比较重要的是要失能自动应答,使能通道0接收:
SPI_RW_Reg(WRITE_REG + EN_AA, 0x00);        // 失能通道0自动应答
SPI_RW_Reg(WRITE_REG + EN_RXADDR, 0x01);    // 接收要使能接收通道0
这样就可以了,接收方就可以进入接收模式去接收数据了,这次的调试就会灵活一些,因为是接收数据,可以在接收方添加一个显示设备把数据直观的显示出来,去对照看是否正确,当然还可以使用和发送方一样的方法:观察STATUS和FIFO_STATUS的值,对照寄存器描述,接收正确时STATUS的值应该是0x40,对于FIFO_STATUS的情况就多了些,因为数据宽度的不同也会造成寄存器的值不一样,24L01最大支持32字节宽度,就是说一次通讯最多可以传输32个字节的数据,在这种情况下,接收成功读数据之前寄存器值应该为0x12,读数据之后就会变成0x11;如果数据宽度定义的小于32字节,那么接收成功读数据之前寄存器值应该为0x10,读数据之后就会变成0x11。这个看起来挺复杂,其实很清晰,大家可以试着分析下,对照数据手册分析每个位的状态就可以得到结果。

好了,到这里对nRF24L01的调试基本上就算通了,但是要明白这些只是调试方法,最终的产品如果不加上应答和重发的话那么数据的稳定性是很难保证的,所以在基本的通讯建立之后就要把发送的配置改为:
SPI_RW_Reg(WRITE_REG + EN_AA, 0x01);       //使能接收通道0自动应答
SPI_RW_Reg(WRITE_REG + EN_RXADDR, 0x01);   // 使能接收通道0
SPI_RW_Reg(WRITE_REG + SETUP_RETR, 0x1a);  // 自动重发10次,间隔500us
接收方的配置也要更改:
SPI_RW_Reg(WRITE_REG + EN_AA, 0x01);        // 失能通道0自动应答
SPI_RW_Reg(WRITE_REG + EN_RXADDR, 0x01);    // 接收要使能接收通道0

这样发送和接收就进入了一个标准状态,发送-等应答-(自动重发)-触发中断;接收-应答-触发中断,一切按部就班,程序里加上自己的应用部分就能实现很多功能了。




其实自己摸索单片机已经有一年了,但没在网上写过笔记,终于觉得既然整天都在各种网站找资料,也看人家的博客,自己也写写笔记,一方面给自己看,一方面作为分享和供批评。
STM32这块片子接触也有半年了,还没真正做自己的作品,这阵子和一个朋友想做一下一个基于STM32的小作品,要用到无线控制,决定用NRF24L01+这片芯片,主要也是这片常用,简单,也便宜。
   一个小插曲是在淘宝上淘模块的时候发现,相当多的店子都写明了是Si24R91(其实是应该是Si24R1,那个好像是不存在的)这一代替芯片,其实看了手册,单单从功能上,Si24R1是提升了一些,属于向下兼容NRF24L01,但是不解的是淘宝发回来的芯片上印的却是NRF24L01,通过比对调试寄存器的内容确实是NRF24L01(这才让我不解,商家搞什么名堂)。
   STM32F103VET6有俩SPI接口,选择用SPI1来驱动NRF24L01.
    1.配置STM32的SPI1接口:
      1)开启GPIOA,AFIO时钟
      2)配置GPIOA对应Pin为SPI1的MISO,MOSI,CLK,NSS(SPI1的NSS已经为他用,故用PA1软件控制NRF24L01的CSN),CE(NRF24L01使能端口),IRQ--NRF24L01的中断信号
     3)配置SPI模式
    2.仔细读手册(重点是时序和寄存器)
    3.编写用SPI1控制NRF的函数数个
   4.用以上的函数编写功能函数
   OK!但这是说的。真正做起来的时候饱受打击。串口转USB线坏了,无奈只能LCD屏来显示调试信息,增大了代码负担。后来J-Link有坏了,差点放弃,好在重写了固件还能复活。
   1)2)步的代码:
void SPI1_Config(void)
{
   RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1|RCC_APB2Periph_GPIOA|RCC_APB2Periph_AFIO,ENABLE);               //使能SPI1时钟
   
   
     //  PA5--CLK PA7--MOSI 复用推挽输入
   GPIO_InitTypeDef GPIO_InitStructure;
     GPIO_InitStructure.GPIO_Pin =  GPIO_Pin_5|GPIO_Pin_7;
     GPIO_InitStructure.GPIO_Speed =GPIO_Speed_10MHz;
     GPIO_InitStructure.GPIO_Mode =GPIO_Mode_AF_PP;
   GPIO_Init(GPIOA, &GPIO_InitStructure);
     //PA6--MISO  输入浮空
   GPIO_InitStructure.GPIO_Pin =GPIO_Pin_6;
     GPIO_InitStructure.GPIO_Mode =GPIO_Mode_IN_FLOATING;
   GPIO_Init(GPIOA, &GPIO_InitStructure);
   
   GPIO_InitStructure.GPIO_Pin = GPIO_Pin_1|GPIO_Pin_2;
     GPIO_InitStructure.GPIO_Speed =GPIO_Speed_10MHz;
     GPIO_InitStructure.GPIO_Mode =GPIO_Mode_Out_PP;
   GPIO_Init(GPIOA, &GPIO_InitStructure);


   GPIO_InitStructure.GPIO_Pin = GPIO_Pin_3;  
     GPIO_InitStructure.GPIO_Mode =GPIO_Mode_IPU;
   GPIO_Init(GPIOA, &GPIO_InitStructure);

SPI1_CE_LOW();
   SPI1_NRF_CSN_HIGH();//拉高CSN失能片选
   
   
   SPI_InitTypeDefSPI_InitStructure;                                 //声明用来初始化的结构体
     SPI_InitStructure.SPI_Direction =SPI_Direction_2Lines_FullDuplex;//全双工
     SPI_InitStructure.SPI_Mode =SPI_Mode_Master;                    //主模式
     SPI_InitStructure.SPI_DataSize =SPI_DataSize_8b;                 //一次传输8位
     SPI_InitStructure.SPI_CPOL =SPI_CPOL_Low;                       //空闲电平低电平
     SPI_InitStructure.SPI_CPHA =SPI_CPHA_1Edge;                     //第一个上升沿采样
     SPI_InitStructure.SPI_NSS =SPI_NSS_Soft;                        //NSS管理为软件件模式
     SPI_InitStructure.SPI_BaudRatePrescaler =SPI_BaudRatePrescaler_8;//波特率预分频8  9MHz
     SPI_InitStructure.SPI_FirstBit =SPI_FirstBit_MSB;      //数据传输低位在前
     SPI_InitStructure.SPI_CRCPolynomial =7;                          //CRC校验方式
   SPI_Init(SPI1,&SPI_InitStructure);                                 //初始化
   
   //SPI_NSSInternalSoftwareConfig(SPI1,SPI_NSSInternalSoft_Set);
   
  
   SPI_Cmd(SPI1, ENABLE); //使能SPI1
}//SPI1_Config()

   *操作NRF24L01寄存器的时候要在待机模式,CE=0的情况下,没发一个命令或数据,都会返回SPI一个字节,此字节是STAUS状态寄存器的内容,所以写命令的函数要发后,读回一字节,不然,发完命令,读数据的时候会出错。下面就是SPI写1 BYTE的函数。

u8 SPI_RW_Byte(SPI_TypeDef* SPIx,unsigned char Byte)
{
     while( SPI_I2S_GetFlagStatus(SPIx,SPI_I2S_FLAG_TXE) == RESET); //查发送缓冲器是否为空,空即可以发送
     SPI_I2S_SendData(SPIx,Byte);   //库函数:发送一个字节
     //当SPI接收缓冲器为空时等待
      while(SPI_I2S_GetFlagStatus(SPIx, SPI_I2S_FLAG_RXNE) ==RESET);
   returnSPI_I2S_ReceiveData(SPIx);
}//SPI_RW_Byte()

  
   *下面一个是发写命令+指定字节数的数据的函数

u8 SPI_NRF_Write(SPI_TypeDef* SPIx,char CMD,unsigned char*WBuff,unsigned char ByteNUM)
{
unsigned chari,status;
SPI1_CE_LOW();
SPI1_NRF_CSN_LOW();//使能片选

status=SPI_RW_Byte( SPIx , CMD);
for(i=0;i
{
    SPI_RW_Byte( SPIx,*WBuff++);
   //printf("写入第%d个数据\r\n",ByteNUM);   
}
SPI1_NRF_CSN_HIGH();//
return status;
}//SPI_NRF_Write()

    *发读命令+指定字节数的数据的函数

u8 SPI_NRF_Read(SPI_TypeDef* SPIx,char CMD,unsigned char*RBuff,unsigned char ByteNUM)
{
unsigned char i,status ;
  
SPI1_CE_LOW();
SPI1_NRF_CSN_LOW();
status=SPI_RW_Byte( SPIx , CMD);
for(i=0;i< ByteNUM ;i++)
{   
    RBuff=SPI_RW_Byte(SPIx,NOP);         // 取接收缓冲器,一个字节
   //printf("读出第%d个数据\r\n",ByteNUM);   
   LCD_Num_6x12_O(100,20*(i+1),RBuff,WHITE);  
}
  

SPI1_NRF_CSN_HIGH();
return status;
}//SPI_NRF_Read()


下面是NRF24L01两种模式TX RX的配置

void SPI_NRF_MOD_TX(void)
{
    u8TX_Array[5];
u8_TX_RX_ADDR_[5]={0xB3,0xB4,0xB5,0xB6,0x05};
  
SPI1_CE_LOW();//CE=0待机模式
  TX_Array[0]=0x03;//设置地址宽度11--5字节 10--4字节 01-3字节 00--不合法
SPI_NRF_Write(SPI1,W_REGISTER+SETUP_AW,TX_Array,1);
  TX_Array[0]=0xf3;//建立自动重发间隔‘1111‘--等待4000+86us  15次
SPI_NRF_Write(SPI1,W_REGISTER+SETUP_RETR,TX_Array,1);
  TX_Array[0]=0x02;//射频通道 X0000010
SPI_NRF_Write(SPI1,W_REGISTER+RF_CH,TX_Array,1);
  TX_Array[0]=0x0f;//射频参数寄存器 00001111 2Mbps 发射功率 00-18dBm 01-12dBm 10-6dBm 11-0dBm 1--低噪声放大器增益
SPI_NRF_Write(SPI1,W_REGISTER+RF_SETUP,TX_Array,1);
  TX_Array[0]=0x3f;//xx11 11110-5接收通道允许
SPI_NRF_Write(SPI1,W_REGISTER+EN_RXADDR,TX_Array,1);
  TX_Array[0]=0x3f;//xx11 11110-5通道允许自动应答
SPI_NRF_Write(SPI1,W_REGISTER+EN_AA,TX_Array,1);
   

SPI_NRF_Write(SPI1,W_REGISTER+TX_ADDR,_TX_RX_ADDR_,5);//写入接收发送数据的地址,这个地址是接收端收件的凭证
SPI_NRF_Write(SPI1,W_REGISTER+RX_ADDR_P0,_TX_RX_ADDR_,5);//写入接收发送数据的地址,这个地址是接收端收件的凭证


TX_Array[0]=0x0e;//中断全开 发送模式 PRIM_RX=0PWR_UP=1
SPI_NRF_Write(SPI1,W_REGISTER+CONFIG,TX_Array,1);

TX_Array[0]=0xfe;//1111 xxxx STATUS寄存器写‘1’清除所有标志
SPI_NRF_Write(SPI1,W_REGISTER+STATUS,TX_Array,1);

SPI1_CE_HIGH();//CE=1使能发射模式
Delay_us(100);//CE拉高需要一定的延时才能进行发送 延时之后即可通过SPI接口发送TX_PLD

}

void SPI_NRF_MOD_RX(void)
{
    u8TX_Array[5];
u8_TX_RX_ADDR_[5]={0xB3,0xB4,0xB5,0xB6,0x05};

SPI1_CE_LOW();//CE=0待机模式
  TX_Array[0]=0x03;//允许接收通道00000011
SPI_NRF_Write(SPI1,W_REGISTER+EN_RXADDR,TX_Array,1);
  TX_Array[0]=0x03;//设置地址宽度11--5字节 10--4字节 01-3字节 00--不合法
SPI_NRF_Write(SPI1,W_REGISTER+SETUP_AW,TX_Array,1);
  TX_Array[0]=0x20;//射频通道 X0000010
SPI_NRF_Write(SPI1,W_REGISTER+RF_CH,TX_Array,1);
  TX_Array[0]=0x0f;//射频参数寄存器 00001111 2Mbps 发射功率 00-18dBm 01-12dBm 10-6dBm 11-0dBm 1--低噪声放大器增益
SPI_NRF_Write(SPI1,W_REGISTER+RF_SETUP,TX_Array,1);
  TX_Array[0]=0x3f;//xx11 11110-5通道允许自动应答
SPI_NRF_Write(SPI1,W_REGISTER+EN_AA,TX_Array,1);
  TX_Array[0]=0x04;//xx11 1111数据通道0 有效数据宽度 (1-32)字节
SPI_NRF_Write(SPI1,W_REGISTER+RX_PW_P0,TX_Array,1);
  TX_Array[0]=0xfe;//1111 xxxxSTATUS寄存器 写‘1’清除所有标志
SPI_NRF_Write(SPI1,W_REGISTER+STATUS,TX_Array,1);

SPI_NRF_Write(SPI1,W_REGISTER+TX_ADDR,_TX_RX_ADDR_,5);//写入接收发送数据的地址,这个地址是接收端收件的凭证
SPI_NRF_Write(SPI1,W_REGISTER+RX_ADDR_P0,_TX_RX_ADDR_,5);//写入接收发送数据的地址,这个地址是接收端收件的凭证



TX_Array[0]=0x0f;//接收模式 PRIM_RX=1 PWR_UP=1允许接收终端
SPI_NRF_Write(SPI1,W_REGISTER+CONFIG,TX_Array,1);

SPI1_CE_HIGH();//CE=1使能发射模式
Delay_us(100);//CE拉高需要一定的延时才能进行发送 延时之后即可通过SPI接口发送TX_PLD
//轮询中断24L01中断的到来 NRF_Read_IRQ()
}


下面是两种模式的测试


ErrorStatus SPI_NRF_TX_DATAS(u8* TBuff,u8 ByteNUM)
{
   u8 Status[1];
do{
  SPI1_CE_LOW();//拉低待机
   SPI_NRF_Write(SPI1,W_TX_PAYLOAD,TBuff,ByteNUM);//发送TBuff数组
  SPI1_CE_HIGH();//拉低待机
  }while(NRF_Read_IRQ()!=0);//中断产生时,IRQ引脚低电平
SPI_NRF_Write(SPI1, FLUSH_TX,TBuff,0);
SPI1_CE_LOW();//拉低待机
Delay_us(100);
SPI_NRF_Read(SPI1,R_REGISTER+STATUS,Status,1);//读取Status
LCD_Num_6x12_O(200,20,Status[0],WHITE);
if(Status[0]&0x10)
{
  Status[0]&=0x10;
  SPI_NRF_Write(SPI1,W_REGISTER+STATUS,Status,1);//
  LCD_Str_6x12_O_P(220 , 10 ,"TxError!", WHITE);//重发超时 发送失败
  return ERROR;
}
else
{
  Status[0]&=0x20;
  SPI_NRF_Write(SPI1,W_REGISTER+STATUS,Status,1);//
  LCD_Str_6x12_O_P(240 , 10 ,"TxSuccess", WHITE);//发送成功
  return SUCCESS;
}
}


ErrorStatus SPI_NRF_RX_DATAS(u8* RBuff)
{
   ErrorStatusRX_Status=SUCCESS;
    u8Status[1];
while(NRF_Read_IRQ()!=0);//中断产生时,IRQ引脚低电平
SPI1_CE_LOW();//拉低待机,才能操作寄存器
Delay_us(100);
SPI_NRF_Read(SPI1,R_REGISTER+STATUS,Status,1);//读取Status
switch(Status[0]&0x0e)
{
case 0x0e: RX_Status=ERROR;break; //RX_FIFO 空
default :LCD_Str_6x12_O_P(200 , 10 ,"RxSuccess!", WHITE);//RX_FIFO非空
    break;

}

SPI_NRF_Read(SPI1,R_RX_PAYLOAD,RBuff,4);//读RX_FIFO
SPI_NRF_Write(SPI1,W_REGISTER+STATUS,Status,1);//处理状态寄存器标志
return RX_Status;
}


特别要注意的是在发射模式的时候,应该先拉低CE,先在TX FIFO里写入要发射的数据,再拉高CE真正发射。



分享到:  QQ好友和群QQ好友和群 QQ空间QQ空间 腾讯微博腾讯微博 腾讯朋友腾讯朋友
收藏收藏9 分享淘帖 顶 踩
回复

使用道具 举报

沙发
ID:53728 发表于 2016-3-21 21:51 | 只看该作者
很好,写的挺好的
回复

使用道具 举报

板凳
ID:140769 发表于 2017-3-9 21:30 | 只看该作者
楼主写的不错,赞!
回复

使用道具 举报

地板
ID:169508 发表于 2017-3-9 22:00 | 只看该作者
...我的模块接收距离贼JB近,感觉像买了假模块
回复

使用道具 举报

5#
ID:134810 发表于 2017-9-29 12:46 | 只看该作者
调通了。又是百度,有事看别人的程序,有事学spi,又是看寄存器,唉。终于搞起了
回复

使用道具 举报

6#
ID:364163 发表于 2018-7-3 16:51 | 只看该作者
492324317 发表于 2017-3-9 22:00
...我的模块接收距离贼JB近,感觉像买了假模块

针对距离短这一块有可能是设置没有设置好。我这边有一个SI24R1通讯距离是150-200米的呢。
回复

使用道具 举报

7#
ID:295704 发表于 2018-11-1 19:08 | 只看该作者
  楼主写的真好
回复

使用道具 举报

8#
ID:428879 发表于 2018-12-4 15:50 来自手机 | 只看该作者
statue一直是ff,换到其它寄存器也是ff这是为什么
回复

使用道具 举报

9#
ID:301896 发表于 2018-12-5 09:21 | 只看该作者
说的太好了,可惜没看懂。。
回复

使用道具 举报

10#
ID:459268 发表于 2019-1-1 13:26 | 只看该作者
STM32F411CE+OV2640+NRF24L01+,有没有时间,帮助开发者这个项目,有偿服务
回复

使用道具 举报

11#
ID:184706 发表于 2019-1-23 13:32 | 只看该作者
无有源码?
回复

使用道具 举报

12#
ID:478236 发表于 2019-3-12 15:34 | 只看该作者
有点错别字,写得不错,哈哈
回复

使用道具 举报

13#
ID:499653 发表于 2019-4-12 14:17 | 只看该作者
想问一下各位怎样实现32单片机与PC端通过NRf2401无线传输?nrf转usb模块买了但是不会用呀
回复

使用道具 举报

14#
ID:363849 发表于 2019-6-29 22:36 | 只看该作者
博主能不能贴一下源码,急需
回复

使用道具 举报

15#
ID:380498 发表于 2019-7-2 11:13 | 只看该作者
对24L01深恶痛绝啊,那东西太难用了,做产品的时候幺蛾子不断,楼主的文章不错
回复

使用道具 举报

16#
ID:689081 发表于 2020-2-9 16:50 | 只看该作者
宏定义能发一下吗
回复

使用道具 举报

17#
ID:691457 发表于 2020-2-9 18:35 | 只看该作者
楼主的文章不错
回复

使用道具 举报

18#
ID:156302 发表于 2020-2-10 11:40 | 只看该作者
刚好在做这个, 学习一下,感谢分享
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

手机版|小黑屋|51黑电子论坛 |51黑电子论坛6群 QQ 管理员QQ:125739409;技术交流QQ群281945664

Powered by 单片机教程网

快速回复 返回顶部 返回列表