无线通信NRF24L01---使用笔记

关于NRF24L01的资料,网上已很详尽了。这篇文章不细红了描述各知识点。

这篇文章,旨在把各处主要知识点胶接起来,梳理成一套完整的步骤,使器件快速上手汇入工作使用。

将按操作顺序,拆分成6个步骤,只注解重点和暗坑。以方便自己进行知识管理,亦方便同行兄弟查阅。

代码方面,尽量做到一行代码一行解释。(如有订正和疑问,可留言交流,将迅速回复。)

  • 一、思维导图
  • 二、引脚连接,解释
  • 三、SPI初始化,函数
  • 四、NRF参数写入
  • 五、中断处理函数
  • 六、发送数据
  • 七、接收数据

一、思维导图

工作模式、功耗、命令字等等这些,不用文字啰嗦,直接思维导图更有效;

花两天时间翻阅各种零星资料,对主要点进行了组织,包括了工作模式、命令字等图例,认真过一遍,心里马上就能有个框框。

点击一下图片能变成清晰的大图。当然,右击保存就更方便查看了。


第二部分: 引脚连接,解释

明确一个,NRF24L01和SI24R1的引脚、程序是通用的,无需任何修改,两者间通信也互通,已测试确认!

共8个引脚:

  1. GND:接地
  2. VCC:3.3V。1.9~3.6V都成,不要接5V,马上烧!
  3. CE:     模式控制。其高低电平,配合CONFIG寄存器PRIM_RX和PWR_UP两个位,可切换工作状态: 发送、接收 、待机 。
  4. IRQ:    中断信号引脚。发生以下三种中断时,引脚电平被NRF拉低:发送成功、接收到数据、已达最大重发次数
  5. CSN:   SPI的CS片选引脚
  6. SCK:   SPI的时钟线引脚
  7. MOSI: SPI的主出从入数据引脚
  8. MISO: SPI的主入从出数据引脚

原代码程序中,有个连接测试函数,可以作模块的连接,没有贴上来,留邮箱发最新版本代码。


第三部分:SPI初始化、函数封装

NRF的配置,就是把各种参数(数值),如频道,速率,目标地址等,用SPI方式写到指定地址(芯片的寄存器).

 这个写入配置的动作,拆分开来看,理解为两部分,是在操作两种通信,别混乱。

首先主机按NRF datasheet的要求,设置和通过SPI通信,向NRF芯片特定地址(寄存器)写入参数值;

而这些写入的数值,就是用于控制NRF与别一个NRF的通信参数。

这两个通信,理解一下~

1:主机和NRF间的通信:       

  • 使用SPI,主机完成对NRF操控,所有操控其实就是4个操作:写参数、读参数、写要发送的数据,读出收到的数据
  • 上面的操控,分拆到SPI的操作上,就是常用的6个函数:SPI初始化、字节收发、写1字节,读1字节,写N字节,读N字节;
  • 写入参数:是NRF与NRF间通信的参数值, 如频道 ,速率,CRC校检,自动回答,自动重发,目标地址....
  • 读取参数,主要是STATUS状态寄存器的值,用于判断中断源;
  • 写入发送数据,把待发送的数据写到TX_FIFO. NRF按包发送数据,包中有效数据最大32字节;要手动做分包处理;
  • 读出收到数据,检测到RX_DR中断发生后(IRQ引脚被拉低),用SPI把RX_FIFO缓冲区的值,读取存放到指自已的缓冲区;
  • NRF的SPI通信速度可达10MHz,但为保证数据传输的完整不掉包,尽量不要超过8MHz;
  • NRF要求SPI通信时,在上升沿采样数据,要注意时序。

2:NRF和NRF间的收发通信:

  • NRF按主机刚才写到芯片的参数值开始工作(手动),通过电波传输到另一个芯片中或接收别一芯片的数据(自动)。
  • 这部份通信我们要做的,仅是控制CE引脚的高低电平,配合CONFIG寄存器,使NRF在接收、发送、待机三种状态切换。
  • NRF间的无线通信,基本没我们什么事。

开始码代码, 工作顺序:  GPIO初始化  >  SPI初始化  >  SPI收发函数  >  写入NRF通信用的参数  >  中断函数  >  收发函数

GPIO初始化代码:

