现在无线在我们的生活中无处不在。而我们开发的物联网产品也大量使用无线通讯。在这一篇文章中,我们将讨论nRF24L01无线通讯模块驱动程序的开发与实现。
1、功能概述
nRF24L01是一款工作在2.4~2.5GHz世界通用ISM 频段的单片无线收发器芯片无线收发器包括:频率发生器、增强型SchockBurst模式控制器、功率放大器、晶体振荡器、调制器、解调器。输出功率、频道选择和协议的设置可以通过SPI 接口进行设置。其封装及引脚定义如下:
1.1、工作模式
nRF24L01无线通讯模块可以设置为多种不同的工作模式:待机模式、掉电模式、数据包处理方式。各模式的功能及操作如下:
1.1.1、待机模式
待机模式I在保证快速启动的同时减少系统平均消耗电流。在待机模式I下,晶振正常工作。在待机模式II下部分时钟缓冲器处在工作模式。当发送端TX FIFO寄存器为空并且CE为高电平时进入待机模式II。在待机模式期间,寄存器配置字内容保持不变。
1.1.2、掉电模式
在掉电模式下,nRF24L01各功能关闭,保持电流消耗最小。进入掉电模式后,nRF24L01停止工作,但寄存器内容保持不变。掉电模式由寄存器中PWR_UP位来控制。
1.1.3、数据包处理方式
nRF24L01数据包处理方式包括ShockBurst模式和增强型ShockBurst模式。
ShockBurst模式下nRF24L01可以与成本较低的低速MCU相连。高速信号处理是由芯片内部的射频协议处理的,nRF24L01提供SPI接口,数据率取决于单片机本身接口速度。ShockBurst模式通过允许与单片机低速通信而无线部分高速通信,减小了通信的平均消耗电流。
增强型ShockBurst模式可以使得双向链接协议执行起来更为容易、有效。典型的双向链接为:发送方要求终端设备在接收到数据后有应答信号,以便于发送方检测有无数据丢失。一旦数据丢失,则通过重新发送功能将丢失的数据恢复。增强型的ShockBurstTM模式可以同时控制应答及重发功能而无需增加MCU工作量。
1.2、数据通讯
1.2.1、通讯指令及数据包
nRF24L01所有配置都在配置寄存器中。所有寄存器都是通过SPI口进行配置的。SPI接口采用标准的SPI接口,其最大的数据传输率为10Mbps。指令格式采用命令字加数据字节的格式。其中命令字由高位到低位(每字节);数据字节从低字节到高字节,每一字节高位在前。nRF24L01支持的指令如下:
R_REGISTER和W_REGISTER寄存器可能操作单字节或多字节寄存器。当访问多字节寄存器时首先要读/写的是最低字节的高位。在所有多字节寄存器被写完之前可以结束写SPI操作,在这种情况下没有写完的高字节保持原有内容不变。例如RX_ADDR_P0寄存器的最低字节可以通过写一个字节给寄存器RX_ADDR_P0来改变。在CSN状态由高变低后可以通过 MISO 来读取状态寄存器的内容。
nRF24L01在增强型ShockBurst模式下和ShockBurst模式下的数据包格式略有不同。
增强型ShockBurst模式下的数据包形式如下:
ShockBurst模式下的数据包形式如下:
在数据包中,前导码用来检测0和1。芯片在接收模式下去除前导码,在发送模式下加入前导码。地址内容为接收机地址。地址宽度可以是3、4或5字节宽度。地址可以对接收通道及发送通道分别进行配置。从接收的数据包中自动去除地址。标志位就是PID数据包识别号,后两位会在每次接收到新的数据包后加,前7位保留。CRC校验是可选的,0-2字节宽度的CRC校验。若采用8位CRC校验,则其特征多项式是:X8 +X2 +X+1;若采用16位CRC校验,则其特征多项式是:X16+X12+X5 +1。
1.2.2、数据通道
nRF24L01配置为接收模式时可以接收6路不同地址相同频率的数据。每个数据通道拥有自己的地址并且可以通过寄存器来进行分别配置。数据通道是通过寄存器EN_RXADDR来设置的,默认状态下只有数据通道0和数据通道1是开启状态的。每一个数据通道的地址是通过寄存器RX_ADDR_Px来配置的。通常情况下不允许不同的数据通道设置完全相同的地址。数据通道0有40位可配置地址。数据通道1~5的地址为32位共用地址+各自的地址(最低字节)。如下所示:
2、驱动设计与实现
我们已经了解了nRF24L01无线通讯模块的功能及操作方式,接下来我们将设计并实现nRF24L01无线通讯模块的驱动程序。
2.1、对象定义
在使用一个对象之前我们需要获得一个对象。同样的我们想要nRF24L01无线通讯模块就需要先定义nRF24L01无线通讯模块的对象。
2.1.1、对象的抽象
我们要得到nRF24L01无线通讯模块对象,需要先分析其基本特性。一般来说,一个对象至少包含两方面的特性:属性与操作。接下来我们就来从这两个方面思考一下nRF24L01无线通讯模块的对象。
先来考虑属性,作为属性肯定是用于标识或记录对象特征的东西。我们来考虑nRF24L01无线通讯模块对象属性。nRF24L01有一些寄存器用于配置工作状态,所以我们将这些寄存器状态作为对象的属性。
接着我们还需要考虑nRF24L01无线通讯模块对象的操作问题。我们通过nRF24L01来收发数据就需要读写SPI接口,而这与特定的硬件平台相关,所以我们将其作为对象的操作。而片选信号和使能信号以及中断输入信号也都与具体的操作平台有关,所以我们也将其作为对象的操作。在进行相关操作时,我们需要控制时序,则需要使用延时操作,但延时处理总是依赖于具体的软硬件平台,所以我们将延时处理作为对象的操作。
根据上述我们对nRF24L01无线通讯模块的分析,我们可以定义nRF24L01无线通讯模块的对象类型如下:
/* 定义NRF24L01对象类型 */
typedef struct NRF24L01Object {uint8_t reg[8];//记录前8个配置寄存器uint8_t (*ReadWriteByte)(uint8_t TxData);//声明向nRF24L01读写一个字节的函数void (*ChipSelect)(NRF24L01CSType cs);//声明片选操作函数void (*ChipEnable)(NRF24L01CEType en);//声明使能及模式操作函数uint8_t (*GetIRQ)(void);//声明中断获取函数void (*Delayms)(volatile uint32_t nTime); //毫秒延时操作指针
}NRF24L01ObjectType;
2.1.2、对象初始化
我们知道,一个对象仅作声明是不能使用的,我们需要先对其进行初始化,所以这里我们来考虑nRF24L01无线通讯模块对象的初始化函数。一般来说,初始化函数需要处理几个方面的问题。一是检查输入参数是否合理;二是为对象的属性赋初值;三是对对象作必要的初始化配置。据此我们设计nRF24L01无线通讯模块对象的初始化函数如下:
/*nRF24L01对象初始化函数*/
NRF24L01ErrorType NRF24L01Initialization(NRF24L01ObjectType *nrf, //nRF24L01对象NRF24L01ReadWriteByte spiReadWrite, //SPI读写函数指针NRF24L01ChipSelect cs, //片选信号操作函数指针NRF24L01ChipEnable ce, //使能信号操作函数指针NRF24L01GetIRQ irq, //中断信号获取函数指针NRF24L01Delayms delayms //毫秒延时)
{int retry=0;if((nrf==NULL)||(spiReadWrite==NULL)||(ce==NULL)||(irq==NULL)||(delayms==NULL)){return NRF24L01_InitError;}nrf->ReadWriteByte=spiReadWrite;nrf->ChipEnable=ce;nrf->GetIRQ=irq;nrf->Delayms=delayms;if(cs!=NULL){nrf->ChipSelect=cs;}else{nrf->ChipSelect=NRF24L01CSDefault;}while(NRF24L01Check(nrf)&&(retry<5)){nrf->Delayms(300);retry++;}if(retry>=5){return NRF24L01_Absent;}for(int i=0;i<8;i++){nrf->reg[i]=0;}SetNRF24L01Mode(nrf,NRF24L01RxMode);return NRF24L01_NoError;
}
2.2、对象操作
我们已经完成了nRF24L01无线通讯模块对象类型的定义和对象初始化函数的设计。但我们的主要目标是获取对象的信息,接下来我们还要实现面向nRF24L01无线通讯模块的各类操作。
2.2.1、读操作
nRF24L01无线通讯模块有很多的寄存器,所谓读操作就是对这些寄存器的读取过程。这个过程就是使用前面我们介绍的命令去获取不同寄存器的数值。具体的时序过程如下所示:
根据上述时序图以及各寄存器的定义,我们将读nRF24L01无线通讯模块寄存器的方式分为两类:一类是读普通的单字节寄存器,这些寄存器主要与配置和状态有关;另一类是读多字节寄存器,这些寄存器与数据通讯相关。具体的实现如下:
/*读取寄存器值*/
static uint8_t NRF24L01ReadRegigster(NRF24L01ObjectType *nrf,uint8_t reg)
{uint8_t reg_val; nrf->ChipSelect(NRF24L01CS_Enable); //使能SPI传输 nrf->ReadWriteByte(reg); //发送寄存器号reg_val=nrf->ReadWriteByte(0XFF); //读取寄存器内容nrf->ChipSelect(NRF24L01CS_Disable); //禁止SPI传输return(reg_val); //返回状态值
}/*在指定位置读出指定长度的数据*/
static uint8_t NRF24L01ReadBuffer(NRF24L01ObjectType *nrf,uint8_t reg,uint8_t *pBuf,uint8_t len)
{uint8_t status; nrf->ChipSelect(NRF24L01CS_Enable); //使能SPI传输status=nrf->ReadWriteByte(reg); //发送寄存器值(位置),并读取状态值for(int i=0;i<len;i++){pBuf[i]=nrf->ReadWriteByte(0XFF);//读出数据}nrf->ChipSelect(NRF24L01CS_Disable); //关闭SPI传输return status; //返回读到的状态值
}
2.2.2、写操作
nRF24L01无线通讯模块有很多的寄存器,所谓写操作就是向这些寄存器写值的过程。在写寄存器之前一定要进入待机模式或掉电模式。虽然寄存器的位数等存在差异,但其操作过程基本是一样的。具体的时序过程如下所示:
同样的,根据上述时序图以及各寄存器的定义,我们将写nRF24L01无线通讯模块寄存器的方式分为两类:一类是写普通的单字节寄存器,这些寄存器主要与配置和状态有关;另一类是写多字节寄存器,这些寄存器与数据通讯相关。具体的实现如下:
/*写寄存器*/
static uint8_t NRF24L01WriteRegister(NRF24L01ObjectType *nrf,uint8_t reg,uint8_t value)
{uint8_t status;nrf->ChipSelect(NRF24L01CS_Enable); //使能SPI传输status =nrf->ReadWriteByte(reg); //发送寄存器号nrf->ReadWriteByte(value); //写入寄存器的值nrf->ChipSelect(NRF24L01CS_Disable); //禁止SPI传输return(status); //返回状态值
}/*在指定位置写指定长度的数据*/
static uint8_t NRF24L01WriteBuffer(NRF24L01ObjectType *nrf,uint8_t reg, uint8_t *pBuf, uint8_t len)
{uint8_t status;nrf->ChipSelect(NRF24L01CS_Enable); //使能SPI传输status = nrf->ReadWriteByte(reg); //发送寄存器值(位置),并读取状态值for(int i=0; i<len; i++){nrf->ReadWriteByte(pBuf[i]); //写入数据 }nrf->ChipSelect(NRF24L01CS_Disable); //关闭SPI传输return status; //返回读到的状态值
}
3、驱动的使用
前面我们已经设计并实现了nRF24L01无线通讯模块的驱动程序,我们还需要验证这一驱动程序的设计是否符合要求,所以在这一节中我们将基于nRF24L01无线通讯模块的驱动程序设计一验证应用。
3.1、声明并初始化对象
使用基于对象的操作我们需要先得到这个对象,所以我们先要使用前面定义的nRF24L01无线通讯模块类型声明一个nRF24L01无线通讯模块对象变量,具体操作格式如下:
NRF24L01ObjectType nrf;
声明了这个对象变量并不能立即使用,我们还需要使用驱动中定义的初始化函数对这个变量进行初始化。这个初始化函数所需要的输入参数如下:
NRF24L01ObjectType *nrf,nRF24L01对象
NRF24L01ReadWriteByte spiReadWrite,SPI读写函数指针
NRF24L01ChipSelect cs,片选信号操作函数指针
NRF24L01ChipEnable ce,使能信号操作函数指针
NRF24L01GetIRQ irq,中断信号获取函数指针
NRF24L01Delayms delayms,毫秒延时
对于这些参数,nRF24L01对象变量我们已经定义了。余下的参数是一些函数指针,这是我们需要定义的,并将函数指针作为参数。这几个函数的类型如下:
//声明向nRF24L01读写一个字节的函数
typedef uint8_t (*NRF24L01ReadWriteByte)(uint8_t TxData);
//声明片选操作函数
typedef void (*NRF24L01ChipSelect)(NRF24L01CSType cs);
//声明使能及模式操作函数
typedef void (*NRF24L01ChipEnable)(NRF24L01CEType en);
//声明中断获取函数
typedef uint8_t (*NRF24L01GetIRQ)(void);
//毫秒延时操作指针
typedef void (*NRF24L01Delayms)(volatile uint32_t nTime);
对于这几个函数我们根据样式定义就可以了,具体的操作可能与使用的硬件平台有关系。片选操作函数用于多设备需要软件操作时,如采用硬件片选可以传入NULL即可。具体函数定义如下:
/* 基于HAL库的SPI读写字节函数 */
static uint8_t NRF24L01ReadWrite(uint8_t txData)
{uint8_t rxData=0;HAL_SPI_TransmitReceive(&nrf24l01hspi,&txData,&rxData,1,1000);return rxData;
}/*实现片选*/
static void NRF24L01ChipSelectf(NRF24L01CSType cs)
{if(NRF24L01CS_Enable==cs){HAL_GPIO_WritePin(GPIOF, GPIO_PIN_4, GPIO_PIN_RESET);}else{HAL_GPIO_WritePin(GPIOF, GPIO_PIN_4, GPIO_PIN_SET);}
}/*实现使能*/
static void NRF24L01ChipEnablef(NRF24L01CEType en)
{if(NRF24L01CE_Enable==en){HAL_GPIO_WritePin(GPIOF, GPIO_PIN_5, GPIO_PIN_RESET);}else{HAL_GPIO_WritePin(GPIOF, GPIO_PIN_5, GPIO_PIN_SET);}
}/*实现Ready状态监视*/
static uint8_t NRF24L01GetIRQf(void)
{return HAL_GPIO_ReadPin(GPIOC,GPIO_PIN_0);
}
对于延时函数我们可以采用各种方法实现。我们采用的STM32平台和HAL库则可以直接使用HAL_Delay()函数。于是我们可以调用初始化函数如下:
NRF24L01Initialization(&nrf,NRF24L01ReadWrite,NRF24L01ChipSelectf,NRF24L01ChipEnablef,NRF24L01GetIRQf,HAL_Delay);
3.2、基于对象进行操作
我们定义了对象变量并使用初始化函数给其作了初始化。接着我们就来考虑操作这一对象获取我们想要的数据。我们在驱动中已经将获取数据并转换为转换值的比例值,接下来我们使用这一驱动开发我们的应用实例。
/*NRF24L01数据通讯*/
void NRF24L01DataExchange(void)
{uint8_t txDatas[32]={0xAA};uint8_t rxDatas[32]={0x00};NRF24L01TransmitPacket(&nrf,txDatas);HAL_Delay(1);NRF24L01ReceivePacket(&nrf,rxDatas);
}
4、应用总结
我们已经设计并实现了nRF24L01无线通讯模块的驱动程序,并且在次驱动程序的基础上开发了简单的测试应用。经测试,这一驱动的设计基本上是正确的。
在使用驱动时需注意,采用SPI接口的器件需要考虑片选操作的问题。如果片选信号是通过硬件电路来实现的,我们在初始化时给其传递NULL值。如果是软件操作片选则传递我们编写的片选操作函数。
在使用驱动时,驱动中修改接收和发送模式时采用的是直接写入数值。其他的寄存器配置也基本都是直接写入数值,如果需要修改则需要在源码中修改。事实上,需要经常修改的可能性并不大,这也是我们写固定值的原因。另外,驱动中配置的是CRC-16校验,如果需要修改也是在源码中修改数值。
欢迎关注: