现在无线在我们的生活中无处不在。而我们开发的物联网产品也大量使用无线通讯。在这一篇文章中,我们将讨论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校验,如果需要修改也是在源码中修改数值。

欢迎关注:

外设驱动库开发笔记26:nRF24L01无线通讯驱动相关推荐

  1. 外设驱动库开发笔记54:外设库驱动设计改进的思考

      不知不觉中我们已经发布了五十多篇外设驱动的文章.前段时间有一位网友提出了一些非常中肯的建议,这也让我们开始考虑怎么优化驱动程序设计的问题.在这一篇中我们将来讨论这一问题. 1.问题分析   首先我 ...

  2. 外设驱动库开发笔记34:OLED显示屏驱动

      现在OLED显示屏在嵌入式系统中应用的越来越多.对于一些显示信息不太复杂,以显示信息为主的需求,我们一般会选择OLED显示屏.在这一篇中,我们将讨论OLED显示屏驱动的设计与实现. 1.功能概述 ...

  3. 外设驱动库开发笔记23:AT24Cxx外部存储器驱动

    在我们的应用开发过程中,经常会使用到外部的EEPROM外部存储器来保存一些参数和配置数据等.而比较常用的就是AT24Cxx系列产品,这一节我们来开发用于操作AT24Cxx系列产品的驱动. 1.功能概述 ...

  4. 外设驱动库开发笔记18:MS5837压力变送器驱动

    绝对压力的检测是常见的需求.在我们的系统中也常常会遇到.而MS5837压力传感器也是我们进场会采用的方案.在这篇里我们将讨论并实现MS5837压力传感器的驱动. 1.功能概述 MS5837压力传感器是 ...

  5. 外设驱动库开发笔记16:MS5536C压力变送器驱动

    压力检测也是经常会遇到的需求,比如环境压力或者低压气体等都会用到压力检测.这类检测压力都比较低,一般不会超过大气压,有时甚至是负压.这一篇我们要讨论的MS5536C就属于这类器件.接下来我们将设计并实 ...

  6. 外设驱动库开发笔记14:DS18B20温度变送器驱动

    在一时候我们需要相对简单的检测温度信号,而DS18B20就是一款功能和应用都相对简单的温度传感器,通过单线就可以实现检测温度信号的需求.这一篇我们就来实现操作DS18B20获取温度数据的驱动. 1.功 ...

  7. 外设驱动库开发笔记33:LCD1602液晶显示屏驱动

      LCD1602是一种工业字符型液晶,能够同时显示16x02即32个字符.LCD1602液晶显示的原理是利用液晶的物理特性,通过电压对其显示区域进行控制,即可以显示出图形.在这一章我们就来讨论LCD ...

  8. 外设驱动库开发笔记40:AT25xxx外部存储器驱动

      我们在前面开发过AT24CXX系列EEPROM存储器,它使用的是I2C接口.不过有时候我们也会使用SPI接口的EEPROM存储器.在这一篇我们将来讨论AT25XXX系列EEPROM存储器的驱动设计 ...

  9. 外设驱动库开发笔记17:MS5803压力变送器驱动

    很多时候我们需要检测被控对象的绝对压力,而且在我们的多款产品中也有这样的需求.当然检测绝对压力的传感器有很多,我们经常使用MS5803来实现压力检测.本篇中我们将设计并实现MS5803系列压力传感器的 ...

最新文章

  1. Asp.net中GridView使用详解(引)【转】
  2. grunt.config()_gruntjs api
  3. 深入浅出Mybatis系列(四)---配置详解之typeAliases别名(mybatis源码篇)
  4. sessionFactory.getCurrent()和sessionFactory.openSession()的区别
  5. PhpStorm 注册相关
  6. CSS3 Flex 弹性布局用法详解
  7. 关于Cococs中的CCActionEase(下)
  8. 哇撒!这几个SpringBoot前后端分离项目(附源码),star过千,快去收藏夹吃灰吧。。。...
  9. switch中使用枚举
  10. 抛硬币概率公式 计算机题,行测古典型概率问题:抛硬币
  11. 张恭庆院士:数学的意义(最全面的解释)
  12. 时空人工智能概念特点和核心能力
  13. 虚拟资源项目是什么?依靠虚拟资源项目月入万元能实现吗?
  14. Python科研绘图——介绍
  15. html编辑 手机浏览器,浏览器编辑HTML
  16. 国外Windows主机的特点
  17. 姓莫的女孩子叫什么名字好听
  18. 提示Microsoft office word 遇到问题需要关闭。还问是否发送错误报告。
  19. “碎片化时代”的灵动工作与生活
  20. 【运维项目】零基础小白也能学会的运维项目

热门文章

  1. Maven:导入Oracle的jar包时出现错误
  2. 图的知识点总结-数据结构
  3. Trie可持久化Trie
  4. 继承和多态二:虚析构函数
  5. VMware虚拟机软件
  6. 两台老机器,AMD K6-2和Intel C366
  7. 【剑指offer】面试题15:二进制中1的个数(Java)
  8. 正则表达式提取器_C++11新特性7 - 正则表达式
  9. 怎么把php的时区配置为本地,PHP本地时区设置
  10. Js拼接嵌套php代码,分享一个js文件中嵌套php会出错的问题