1)实验平台:正点原子stm32f103战舰开发板V4
2)平台购买地址:https://detail.tmall.com/item.htm?id=609294757420
3)全套实验源码+手册+视频下载地址: http://www.openedv.com/thread-340252-1-1.html#
本章,我们将介绍如何使用2.4G无线模块NRF24L01实现无线通信。将使用两块STM32开发板,一块用于发送,一块用于接收,从而实现无线数据传输,并把数据显示在LCD上。本章分为如下几个小节:
44.1 NRF24L01无线模块介绍
44.2 硬件设计
44.3 程序设计
44.4 下载验证
44.1 NRF24L01无线模块介绍
44.1.1 NRF24L01简介
NRF24L01无线模块,采用的芯片是NRF24L01+。该芯片是由NORDIC公司生产,并且集成NORDIC自家的Enhance ShortBurst协议,主要特点如下:
1)2.4G全球开放的ISM频段,免许可证使用
2)最高工作速率2Mbps,高效的GFSK调制,抗干扰能力强
3)126个可选的频道,满足多点通信和调频通信的需要
4)6个数据通道可支持点对多点的通信地址控制
5)低工作电压(1.9~3.6V)
6)硬件CRC和自动处理字头
7)可设置自动应答,确保数据可靠传输
由于高速信号是由芯片内部的射频协议处理后进行无线高速通信,对MCU的时钟频率要求不高,只需要对NRF24L01某些寄存器进行配置即可。芯片与外部MCU是通过SPI通信接口进行数据通信,并且最大的SPI速度可达10MHz。
这个芯片是NRF24L01的升级版。相比NRF24L01,升级版支持250k,1M,2M三种传输速率;支持更多种功率配置,根据不同应用有效节省功耗;稳定性及可靠性更高。
该模块的外形和引脚图如图44.1.1.1所示:
图44.1.1.1 NRF24L01无线模块外形和引脚图
模块VCC脚的电压范围为1.9~3.6V,建议不要超过3.6V,否则可能烧坏模块,一般用3.3V电压比较合适。除了VCC和GND脚,其他引脚都可以和5V单片机的IO口直连,正是因为其兼容5V单片机的IO,所以使用上具有很大优势。
具体引脚介绍如表44.1.1.1所示。
模块引脚 GND VCC CE CSN SCK MOSI MISO IRQ
功能说明 地线 3.3V电源线 模式控制线 片选 时钟 数据输出 数据输入 中断
表44.1.1.1 引脚介绍表
引脚部分主要分为电源相关的VCC和GND,SPI通信接口相关的CSN/SCK/MOSI/MISO,模式选择相关的CE,中断相关的IRQ。CE引脚会与CONFIG寄存器共同控制NRF24L01进入某个工作模式。IRQ引脚会在寄存器的配置下生效,当收到数据、成功发送数据或达到最大重发次数时,IRQ引脚会变为低电平。
由于在前面的SPI实验章节已经对SPI通信协议进行了详细的讲解,这里就不做展开。后面代码部分我们再进行分析NRF24L01使用到SPI的配置。
NRF24L01的Enhance ShockBurstTM模式具体表现在自动应答和重发机制,发送端要求接收端在接收到数据后要有应答信号,便于发送端检测有无数据丢失,一旦有数据丢失,则通过重发功能将丢失的数据恢复,这个过程无需MCU。Enhance ShockBurstTM模式可以通过EN_AA寄存器进行配置。
接下来看一下Enhanced ShockBurstTM模式下NRF24L01通信图,如图44.1.1.2所示:
图44.1.1.2 NRF24L01通信图
这里我们抽离PTX6和PRX出来,分析一下通信过程。
PTX6作为发送端,它就需要设置发送地址,可以看到TX_ADDR为0x7878787878,PRX作为接收端,它使能接收通道0并设置接收通道0接收地址0x7878787878。通信时,发送端发送数据接收端接收到数据并记录TX地址接收端以TX地址为目的地址发送应答信号发送端会以通道0接收应答信号。
NRF24L01规定:发送端中的数据通道0是用来接收接收端发送的应答信号,所以数据通道0的接收地址要与发送地址要相同才能确保收到正确的应答信号,这里十分重要,必须要在相关寄存器中配置正确。
44.1.2 NRF24L01工作模式介绍
NRF24L01作为无线通信模块,功耗问题十分重要,有数据发送与空闲状态下能耗肯定是需要调整,所以设计者给芯片设计了多种工作模块,如表44.1.2.1所示:
NRF24L01工作模式是由CE引脚和CONFIG寄存器的PWR_UP位和PRIM_RX位共同控制。CE引脚在前面也说到是模式控制线,而PWR_UP位是上电位,PRIM_RX位可以理解为配置身份位(TX or RX)。可以看到发送模式有两种,待机模式也有两种,功耗上各不相同,没有标红的发送模式和待机模式I是官方推荐使用,更加节能,但是本实验用到的模式就是上表中标红色部分,因为标红的的模式使用起来更加方便。单看发送模式,使用官方推荐的发送模式,你要发送三级TX_FIFO数据需要产生三个边沿信号(CE从高电平变为低电平)。而我们使用的发送模式,从CE引脚的操作上看,只需要拉高,就可以把所有TX_FIFO里的数据发送完成。
NRF24L01的发送和接收都有三级FIFO,每一级FIFO就有32个字节。发送和接收都是对FIFO进行操作,并且最大操作的数据量就是一级FIFO即32字节。发送时,只需要把数据存进TX_FIFO并按照发送模式下的操作(参考NRF24L01工作模式表中的发送模式)即可让NRF24L01启动发射,这个发射过程就包括:无线系统上电,启动内部16MHz时钟,无线发送数据打包,高速发送数据。接收时,也是通过读取RX_FIFO里的内容。
44.1.3 NRF24L01寄存器
在这里简单介绍一下本实验用到的NRF24L01比较重要的寄存器。
配置寄存器(CONFIG)
寄存器地址0x01,复位值为0x80,用来配置NRF24L01工作状态以及中断相关,描述如图44.1.2.1所示:
图44.1.2.1 配置寄存器图
需要配置成发送模式,可以把该寄存器赋值为0x0E,如果配置成接收模式,可以把该寄存器赋值为0x0F。无论是发送模式还是接收模式,都使能16位CRC以及使能接收中断、发送中断和最大重发次数中断,这里发送端和接收端配置需要一致。
自动应答功能寄存器(EN_AA)
寄存器地址0x01,复位值为0x3F,用来设置通道0~5的自动应答功能,描述如图44.1.2.3所示:
图44.1.2.3 自动应答功能寄存器图
本实验,接收端是以数据通道0作为接收通道,并且前面也提及Enhanced ShockBurstTM模式的自动应答流程,接收端接收到数据后,需要回复应答信号,通过该寄存器ENAA_P0置1即可实现。另外,使能自动应答也相当于配置成Enhanced模式,所以发送端也需要进行自动应答允许。
接收地址允许寄存器(EN_RXADDR)
寄存器地址0x02,复位值为0x03,用于使能接收通道0~5,描述如图44.1.2.4所示:
前面也说到接收端使用的是通道0进行接收数据,所以ERX_P0需要置1处理。同样的,发送端也需要使能数据通道0来接收应答信号。
地址宽度设置寄存器(SETUP_AW)
寄存器地址0x03,复位值为0x03,对接收/发送地址宽度设置位,描述如图44.1.2.5所示:
图44.1.2.5 地址宽度设置寄存器图
本实验中,无论是发送地址还是接收地址都是使用5字节,也就是默认设置便是使用5字节宽度的地址。
自动重发配置寄存器(SETUP_RETR)
寄存器地址0x04,复位值为0x00,对发送端的自动重发数值和延时进行设置,描述如图44.1.2.6所示:
图44.1.2.6 自动重发配置寄存器图
本实验中,直接对该寄存器写入0x1A,即自动重发间隔时间为586us,最大自动重发次数为10次。在使能了MAX_RT中断时,连续重发10次还是发送失败的时候,IRQ中断引脚就会拉低。
射频频率设置寄存器(RF_CH)
寄存器地址0x05,复位值为0x05,对NRF24L01的频段进行设置,描述如图44.1.2.7所示:
参数 位 描述
Reserved 7 保留位
RF_CH 6:0 0 ~ 125,设置NRF24L01的射频频率,接收端和发送端需一致
图44.1.2.7 射频频率设置寄存器图
频率计算公式:2400 + RF_CH(MHz)
本实验中,直接对该寄存器写入40即射频频率为2440MHz。通信双方该寄存器必须配置一样才能通信成功。
发射参数设置寄存器(RF_SETUP)
寄存器地址0x06,复位值为0x0E,对NRF24L01的发射功率、无线速率进行设置,描述如图44.1.2.8所示:
图44.1.2.8 发射参数设置寄存器图
本实验中,直接对该寄存器写入0x0F即射频输出功率为0dBm增益,传输速率为2MHz。发送端和接收端该寄存器的配置需一样。功率越小耗电越少,同等条件下,传输距离越小,这里我们设置射频部分功耗为最大,当然大家可以根据实际应用而选择对应的功率配置。
状态寄存器(STATUS)
地址0x07,复位值为0x0E,反应NRF24L01当前工作状态,描述如图44.1.2.2所示:
参数 位 描述
Reserved 7 保留位
RX_DR 6 接收数据标记,收到数据后置1。写1清除中断
TX_DS 5 数据发送完成标记。工作在自动应答模式,必须收到ACK才会置1。写1清除中断。
MAX_RT 4 达到最大重发次数标记。写1清除中断(如果MAX_RX中断产生则必须清除后系统才能进行通信)
RX_P_NO 3:1 接收数据通道:
000~101数据通道号 110未使用 111 RX_FIFO为空
TX_FULL 0 TX_FIFO寄存器满标记(1:满 0:未满)
图44.1.2.2 状态寄存器图
该寄存器作为查询作用,作为发送端,发送完数据后,可以查询一下TX_DS位状态便知是否成功发送数据,发送数据异常时,也可以通过查询MAX_RT位状态获知是否达到最大重发次数。作为接收端,就可以通过查询RX_OK位状态获知是否接收到数据。我们查询相关位后都需要将该位置1清除中断。
此外,我们还用到设置接收通道0地址寄存器RX_ADDR_P0(0x0A)和发送地址设置寄存器TX_ADDR(0x10)以及接收通道0有效数据看度设置寄存器RX_PW_P0(0x11),由于这三个寄存器比较简单,所以这里就不列出来了。
44.2 硬件设计
图44.2.1 无线模块与STM32的接口
NRF24L01使用的是SPI2,与NOR FLASH共用一个SPI接口,所以如果同时使用这些设备的时候,必须分时复用。为了防止其他器件对NRF24L01的通信造成干扰,最好可以把NOR FLASH的片选信号引脚拉高。
由于无线通信实验是双向的,所以至少要有两个模块同时能工作,这里我们使用2套开发板来向大家演示。
44.3 程序设计
NRF24L01配置步骤
1)SPI参数初始化(工作模式、数据时钟极性、时钟相位等)。
HAL库通过调用SPI初始化函数HAL_SPI_Init完成对SPI参数初始化,详见例程源码。
注意:该函数会调用:HAL_SPI_MspInit函数来完成对SPI底层的初始化,包括:SPI及GPIO时钟使能、GPIO模式设置等。
2)使能SPI时钟和配置相关引脚的复用功能以及NRF24L01的其他相关管脚。
本实验用到SPI2,使用PB13、PB14和PB15作为SPI_SCK、SPI_MISO和SPI_MOSI,以及NRF24L01的CE、CSN和IRQ分别对应PG8,PG7和PG6,因此需要先使能SPI2、GPIOB和GPIOG时钟。参考代码如下:
__HAL_RCC_SPI2_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();
__HAL_RCC_GPIOG_CLK_ENABLE();
IO口复用功能是通过函数HAL_GPIO_Init来配置的。
3)使能SPI
通过__HAL_SPI_ENABLE函数使能SPI,便可进行数据传输。
4)SPI传输数据
通过HAL_SPI_Transmit函数进行发送数据。
通过HAL_SPI_Receive函数进行接收数据。
也可以通过HAL_SPI_TransmitReceive函数进行发送与接收操作。
5)编写NRF24L01的读写函数
基于SPI的读写函数的基础上,编写NRF24L01的读写函数
6)编写NRF24L01接收模式与发送模式函数
通过查看寄存器,编写配置NRF24L01接收和发送模式的函数。
44.3.1 程序流程图
图44.3.1.1无线通信实验程序流程图
44.3.2 程序解析
本实验中,使用到的SPI配置与SPI实验章节差异不大,所以这里不作展开了,大家可以先回顾一下SPI实验的内容再来学习。
/* NRF24L01 操作引脚 定义(不包含SPI_SCK/MISO/MISO等三根线) */
#define NRF24L01_CE_GPIO_PORT GPIOG
#define NRF24L01_CE_GPIO_PIN GPIO_PIN_8
#define NRF24L01_CE_GPIO_CLK_ENABLE()
do{ __HAL_RCC_GPIOG_CLK_ENABLE();}while(0) /* PG口时钟使能 */
#define NRF24L01_CSN_GPIO_PORT GPIOG
#define NRF24L01_CSN_GPIO_PIN GPIO_PIN_7
#define NRF24L01_CSN_GPIO_CLK_ENABLE()
do{ __HAL_RCC_GPIOG_CLK_ENABLE();}while(0) /* PE口时钟使能 */
#define NRF24L01_IRQ_GPIO_PORT GPIOG
#define NRF24L01_IRQ_GPIO_PIN GPIO_PIN_6
#define NRF24L01_IRQ_GPIO_CLK_ENABLE()
do{ __HAL_RCC_GPIOG_CLK_ENABLE();}while(0) /* PG口时钟使能 */
/* 24L01操作线 */
#define NRF24L01_CE(x) do{ x ? \
HAL_GPIO_WritePin(NRF24L01_CE_GPIO_PORT,NRF24L01_CE_GPIO_PIN,GPIO_PIN_SET):\
HAL_GPIO_WritePin(NRF24L01_CE_GPIO_PORT,NRF24L01_CE_GPIO_PIN,GPIO_PIN_RESET);\
}while(0) /* 24L01模式选择信号 */
#define NRF24L01_CSN(x) do{ x ? \
HAL_GPIO_WritePin(NRF24L01_CSN_GPIO_PORT,NRF24L01_CSN_GPIO_PIN,GPIO_PIN_SET):\
HAL_GPIO_WritePin(NRF24L01_CSN_GPIO_PORT,
NRF24L01_CSN_GPIO_PIN,GPIO_PIN_RESET);\
}while(0) /* 24L01片选信号 */
#define NRF24L01_IRQ HAL_GPIO_ReadPin(NRF24L01_IRQ_GPIO_PORT, \
NRF24L01_IRQ_GPIO_PIN) /* IRQ主机数据输入 */
以上除了有NRF24L01的引脚定义及引脚操作函数,此外还有一些NRF24L01寄存器操作命令以及其寄存器地址,由于篇幅太大,所以这里就不列出来了,大家可以去看一下工程文件。
下面看一下NRF24L01的初始化函数,其定义如下:
/**
* @brief 初始化24L01的IO口
* @note 将SPI2模式改成SCK空闲低电平,及SPI 模式0
* @param 无
* @retval 无
*/
void nrf24l01_init(void)
{
GPIO_InitTypeDef gpio_init_struct;
NRF24L01_CE_GPIO_CLK_ENABLE(); /* CE脚时钟使能 */
NRF24L01_CSN_GPIO_CLK_ENABLE(); /* CSN脚时钟使能 */
NRF24L01_IRQ_GPIO_CLK_ENABLE(); /* IRQ脚时钟使能 */
gpio_init_struct.Pin = NRF24L01_CE_GPIO_PIN;
gpio_init_struct.Mode = GPIO_MODE_OUTPUT_PP; /* 推挽输出 */
gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速 */
HAL_GPIO_Init(NRF24L01_CE_GPIO_PORT, &gpio_init_struct); /* 初始化CE引脚 */
gpio_init_struct.Pin = NRF24L01_CSN_GPIO_PIN;
HAL_GPIO_Init(NRF24L01_CSN_GPIO_PORT, &gpio_init_struct); /* 初始化CS引脚 */
gpio_init_struct.Pin = NRF24L01_IRQ_GPIO_PIN;
gpio_init_struct.Mode = GPIO_MODE_INPUT; /* 输入 */
gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */
gpio_init_struct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速 */
HAL_GPIO_Init(NRF24L01_IRQ_GPIO_PORT, &gpio_init_struct); /* 初始化CE引脚 */
spi2_init(); /* 初始化SPI2 */
nrf24l01_spi_init(); /* 针对NRF的特点修改SPI的设置 */
NRF24L01_CE(0); /* 使能24L01 */
NRF24L01_CSN(1); /* SPI片选取消 */
}
/**
* @brief 针对NRF24L01修改SPI2驱动
* @param 无
* @retval 无
*/
void nrf24l01_spi_init(void)
{
__HAL_SPI_DISABLE(&g_spi2_handler); /* 先关闭SPI2 */
/* 串行同步时钟的空闲状态为低电平 */
g_spi2_handler.Init.CLKPolarity = SPI_POLARITY_LOW;
/* 串行同步时钟的第1个跳变沿(上升或下降)数据被采样 */
g_spi2_handler.Init.CLKPhase = SPI_PHASE_1EDGE;
HAL_SPI_Init(&g_spi2_handler);
__HAL_SPI_ENABLE(&g_spi2_handler); /* 使能SPI2 */
}
在初始化函数中,我们主要对该模块用到的管脚进行配置以及从初始化工作以及需要调用spi.c文件中的spi_init函数对SPI1的引脚进行初始化。现在让我们看一下 NRF24L01的工作时序图见下图44.3.2.1所示。
图44.3.2.1 NRF24L01的工作时序图
大家可以比对一下前面章节的SPI工作时序图,符合工作模式1的时序,即在奇数边沿上升沿进行数据的采集。所以我们调用nrf24l01_spi_init函数针对NRF的特点修改SPI的设置。该函数就是将SPI的工作模式配置成串行同步时钟空闲状态为低电平,在奇数边沿数据被采集,也就是前面SPI实验章节中SPI的工作模式0。现在看看工作时序图的某些标号意义,Cn代表指令位,Sn代表状态寄存器位,Dn代表数据位。
下面介绍一下NRF24L01的读写函数,其代码如下:
/**
* @brief NRF24L01写寄存器
* @param reg : 寄存器地址
* @param value : 写入寄存器的值
* @retval 状态寄存器值
*/
static uint8_t nrf24l01_write_reg(uint8_t reg, uint8_t value)
{
uint8_t status;
NRF24L01_CSN(0); /* 使能SPI传输 */
status = spi2_read_write_byte(reg); /* 发送寄存器号 */
spi2_read_write_byte(value); /* 写入寄存器的值 */
NRF24L01_CSN(1); /* 禁止SPI传输 */
return status; /* 返回状态值 */
}
/**
* @brief NRF24L01读寄存器
* @param reg : 寄存器地址
* @retval 读取到的寄存器值;
*/
static uint8_t nrf24l01_read_reg(uint8_t reg)
{
uint8_t reg_val;
NRF24L01_CSN(0); /* 使能SPI传输 */
spi2_read_write_byte(reg); /* 发送寄存器号 */
reg_val = spi2_read_write_byte(0XFF); /* 读取寄存器内容 */
NRF24L01_CSN(1); /* 禁止SPI传输 */
return reg_val; /* 返回状态值 */
}
/**
* @brief 在指定位置读出指定长度的数据
* @param reg : 寄存器地址
* @param pbuf : 数据指针
* @param len : 数据长度
* @retval 状态寄存器值
*/
static uint8_t nrf24l01_read_buf(uint8_t reg, uint8_t *pbuf, uint8_t len)
{
uint8_t status, i;
NRF24L01_CSN(0); /* 使能SPI传输 */
status = spi2_read_write_byte(reg); /* 发送寄存器值(位置),并读取状态值 */
for (i = 0; i < len; i++)
{
pbuf[i] = spi2_read_write_byte(0XFF); /* 读出数据 */
}
NRF24L01_CSN(1); /* 关闭SPI传输 */
return status; /* 返回读到的状态值 */
}
/**
* @brief 在指定位置写指定长度的数据
* @param reg : 寄存器地址
* @param pbuf : 数据指针
* @param len : 数据长度
* @retval 状态寄存器值
*/
static uint8_t nrf24l01_write_buf(uint8_t reg, uint8_t *pbuf, uint8_t len)
{
uint8_t status, i;
NRF24L01_CSN(0); /* 使能SPI传输 */
status = spi2_read_write_byte(reg);/* 发送寄存器值(位置),并读取状态值 */
for (i = 0; i < len; i++)
{
spi2_read_write_byte(*pbuf++); /* 写入数据 */
}
NRF24L01_CSN(1); /* 关闭SPI传输 */
return status; /* 返回读到的状态值 */
}
以上是NRF24L01的写寄存器函数和读寄存器函数,以及扩展的函数:在指定位置写入指定长度的数据函数和指定位置读取指定长度的数据函数。先讲一下NRF24L01读写寄存器函数实现的具体过程:
先拉低片选线→发送寄存器号→发送数据/接收数据→拉高片选线。
这里提及一下SPI的相关知识:SPI是通过移位寄存器进行数据传输,所以发一字节数据就会收到一个字节数据。那么发数据就可以直接发送数据,接收数据只需要发送0xFF,寄存器会返回要读取的数据。
在指定位置写入指定长度的数据函数和在指定位置读取指定长度的数据函数的实现方式也是通过调用SPI的读写一字节函数实现,这里跟写寄存器和读寄存器函数的实现差不多,这里就不做展开了。不懂的话,也可以回顾一下SPI实验章节。
下面看一下这两种模式的初始化过程:
Rx模式初始化过程:
1)写Rx节点的地址
2)使能通道x自动应答
3)使能通道x接收地址
4)设置通信频率
5)选择通道x的有效数据宽度
6)配置发射参数(发射功率、无线速率)
7)配置NRF24L01的基本参数以及工作模式
其代码如下:
/**
* @brief NRF24L01进入接收模式
* @note 设置RX地址,写RX数据宽度,选择RF频道,波特率和LNA HCURR
* 当CE变高后,即进入RX模式,并可以接收数据了
* @param 无
* @retval 无
*/
void nrf24l01_rx_mode(void)
{
NRF24L01_CE(0);
nrf24l01_write_buf(NRF_WRITE_REG + RX_ADDR_P0, (uint8_t *)RX_ADDRESS,
RX_ADR_WIDTH); /* 写RX节点地址 */
nrf24l01_write_reg(NRF_WRITE_REG + EN_AA, 0x01); /* 使能通道0的自动应答 */
nrf24l01_write_reg(NRF_WRITE_REG + EN_RXADDR, 0x01); /* 使能通道0的接收地址 */
nrf24l01_write_reg(NRF_WRITE_REG + RF_CH, 40); /* 设置RF通信频率 */
/* 选择通道0的有效数据宽度 */
nrf24l01_write_reg(NRF_WRITE_REG + RX_PW_P0, RX_PLOAD_WIDTH);
/* 设置TX发射参数,0db增益,2Mbps */
nrf24l01_write_reg(NRF_WRITE_REG + RF_SETUP, 0x0f);
/* 配置基本工作模式的参数;PWR_UP,EN_CRC,16BIT_CRC,接收模式 */
nrf24l01_write_reg(NRF_WRITE_REG + CONFIG, 0x0f);
NRF24L01_CE(1); /* CE为高,进入接收模式 */
}
Tx模式初始化过程:
1)写Tx节点的地址
2)写Rx节点的地址,主要为了使能硬件的自动应答
3)使能通道x的自动应答
4)使能通道x接收地址
5)配置自动重发次数
6)配置通信频率
7)选择通道x的有效数据宽度
8)配置发射参数(发射功率、无线速率)
9)配置NRF24L01的基本参数以及切换工作模式
其代码如下:
/**
* @brief NRF24L01进入发送模式
* @note 设置TX地址,写TX数据宽度,设置RX自动应答地址,填充TX发送数据,选择RF频道,
波特率和PWR_UP,CRC使能
* 当CE变高后,即进入TX模式,并可以发送数据了, CE为高大于10us,则启动发送.
* @param 无
* @retval 无
*/
void nrf24l01_tx_mode(void)
{
NRF24L01_CE(0);
nrf24l01_write_buf(NRF_WRITE_REG + TX_ADDR, (uint8_t *)TX_ADDRESS,
TX_ADR_WIDTH); /* 写TX节点地址 */
nrf24l01_write_buf(NRF_WRITE_REG + RX_ADDR_P0, (uint8_t *)RX_ADDRESS,
RX_ADR_WIDTH); /* 设置RX节点地址,主要为了使能ACK */
nrf24l01_write_reg(NRF_WRITE_REG + EN_AA, 0x01); /* 使能通道0的自动应答 */
nrf24l01_write_reg(NRF_WRITE_REG + EN_RXADDR, 0x01); /* 使能通道0的接收地址 */
/* 设置自动重发间隔时间:500us + 86us;最大自动重发次数:10次 */
nrf24l01_write_reg(NRF_WRITE_REG + SETUP_RETR, 0x1a);
nrf24l01_write_reg(NRF_WRITE_REG + RF_CH, 40); /* 设置RF通道为40 */
/* 设置TX发射参数,0db增益,2Mbps,低噪声增益开启 */
nrf24l01_write_reg(NRF_WRITE_REG + RF_SETUP, 0x0f);
/* 配置基本工作模式的参数;PWR_UP,EN_CRC,16BIT_CRC,接收模式,开启所有中断 */
nrf24l01_write_reg(NRF_WRITE_REG + CONFIG, 0x0e);
NRF24L01_CE(1); /* CE为高,10us后启动发送 */
}
以上就是两种模式的配置,看过完整代码的,会发现TX_ADDR和RX_ADDR两个地址是一样的,跟前面说法一致,我们必须保持地址的匹配才能通信成功。以上代码中的发送函数都有一个特点,并不是单纯发送寄存器地址,而是操作指令+寄存器地址,这一点需要记得。NRF24L01的操作指令也有好几个,它是配合寄存器完成特定的操作,其定义如下:
/* NRF24L01寄存器操作命令 */
#define NRF_READ_REG 0x00 /* 读配置寄存器,低5位为寄存器地址 */
#define NRF_WRITE_REG 0x20 /* 写配置寄存器,低5位为寄存器地址 */
#define RD_RX_PLOAD 0x61 /* 读RX有效数据,1~32字节 */
#define WR_TX_PLOAD 0xA0 /* 写TX有效数据,1~32字节 */
#define FLUSH_TX 0xE1 /* 清除TX FIFO寄存器.发射模式下用 */
#define FLUSH_RX 0xE2 /* 清除RX FIFO寄存器.接收模式下用 */
#define REUSE_TX_PL 0xE3 /* 重新使用上一包数据,CE为高,数据包被不断发送. */
#define NOP 0xFF /* 空操作,可以用来读状态寄存器 */
经过上面的发送或者接收模式初始化步骤后,NRF24L01就可以准备启动发送数据或者等待接收数据了。
下面来看一下启动NRF24L01发送一次数据的函数,其定义如下:
/**
* @brief 启动NRF24L01发送一次数据(数据长度 = TX_PLOAD_WIDTH)
* @param ptxbuf : 待发送数据首地址
* @retval 发送完成状态
* @arg 0 : 发送成功
* @arg 1 : 达到最大发送次数,失败
* @arg 0XFF : 其他错误
*/
uint8_t nrf24l01_tx_packet(uint8_t *ptxbuf)
{
uint8_t sta;
uint8_t rval = 0XFF;
NRF24L01_CE(0);
/* 写数据到TX BUF TX_PLOAD_WIDTH个字节 */
nrf24l01_write_buf(WR_TX_PLOAD, ptxbuf, TX_PLOAD_WIDTH);
NRF24L01_CE(1); /* 启动发送 */
while (NRF24L01_IRQ != 0); /* 等待发送完成 */
sta = nrf24l01_read_reg(STATUS); /* 读取状态寄存器的值 */
nrf24l01_write_reg(NRF_WRITE_REG + STATUS, sta);/* 清除TX_DS/MAX_RT中断标志*/
if (sta & MAX_TX) /* 达到最大重发次数 */
{
nrf24l01_write_reg(FLUSH_TX, 0xff); /* 清除TX FIFO寄存器 */
rval = 1;
}
if (sta & TX_OK) /* 发送完成 */
{
rval = 0; /* 标记发送成功 */
}
return rval; /* 返回结果 */
}
在这里启动发送数据函数中,具体实现很简单,拉低片选信号→向发送数据寄存器写入数据→拉高片选信号。这里说明一下,在发送完寄存器号后都会返回一个status值,返回的这个值就是前面介绍的STATUS寄存器的内容。在这个基础上就可以知道数据是否发送完成以及现在的状态。
然后介绍一下NRF24L01接收一次数据函数,其定义如下:
/**
* @brief 启动NRF24L01接收一次数据(数据长度 = RX_PLOAD_WIDTH)
* @param prxbuf : 接收数据缓冲区首地址
* @retval 接收完成状态
* @arg 0 : 接收成功
* @arg 1 : 失败
*/
uint8_t nrf24l01_rx_packet(uint8_t *prxbuf)
{
uint8_t sta;
uint8_t rval = 1;
sta = nrf24l01_read_reg(STATUS); /* 读取状态寄存器的值 */
nrf24l01_write_reg(NRF_WRITE_REG + STATUS, sta);/*清除TX_DS或MAX_RT中断标志*/
if (sta & RX_OK) /* 接收到数据 */
{
nrf24l01_read_buf(RD_RX_PLOAD, prxbuf, RX_PLOAD_WIDTH); /* 读取数据 */
nrf24l01_write_reg(FLUSH_RX, 0xff); /* 清除RX FIFO寄存器 */
rval = 0; /* 标记接收完成 */
}
return rval; /* 返回结果 */
}
在启动接收的过程中,首先需要判断当前NRF24L01的状态,往后才是真正的读取数据,清除接收寄存器的缓冲,完成数据的接收。这里需要注意的是我们通过RX_ PLOAD_WIDTH和TX_PLOAD_WIDTH决定了接收和发送的数据宽度,这也决定每次发送和接收的有效字节数。NRF24L01每次最多传输32个字节,再多的字节传输则需要多次传输。通信双方的发送和接收数据宽度必须一致才能正常通信。
2. main.c代码
在main.c里编写如下代码:
int main(void)
{
uint8_t key, mode;
uint16_t t = 0;
uint8_t tmp_buf[33];
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
delay_init(72); /* 延时初始化 */
usart_init(115200); /* 串口初始化为115200 */
led_init(); /* 初始化LED */
lcd_init(); /* 初始化LCD */
key_init(); /* 初始化按键 */
nrf24l01_init(); /* 初始化NRF24L01 */
lcd_show_string(30, 50, 200, 16, 16, "STM32", RED);
lcd_show_string(30, 70, 200, 16, 16, "NRF24L01 TEST", RED);
lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
while (nrf24l01_check()) /* 检查NRF24L01是否在线 */
{
lcd_show_string(30, 110, 200, 16, 16, "NRF24L01 Error", RED);
delay_ms(200);
lcd_fill(30, 110, 239, 130 + 16, WHITE);
delay_ms(200);
}
lcd_show_string(30, 110, 200, 16, 16, "NRF24L01 OK", RED);
while (1) /* 提醒用户选择模式 */
{
key = key_scan(0);
if (key == KEY0_PRES)
{
mode = 0; /* 接收模式 */
break;
}
else if (key == KEY1_PRES)
{
mode = 1; /* 发送模式 */
break;
}
t++;
if (t == 100) /* 显示提示信息 */
{
lcd_show_string(10,130,230,16,16,"KEY0:RX_Mode KEY1:TX_Mode", RED);
}
if (t == 200) /* 关闭提示信息 */
{
lcd_fill(10, 130, 230, 150 + 16, WHITE);
t = 0;
}
delay_ms(5);
}
lcd_fill(10, 130, 240, 166, WHITE); /* 清空上面的显示 */
if (mode == 0) /* RX模式 */
{
lcd_show_string(30, 130, 200, 16, 16, "NRF24L01 RX_Mode", BLUE);
lcd_show_string(30, 150, 200, 16, 16, "Received DATA:", BLUE);
nrf24l01_rx_mode(); /* 进入RX模式 */
while (1)
{
if (nrf24l01_rx_packet(tmp_buf) == 0) /* 一旦接收到信息,则显示出来. */
{
tmp_buf[32] = 0; /* 加入字符串结束符 */
lcd_show_string(0,170,lcddev.width-1,32,16,(char *)tmp_buf, BLUE);
}
else
delay_us(100);
t++;
if (t == 10000) /* 大约1s钟改变一次状态 */
{
t = 0;
LED0_TOGGLE();
}
}
}
else /* TX模式 */
{
lcd_show_string(30, 130, 200, 16, 16, "NRF24L01 TX_Mode", BLUE);
nrf24l01_tx_mode(); /* 进入TX模式 */
mode = ' '; /* 从空格键开始发送 */
while (1)
{
if (nrf24l01_tx_packet(tmp_buf) == 0) /* 发送成功 */
{
lcd_show_string(30, 150, 239, 32, 16, "Sended DATA:", BLUE);
lcd_show_string(0,170,lcddev.width-1,32,16,(char *)tmp_buf, BLUE);
key = mode;
for (t = 0; t < 32; t++)
{
key++;
if (key > ('~'))
key = ' ';
tmp_buf[t] = key;
}
mode++;
if (mode > '~')
mode = ' ';
tmp_buf[32] = 0; /* 加入结束符 */
}
else
{
lcd_fill(0, 150, lcddev.width, 170 + 16 * 3, WHITE); /* 清空显示 */
lcd_show_string(30,150,lcddev.width-1,32,16,"Send Failed ", BLUE);
}
LED0_TOGGLE();
delay_ms(200);
}
}
}
程序运行时先通过nrf24l01_cheak函数检测NRF24L01是否存在,如果存在,则让用户选择发送模式还是接收模式,在确定模式之后,设置NRF24L01的工作模式,然后执行相对应的数据发送/接收处理。
44.4 下载验证
将程序下载到开发板后,可以看到LCD显示的内容如图44.4.1所示:
图44.4.1 选择工作模式图
通过KEY0和KEY1来选择NRF24L01模块所要进入的工作模式,我们两个开发板一个选择发送模式,一个选择接收模式就可以了。设置好的通信界面如图44.4.2和图44.4.3所示:
图44.4.3 开发板B接收数据
图44.4.2来自于开发板A,工作在发送模式。图44.4.3来自于开发板B,工作在接收模式,A发送,B接收。发送和接收图片的数据不一样,是因为我们拍照的时间不一样导致的。大家看到收发数据是一致,那就说明实验成功。