/*** SPI通信引脚, CS, SCK, MOSI, MISO  ***/
GPIOSet (NRF24L01_SPI_CSN_GPIO,  NRF24L01_SPI_CSN_PIN,   G_MODE_OUT , G_OTYPE_PP,  G_OSPEED_25M,  G_PUPD_UP ,     0);            // cs
GPIOSet (NRF24L01_SPI_CLK_GPIO , NRF24L01_SPI_CLK_PIN ,  G_MODE_AF ,  G_OTYPE_PP , G_OSPEED_25M , G_PUPD_DOWN ,   G_AF_SPI2 );  // sck
GPIOSet (NRF24L01_SPI_MOSI_GPIO ,NRF24L01_SPI_MOSI_PIN , G_MODE_AF ,  G_OTYPE_PP , G_OSPEED_25M , G_PUPD_DOWN ,   G_AF_SPI2 );  // mosi
GPIOSet (NRF24L01_SPI_MISO_GPIO ,NRF24L01_SPI_MISO_PIN , G_MODE_AF ,  G_OTYPE_PP , G_OSPEED_25M , G_PUPD_DOWN ,   G_AF_SPI2 );  // miso	
    
/*** NRF控制引脚, CE, IRQ ***/
GPIOSet (NRF24L01_CE_GPIO ,      NRF24L01_CE_PIN ,       G_MODE_OUT , G_OTYPE_PP , G_OSPEED_25M , G_PUPD_DOWN ,   0);           // CE
GPIOSet (NRF24L01_IRQ_GPIO ,     NRF24L01_IRQ_PIN ,      G_MODE_IN  , G_OTYPE_PP , G_OSPEED_25M , G_PUPD_UP   ,   0);           // IRQ   NRF24L01芯片的IRQ引脚下拉能力很弱,注意外部上拉电阻大小,及MCU的上下拉
    

GPIOSet是个自己封装的初始化函数,不用管,重点看其中的参数就好,注意各上下拉。

SPI 初始化代码:

/*** SPI通信部分 ***/	
CSN_HIGH;                      //失能NRF
NRF24L01_SPI_EN_CLOCK ;        // 时钟
NRF24L01_SPIX->CR1 = 0;        // 清0
NRF24L01_SPIX->CR1 |= 0<<0;    // 采样沿数, NRF要求上升沿采样   CPHA:时钟相位,0x1=在第2个时钟边沿进行数据采样, 
NRF24L01_SPIX->CR1 |= 0<<1;    // 时钟线闲时极性,  CPOL:时钟极性,0x1=空闲状态时,SCK保持高电平
NRF24L01_SPIX->CR1 |= 1<<2;    // 主从模式,       0=从,1=主
NRF24L01_SPIX->CR1 |= 2<<3;    // 波特率控制[5:3], 0=fPCLK/2,  1=/4倍  2=/8  3/16
NRF24L01_SPIX->CR1 |= 0<<7;    // LSB先行,        0=MSB,  1=LSB
NRF24L01_SPIX->CR1 |= 1<<8;    // 内部从器件选择,根据9位设置(失能内部NSS)
NRF24L01_SPIX->CR1 |= 1<<9;    // 软件从器件管理 :  0=禁止软件管理从设备, 1=使能软件从器件管理(软件NSS)
NRF24L01_SPIX->CR1 |= 0<<11;   // 数据帧格式,       0=8位,  1=16位	

NRF24L01_SPIX->CR1 |= 1<<6;    // 使能

SPI 初始化重点:

  • 一行一注释,注意细看
  • NRF要求在上升沿时采集数值。这就要闲时电平和采样沿要配合好。
  • NRF的SPI可达10MHz, 但实际使用时,不要超过8MHz。这个在SPI的波特率中可控制,因为SPI通信速率受限低速一方。

插个话题,为什么使用寄存器操作编程?因为:使用寄存器=简单+清晰,你看,不是吗?

SPI收发函数

/*****************************************************************************
*函  数: u8 SPI_RW(u8 Data)
*功  能: SPI写入一个字节,并返回一个字节
*参  数: 要写入的一字节
*返回值: 返回一字节数据
*****************************************************************************/
u8 SPI_SendByte(u8 Data)
{
    u8 retry =0;
    while((NRF24L01_SPIX ->SR & 2) == 0){   // 理解方式,应该把前式的结果理解为一个寄存器位值,如果这个位值是等号后面的值,就等待
        retry++;
        if(retry>200) return 0;
    }
    NRF24L01_SPIX ->DR = Data;
	
    retry=0;
	while((NRF24L01_SPIX->SR & 1) == 0 ){
        retry++;
        if(retry>200) return 0;
    }
	return NRF24L01_SPIX->DR ;
}

/*****************************************************************************
*函  数:u8 Nrf24l01_WriteReg(u8 reg,u8 value)
*功  能:向指定寄存器地址,写一个字节数据
*参  数:reg: 寄存器地址
*       val:  要写入的值
*返回值:status
*****************************************************************************/
u8 Nrf24l01_WriteReg(u8 reg,u8 value)
{
	u8 status;    
	CSN_LOW;
	status = SPI_SendByte(reg) ;
	SPI_SendByte(value);
	CSN_HIGH;    
	return status;
}

/*****************************************************************************
*函  数:u8 NRF24l01_read_reg(u8 reg)
*功  能:向指定寄存器地址,读出一字节数据
*参  数:reg: 寄存器地址
*返回值:reg_val(第二个读取到的字节)
*****************************************************************************/
u8 Nrf24l01_ReadReg(u8 reg)
{
	u8 reg_val;    
	CSN_LOW;
	SPI_SendByte(reg);
	reg_val = SPI_SendByte(0xFF);
	CSN_HIGH;    
	return reg_val;
}

/*****************************************************************************
*函  数:u8 Nrf24l01_WriteBuf(u8 reg, u8 *pBuf, u8 len)
*功  能:写一组数据到寄存器
*参  数:reg: 寄存器地址
*       pBuf: 要写入数据的地址
*        len:  要写入的数据长度
*返回值:status
*备  注:NRF2401代码移植只需把SPI驱动修改成自己的即可
*****************************************************************************/
u8 Nrf24l01_WriteBuf(u8 reg, u8 *pBuf, u8 len)
{
	u8 status;    
	CSN_LOW;
	status = SPI_SendByte(reg);
	for(u8 i=0; i<len; i++)	{
		SPI_SendByte(pBuf[i]);		
	}
	CSN_HIGH;    
	return status;
}

/*****************************************************************************
*函  数: u8 vNrf24l01_ReadBuf(u8 reg, u8 *pBuf, u8 len)
*功  能: 向指定寄存器地址,读出指定长度的数据
*参  数: reg :    寄存器地址
*        pBuf :   数据存放缓冲区
*        len  :   读取的字节数量
*返回值: status : 设备状态字
*****************************************************************************/
u8 Nrf24l01_ReadBuf(u8 reg, u8 *pBuf, u8 len)
{
	u8 status;	
	CSN_LOW;
	status = SPI_SendByte(reg);
	for(u8 i = 0; i<len ;i++){
		pBuf[i] = SPI_SendByte(0xFF);		
	}
	CSN_HIGH;    
	return status;
}

第四部分:NRF24L01参数写入

使用上面初始化的SPI,和刚封装好的几个函数,就可以把需要的参数,写到NRF特定的地址(寄存器), 完成对其配置。

NRF24L01 参数配置代码:

/*** NRF24L01通信配置 30,2M,***/    
CE_LOW;                                                          // 热待机模式, 只有在ce置低时,才能配置寄存器
//delayUs(2000);                                                 // PowerDown 切换为 PowerUp需要1.5ms 
Nrf24l01_WriteReg(W_REGISTER + RF_CH,        30);                // 射频通道,即频率(0-125)
Nrf24l01_WriteReg(W_REGISTER + RF_SETUP,   0x0F);                // 设置TX发射参数,0db增益,2Mbps,低噪声增益关闭 (注意:低噪声增益关闭/开启直接影响通信,要开启都开启,要关闭都关闭0x0f)0x07
Nrf24l01_WriteReg(W_REGISTER + SETUP_AW,   0x03);                // 地址长度,默认值时0x03,即5字节
Nrf24l01_WriteBuf(W_REGISTER + TX_ADDR,    (u8*)TX_ADDRESS, 5);  // 写TX节点地址, 地址宽度:5字节,40位 
Nrf24l01_WriteBuf(W_REGISTER + RX_ADDR_P0, (u8*)TX_ADDRESS, 5);  // 设置TX节点地址,主要为了使能ACK,, 地址宽度:5字节,40位
Nrf24l01_WriteReg(W_REGISTER + SETUP_RETR, 0x0A);                // 设置自动重发间隔时间:500us + 86us;最大自动重发次数:10次 0x1A	
Nrf24l01_WriteReg(W_REGISTER + EN_RXADDR,  0x01);                // 使能通道0的接收地址  
Nrf24l01_WriteReg(W_REGISTER + EN_AA,      0x01);                // 使能通道0自动应答
Nrf24l01_WriteReg(W_REGISTER + RX_PW_P0,     32);                // 选择通道0的有效数据宽度  
Nrf24l01_WriteReg(FLUSH_TX,                0xff);                // 清除TX_FIFO
Nrf24l01_WriteReg(W_REGISTER+STATUS,       0X7E);                // 清除所有中断,防止一进去接收模式就触发中断    
Nrf24l01_WriteReg(W_REGISTER+CONFIG,       0x0F);                // 配置为接收模式    
CE_HIGH;   	                                                 // CE置高,进入状态

重点:

  • CE引脚置低,方可配置NRF寄存器!  各教程都这教的,但测试过不置低也可正常配置。
  • RF_CH,:频道,2.4G为基址,1M为间隔值,如上面的30,代表使用2.430G的频率进行芯片间通信。可设值0~125。
  • RF_SETUP: 发射功率可配置,说白了就是的耗电程度。接收状态的功率是不能设置的,所以接收才是耗电的大头。数据传输率,常说的空中速率,一般设置2M
  • 地址长度,5字节,这个很独特
  • TX_ADDR:数据要发送到的目标地址
  • RX_ADDR_P0: 接收通道0的接收地址,接收这个地址设备发来的数据,这里设为和TX_ADDR一致,是为了自动应答。
  • NRF共有6个接收通道,p0~p5, 可同时监听6个不同地址的信号,p0通道也用于作自动应答作用。
  • TX_FIFO, 发送数据缓冲区,96字节,32字节为一组,共3组,可理解为缓冲区可存放3组发送数据。
  • RX_FIFO, 同上,两个FIFO相互独立。 虽然是FIFO,虽然有3组,但最好还是一包一包收发,状态切换时间难把握。
  • 倒数第二行,配置为接收模式,注意,这个时候还没开始工作的,还处于PownDown状态,状态和模式是两回事。
  • 最后一行,CE置高10us后,才进入工作状态,因之前配置的是接收模式,所以将进入接收状态

第五部分:中断处理函数

为什么要先说中断?感觉先了解了中断,那么发送、接收就更好理解。

其实不应该叫中断的,但这样好理解,还是遵从约定俗成吧。

中断时, IRQ电平被拉低,是由NRF控制产生的,三种情况可触发:发送成功、达到重发最大次数、接收到数据

发送成功:

  • 1:PTX发送数据后,马上开始计时,130us后切换到接收模式,此时计时还在继续
  • 2:PRX在收到数据后,经CRC校检,数据完整后发回ACK信号
  • 3:PTX在规定时间内收到ACK信号,则置位TX_DS标志,IRQ引脚被拉低

达到重发最大次数:

  • 1:PTX发送数据后,马上开始计时,130us后切换到接收模式,此时计时还在继续
  • 2:PTX在规定时间内,没收到ACK信号,原因很多:如PRX没收到数据,CRC校验错误....
  • 3:PTX再次发送一次数据,重复步骤1
  • 4:SETUP_RETR寄存器可设置重发的次数,达到最大次数后,MAX_RT位被置位,IRQ引脚被拉低。

接收到数据:

  • PRX收到数据,经CRC校检,数据完整,有效数据存放到RX_FIFO,RX_DR位置高,IRQ引脚被拉低。

说说清理中断,要清理的中断有两处:

  • NRF的中断位,上面的三种标志中断位,都在寄存器STATUS中,各位置1可清0,IRQ引脚即回到高电平。
  • 单片机mcu的中断位,清理外部中断线标志位,否则会在中断函数里死循环。

中断处理函数代码:

void NRF24L01_IRQ_IRQHANDLER(void)
{                    
    u8  status=0 ;

    CE_LOW;                                               // 拉低CE,以便读取NRF中STATUS中的数据	
    status = Nrf24l01_ReadReg(R_REGISTER + STATUS);       // 读取STATUS中的数据,以便判断是由什么中断源触发的IRQ中断

    /*** 发送完成中断 ***/
    if(status & STATUS_TX){	                                     
        Nrf24l01_WriteReg(W_REGISTER+STATUS, STATUS_TX);  // 清NRF中断:发送完成
        Nrf24l01_WriteReg(FLUSH_TX, 0xff);                // 清发送缓冲区:TX_FIFO
        printf("\r\n发送成功!!!!\r\n");
        vNrf24l01_RxMode ();                              // 切换为接收状态
    }

    /*** 接收完成中断 ***/
    if(status & STATUS_RX){ 
        memset (NRF_RX_DATA , 0, 32);           
        Nrf24l01_ReadBuf(R_RX_PAYLOAD, NRF_RX_DATA , RX_PAYLO_WIDTH); // 读数据
        Nrf24l01_WriteReg(W_REGISTER+STATUS, STATUS_RX);              // 清NRF中断:收到数据
        Nrf24l01_WriteReg(FLUSH_RX,0xff);                             // 清除RX FIFO(注意:这句话很必要)   	

        printf("\r\n接收到数据: %s\r\n", NRF_RX_DATA);	        
        vNrf24l01_RxMode ();                             // 切换为接收状态
    }

    /*** 最大重发次数中断 ***/
    if(status & STATUS_MAX){	
        Nrf24l01_WriteReg(W_REGISTER+STATUS, 0x70);       // 清NRF中断:三个
        Nrf24l01_WriteReg(FLUSH_TX, 0xff);                // 清除TX_FIFO      

        printf("\r\n发送失败,达到最大重发次数!!!\r\n"); 
        vNrf24l01_RxMode();                               // 切换为接收状态
    }

    EXTI->PR |= NRF24L01_IRQ_PIN ;                        // 清理外部中断线标志位
}    

第六部分:发送数据

NRF的数据收发,是一包一包进行的,一包(帧)数据:包括了前导码、目标地址、包控制域、有效数据、CRC, 但我们只管有效数据,其它的不用我们负责,NRF发送时自动打包,接收到数据时自动拆包。

每一包的有效数据最大为32个字节。当然,也可以只发一个字节的数据。

要发送的数据大于32字节,就要分包进行,自行手动分包处理。

因为在配置部分时,已配置好了频道,速率,重发次数等各种参数,在需要发送数据时,只要往芯片写入要发送的数据和地址,然后切换为发送状态,芯片就会自动发送。

发送成功(收到ack),会产生TX_DS中断。

发送失败了(达到最大重发次数), 也会产生MAR_RT中断。

在中断函数里,根据情况作处理就好。

发送数据代码:

void vNrf24l01_TxPacket(u8 *txbuf)
{
    CE_LOW; 
    Nrf24l01_WriteBuf(W_TX_PAYLOAD,           txbuf,          32);   // 写数据到TX_BUFF    
    Nrf24l01_WriteBuf(W_REGISTER+TX_ADDR,    (u8*)TX_ADDRESS,  5);   // 写入要发送的目标地址 
    Nrf24l01_WriteBuf(W_REGISTER+RX_ADDR_P0, (u8*)TX_ADDRESS,  5);   // 通道0的地址设为和目标地址一致,以接收自动回复auto_ack信号 
    Nrf24l01_WriteReg(W_REGISTER+CONFIG,      0x0E);	             // 设置为发送模式,开启所有中断	    
    CE_HIGH;	
}

发送就这几句!

重点:RX_ADDR_P0的地址和TX_ARRD一样,目的是自动应答。有个前提,在配置中已使能自动应答。

把操作封装成一个函数,要发什么,就往函数里掉数据就好,每次不要大于32字节。


第七部分:接收数据

当系统或程序运行时,大部分时间是运行在接收状态下的。如:

  • 第四个部分配置步骤完成后,程序已处在接收状态。
  • 第五个部分,在中断处理函数中,三种中断处理后,也切换为接收状态,当然,也可以切换为更省电的待机状态。

NRF有6个接收通道,指在可同时监听接收同一个频道,同一速率的6个不同设备的数据。

常用的只是通道P0, 如果只使能了通道P0,那就只能接收到P0中地址设备发来的数据。

可以使能全部6个通道,设置6个不同设备地址,就可以监听接收6个设备发来的数据(同一时间,只能接收其中1个设备的数据);

注意,接收数据,指在接收中断发生后,我们从RX_FIFO中把数据读存到主机。而中断发生前的监听接收,NRF自动完成。

接收数据的代码:

接收本来就是最简单的,没啥特别代码,下面的代码,只是在中断处理函数里,再贴出来而已。

Nrf24l01_ReadBuf(R_RX_PAYLOAD, NRF_RX_DATA , RX_PAYLO_WIDTH); // 读数据到数组
Nrf24l01_WriteReg(W_REGISTER+STATUS, STATUS_RX);              // 清NRF中断:收到数据
Nrf24l01_WriteReg(FLUSH_RX,0xff);                             // 清除RX FIFO(注意:这句话很必要)  

把接收到的数据,先存放NRF_RX_DATA数组,再进行处理,不要在中断函数中处理。

读完后,记得要清理RX_FIFO,不然它一直占用NRF的缓冲区。RX_FIFO共96字节,分成32字节3组,NRF每次接收到数据就存放到最后面的一组中,当存满了3组,后面再接收到的数据,就会被NRF掉弃。


最后,原来想把原工程代码上传上来的,但下载时要消耗积分...

哪位朋友如果需要原代码的,留个邮箱地址 ,给你发最新测试修改好的工程文件

写了三四个小时,累~~~

发布了49 篇原创文章 · 获赞 25 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/zhouml_msn/article/details/104334432