1)实验平台:正点原子MiniPro H750开发板 2)平台购买地址:https://detail.tmall.com/item.htm?id=677017430560 3)全套实验源码+手册+视频下载地址:http://www.openedv.com/thread-336836-1-1.html 4)对正点原子STM32感兴趣的同学可以加群讨论:879133275
第四十一章 无线通信实验
本章,我们将介绍如何使用2.4G无线模块NRF24L01实现无线通信。将使用两块STM32开发板,一块用于发送,一块用于接收,从而实现无线数据传输,并把数据显示在LCD上。本章分为如下几个小节: 41.1 SPI&NRF24L01无线模块介绍 41.2 硬件设计 41.3 程序设计 41.4 下载验证
41.1 SPI&NRF24L01无线模块介绍
41.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三种传输速率;支持更多种功率配置,根据不同应用有效节省功耗;稳定性及可靠性更高。 该模块的外形和引脚图如图41.1.1.1所示:
图41.1.1.1 NRF24L01无线模块外形和引脚图 模块VCC脚的电压范围为1.9~3.6V,建议不要超过3.6V,否则可能烧坏模块,一般用3.3V电压比较合适。除了VCC和GND脚,其他引脚都可以和5V单片机的IO口直连,正是因为其兼容5V单片机的IO,所以使用上具有很大优势。 具体引脚介绍如表41.1.1.1所示。 模块引脚 GND VCC CE CSN SCK MOSI MISO IRQ 功能说明 地线 3.3V电源线 使能端 片选 时钟 数据输出 数据输入 中断 表41.1.1.1 引脚介绍表 引脚部分主要分为电源相关的VCC和GND,SPI通信接口相关的CSN/SCK/MOSI/MISO,模式选择相关的CE,中断相关的IRQ。CE引脚会与CONFIG寄存器共同控制NRF24L01进入某个工作模式。IRQ引脚会在寄存器的配置下生效,当收到数据、成功发送数据或达到最大重发次数时,IRQ引脚会变为低电平。 SPI通信接口将会在41.1.4小节进行讲解。 NRF24L01的Enhance ShockBurstTM模式具体表现在自动应答和重发机制,发送端要求接收端在接收到数据后要有应答信号,便于发送端检测有无数据丢失,一旦有数据丢失,则通过重发功能将丢失的数据恢复,这个过程无需MCU。Enhance ShockBurstTM模式可以通过EN_AA寄存器进行配置。 接下来看一下Enhanced ShockBurstTM模式下NRF24L01通信图,如图41.1.1.2所示:
图41.1.1.2 NRF24L01通信图 这里我们抽离PTX6和PRX出来,分析一下通信过程。 PTX6作为发送端,它就需要设置发送地址,可以看到TX_ADDR为0x7878787878,PRX作为接收端,它使能接收通道0并设置接收通道0接收地址0x7878787878。通信时,发送端发送数据接收端接收到数据并记录TX地址接收端以TX地址为目的地址发送应答信号发送端会以通道0接收应答信号。 NRF24L01规定:发送端中的数据通道0是用来接收接收端发送的应答信号,所以数据通道0的接收地址要与发送地址要相同才能确保收到正确的应答信号,这里十分重要,必须要在相关寄存器中配置正确。 41.1.2 NRF24L01工作模式介绍 NRF24L01作为无线通信模块,功耗问题十分重要,有数据发送与空闲状态下能耗肯定是需要调整,所以设计者给芯片设计了多种工作模块,如表41.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里的内容。 41.1.3 NRF24L01寄存器 在这里简单介绍一下本实验用到的NRF24L01比较重要的寄存器。 ?配置寄存器(CONFIG) 寄存器地址0x01,复位值为0x80,用来配置NRF24L01工作状态以及中断相关,描述如图41.1.2.1所示:
图41.1.2.1 配置寄存器图 需要配置成发送模式,可以把该寄存器赋值为0x0E,如果配置成接收模式,可以把该寄存器赋值为0x0F。无论是发送模式还是接收模式,都使能16位CRC以及使能接收中断、发送中断和最大重发次数中断,这里发送端和接收端配置需要一致。 ?自动应答功能寄存器(EN_AA) 寄存器地址0x01,复位值为0x3F,用来设置通道0~5的自动应答功能,描述如图41.1.2.3所示:
图41.1.2.3 自动应答功能寄存器图 本实验,接收端是以数据通道0作为接收通道,并且前面也提及Enhanced ShockBurstTM模式的自动应答流程,接收端接收到数据后,需要回复应答信号,通过该寄存器ENAA_P0置1即可实现。另外,使能自动应答也相当于配置成Enhanced模式,所以发送端也需要进行自动应答允许。 ?接收地址允许寄存器(EN_RXADDR) 寄存器地址0x02,复位值为0x03,用于使能接收通道0~5,描述如图41.1.2.4所示:
图41.1.2.4 接收地址允许寄存器图 前面也说到接收端使用的是通道0进行接收数据,所以ERX_P0需要置1处理。同样的,发送端也需要使能数据通道0来接收应答信号。 ?地址宽度设置寄存器(SETUP_AW) 寄存器地址0x03,复位值为0x03,对接收/发送地址宽度设置位,描述如图41.1.2.5所示:
图41.1.2.5 地址宽度设置寄存器图 本实验中,无论是发送地址还是接收地址都是使用5字节,也就是默认设置便是使用5字节宽度的地址。 ?自动重发配置寄存器(SETUP_RETR) 寄存器地址0x04,复位值为0x00,对发送端的自动重发数值和延时进行设置,描述如图41.1.2.6所示: 参数 位 描述 ADR 7:4 自动重发延时: 0000~111186 us + 250 * (ARD + 1) us ARC 3:0 自动重发计数 0000~1111自动重发次数。0代表禁止 图41.1.2.6 自动重发配置寄存器图 本实验中,直接对该寄存器写入0x1A,即自动重发间隔时间为586us,最大自动重发次数为10次。在使能了MAX_RT中断时,连续重发10次还是发送失败的时候,IRQ中断引脚就会拉低。 ?射频频率设置寄存器(RF_CH) 寄存器地址0x05,复位值为0x05,对NRF24L01的频段进行设置,描述如图41.1.2.7所示:
图41.1.2.7 射频频率设置寄存器图 频率计算公式:2400 + RF_CH(MHz) 本实验中,直接对该寄存器写入40即射频频率为2440MHz。通信双方该寄存器必须配置一样才能通信成功。 ?发射参数设置寄存器(RF_SETUP) 寄存器地址0x06,复位值为0x0E,对NRF24L01的发射功率、无线速率进行设置,描述如图41.1.2.8所示:
图41.1.2.8 发射参数设置寄存器图 本实验中,直接对该寄存器写入0x0F即射频输出功率为0dBm增益,传输速率为2MHz。发送端和接收端该寄存器的配置需一样。功率越小耗电越少,同等条件下,传输距离越小,这里我们设置射频部分功耗为最大,当然大家可以根据实际应用而选择对应的功率配置。 ?状态寄存器(STATUS) 地址0x07,复位值为0x0E,反应NRF24L01当前工作状态,描述如图41.1.2.2所示:
图41.1.2.2 状态寄存器图 该寄存器作为查询作用,作为发送端,发送完数据后,可以查询一下TX_DS位状态便知是否成功发送数据,发送数据异常时,也可以通过查询MAX_RT位状态获知是否达到最大重发次数。作为接收端,就可以通过查询RX_OK位状态获知是否接收到数据。我们查询相关位后都需要将该位置1清除中断。 此外,我们还用到设置接收通道0地址寄存器RX_ADDR_P0(0x0A)和发送地址设置寄存器TX_ADDR(0x10)以及接收通道0有效数据看度设置寄存器RX_PW_P0(0x11),由于这三个寄存器比较简单,所以这里就不列出来了。 41.1.4 SPI接口简介 SPI 是英语Serial Peripheral interface的缩写,顾名思义就是串行外围设备接口。是Motorola首先在其MC68HCXX系列处理器上定义的。SPI接口主要应用在 EEPROM,FLASH,实时时钟,AD转换器,还有数字信号处理器和数字信号解码器之间。 SPI,是一种高速的,全双工,同步的通信总线,并且在芯片的管脚上只占用四根线,节约了芯片的管脚,同时为PCB的布局上节省空间,提供方便,正是出于这种简单易用的特性,现在越来越多的芯片集成了这种通信协议,STM32H750也有SPI接口,其框图如图41.1.3.1所示:
图41.1.3.1 STM32H750的SPI框图 图中我们挑出了4处和我们本章例程相关的位置进行重点介绍,其中①处是SPI的对外信号,一般由4根信号线组成(MCK是I2S用的,不属于SPI信号线): MISO(SDI): 主设备数据输入,从设备数据输出。 MOSI(SDO):主设备数据输出,从设备数据输入。 SCK(CK):时钟信号,由主设备产生。 SS(WS):从设备片选信号(也称CS),由主设备控制。 图中②处是SPI的时钟生成器,将来自spi_ker_ck的时钟进行分频输出,最终产生SCK信号,并驱动RX/TX移位寄存器接收/发送数据。SPI1/2/3的spi_ker_ck时通过RCC_D2CCIP1R寄存器的SPI123SEL[2:0]设置,默认为0,则spi_ker_ck来自pll1_q_ck,为200Mhz。 图中③处重点由:TX移位寄存器、RX移位寄存器等组成,分别用于发送数据和接收数据, TxFIFO用于发送数据,RxFIFO用于接收数据,我们可以设置FIFO大小为1,这样写入TXDR的数据就可以直接输出到TX移位寄存器,在SCK时钟的驱动下,输出到MOSI脚。同理,当FIFO大小为1时,RX移位寄存器接收到的数据,也会直接输出给RXDR寄存器。 图中④处是SPI的一堆控制寄存器,用于设置SPI的各种工作情况(或模式),该区域由spi_pclk时钟驱动。一般我们需要合理的设置这些寄存器,SPI才可以正常使用。 SPI总线有四种工作模式,如表41.1.3.1所示:
表41.1.3.1 SPI四种工作模式 其中CPOL和CPHA分别控制SCK的时钟极性和相位时钟,可以通过相关寄存器设置。 不同时钟/相位下的总线数据传输时序如图41.1.3.2所示:
图41.1.3.2 不同时钟相位下的总线传输时序(CPHA=0/1) SPI在驱动不同的器件时,必须注意其支持的工作模式,需要设置合适的SPI工作模式,才可以正常通信。 对于STM32H7来说,SPI的MSB和LSB是可以配置的,通过SPI_CFG2的LSBFIRST位进行控制,当该位为1时,表示LSB在前;当该位为0时,表示MSB在前; STM32H7的SPI功能很强大,SPI时钟最高可以到133Mhz,支持DMA,可以配置为SPI协议或者I2S协议(支持全双工I2S)。 本章,我们将使用STM32H750的SPI来驱动NRF24L01无线模块。这里对SPI我们只简单介绍一下SPI的使用,STM32H750的SPI详细介绍请参考《STM32H7xx考手册》第2180页,第50章。 SPI主模式配置步骤 1)配置相关引脚的复用功能,使能SPI2时钟。 我们要用SPI2,第一步就要使能SPI2的时钟,SPI2的时钟通过APB1LENR的第14位来设置。其次要设置SPI2的相关引脚为复用(AF5)输出,这样才会连接到SPI2上。这里我们使用的是PB13、14、15这3个(SCK.、MISO、MOSI,CS使用软件管理方式),所以设置这三个为复用IO,复用功能为AF5。 2)设置SPI2的波特率和数据格式。 这一步通过SPI2_CFG1来设置,SPI2的波特率(就是SCK的频率,最大133Mhz)通过SPI2_CFG1寄存器的MBR[2:0]来设置,可以设置为spi_ker_ck的2~256分频。而数据格式通过SPI2_CFG1寄存器的DSIZE[4:0]位设置,数据长度等于DSIZE[4:0]+1,比如,我们要设置为8位数据格式,则设置DSIZE[4:0]=7即可。 3)设置SPI2工作模式。 这一步通过SPI2_CFG2来设置,我们设置SPI2为主机模式,全双工,然后通过CPOL和CPHA位来设置SCK时钟极性及采样方式。并设置SPI2的SS(即CS)控制方式为软件控制。 4)使能SPI2。 通过SPI2_CR1的bit0来设置,以启动SPI2,在启动之后,我们就可以开始SPI通讯了。 41.2 硬件设计
- 例程功能
开机的时候先检测NRF24L01模块是否存在,在检测到NRF24L01模块之后,根据KEY0和KEY1的设置来决定模块的工作模式。在设定好工作模式之后,就会不停的发送/接收数据,同时在LCD上面显示相关信息。LED0闪烁用于提示程序正在运行。 - 硬件资源
1)LED灯 LED0 – PB4 2 ) 独立按键 KEY0 – PA1 KEY1 – PA15 3)2.4G无线模块 NRF24L01模块 4)正点原子2.8/3.5/4.3/7/10寸TFTLCD模块(仅限MCU屏,16位8080并口驱动) 5)串口1(PA9/PA10连接在板载USB转串口芯片CH340上面) 6)SPI2(连接在PB13/PB14/PB15上) - 原理图
NRF24L01模块与STM32的连接关系,如下图所示:
图41.2.1 NRF24L01模块接口与STM32连接原理图 这里NRF24L01使用的是SPI2,连接在PB13/PB14/PB15上。注意:NRF_IRQ和GBC_LED共用了PC3,所以,他们不能同时使用,需要分时复用。 由于无线通信实验是双向的,所以至少要有两个模块同时能工作,这里我们使用2套开发板来向大家演示。 41.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分别对应PC0,PE0和PC3,因此需要先使能SPI2、GPIOC和GPIOE时钟。参考代码如下: __HAL_RCC_GPIOx_CLK_ENABLE(); /* 使能GPIOx时钟,x=A……K / __HAL_RCC_SPI2_CLK_ENABLE (); / 使能SPI时钟 */ GPIO模式设置通过调用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接收和发送模式的函数。 41.3.1 程序流程图
图41.3.1.1无线通信实验程序流程图 41.3.2 程序解析
- SPI驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。SPI驱动源码包括两个文件:spi.c和spi.h。 首先介绍的是spi.h文件,具体定义如下:
#define SPI2_SCK_GPIO_PORT GPIOB
#define SPI2_SCK_GPIO_PIN GPIO_PIN_13
#define SPI2_SCK_GPIO_AF GPIO_AF5_SPI2
#define SPI2_SCK_GPIO_CLK_ENABLE()
do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0)
#define SPI2_MISO_GPIO_PORT GPIOB
#define SPI2_MISO_GPIO_PIN GPIO_PIN_14
#define SPI2_MISO_GPIO_AF GPIO_AF5_SPI2
#define SPI2_MISO_GPIO_CLK_ENABLE()
do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0)
#define SPI2_MOSI_GPIO_PORT GPIOB
#define SPI2_MOSI_GPIO_PIN GPIO_PIN_15
#define SPI2_MOSI_GPIO_AF GPIO_AF5_SPI2
#define SPI2_MOSI_GPIO_CLK_ENABLE()
do{ __HAL_RCC_GPIOB_CLK_ENABLE(); }while(0)
#define SPI2_SPI SPI2
#define SPI2_SPI_CLK_ENABLE()
do{ __HAL_RCC_SPI2_CLK_ENABLE(); }while(0)
上面的宏定义是SPI2的接口(PB13\PB14\PB15)的相关宏定义。
下面介绍spi.c文件,首先是SPI初始化相关函数,具体如下:
SPI_HandleTypeDef g_spi_handle;
void spi2_init(void)
{
g_spi_handle.Instance = SPI2;
g_spi_handle.Init.Mode = SPI_MODE_MASTER;
g_spi_handle.Init.Direction = SPI_DIRECTION_2LINES;
g_spi_handle.Init.DataSize = SPI_DATASIZE_8BIT;
g_spi_handle.Init.CLKPolarity = SPI_POLARITY_HIGH;
g_spi_handle.Init.CLKPhase = SPI_PHASE_2EDGE;
g_spi_handle.Init.NSS = SPI_NSS_SOFT;
g_spi_handle.Init.NSSPMode = SPI_NSS_PULSE_DISABLE;
g_spi_handle.Init.MasterKeepIOState = SPI_MASTER_KEEP_IO_STATE_ENABLE;
g_spi_handle.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_256;
g_spi_handle.Init.FirstBit = SPI_FIRSTBIT_MSB;
g_spi_handle.Init.TIMode = SPI_TIMODE_DISABLE;
g_spi_handle.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
g_spi_handle.Init.CRCPolynomial = 7;
HAL_SPI_Init(&g_spi_handle);
__HAL_SPI_ENABLE(&g_spi_handle);
spi2_read_write_byte(0Xff);
}
void HAL_SPI_MspInit(SPI_HandleTypeDef *hspi)
{
GPIO_InitTypeDef gpio_init_struct;
RCC_PeriphCLKInitTypeDef rcc_periph_clk_init;
SPI2_SPI_CLK_ENABLE();
SPI2_SCK_GPIO_CLK_ENABLE();
SPI2_MISO_GPIO_CLK_ENABLE();
SPI2_MOSI_GPIO_CLK_ENABLE();
rcc_periph_clk_init.PeriphClockSelection = RCC_PERIPHCLK_SPI2;
rcc_periph_clk_init.Spi123ClockSelection = RCC_SPI123CLKSOURCE_PLL;
HAL_RCCEx_PeriphCLKConfig(&rcc_periph_clk_init);
gpio_init_struct.Pin = SPI2_SCK_GPIO_PIN;
gpio_init_struct.Mode = GPIO_MODE_AF_PP;
gpio_init_struct.Pull = GPIO_PULLUP;
gpio_init_struct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
gpio_init_struct.Alternate = SPI2_SCK_GPIO_AF;
HAL_GPIO_Init(SPI2_SCK_GPIO_PORT, &gpio_init_struct);
gpio_init_struct.Pin = SPI2_MISO_GPIO_PIN;
gpio_init_struct.Alternate = SPI2_MISO_GPIO_AF;
HAL_GPIO_Init(SPI2_MISO_GPIO_PORT, &gpio_init_struct);
gpio_init_struct.Pin = SPI2_MOSI_GPIO_PIN;
gpio_init_struct.Alternate = SPI2_MOSI_GPIO_AF;
HAL_GPIO_Init(SPI2_MOSI_GPIO_PORT, &gpio_init_struct);
}
上面介绍的是SPI2初始化函数(SPI2_Init)和SPI初始化回调函数HAL_SPI_MspInit。
最后介绍的是SPI2速度设置函数和SPI2读写一个字节数据函数,具体如下:
void spi2_set_speed(uint32_t speed)
{
assert_param(IS_SPI_BAUDRATE_PRESCALER(speed));
__HAL_SPI_DISABLE(&g_spi_handle);
g_spi_handle.Instance->CFG1 &= ~(0X7 << 28);
g_spi_handle.Instance->CFG1 |= speed;
__HAL_SPI_ENABLE(&g_spi_handle);
}
uint8_t spi2_read_write_byte(uint8_t txdata)
{
uint8_t rxdata;
HAL_SPI_TransmitReceive(&g_spi_handle,&txdata,&rxdata, 1, 1000);
return rxdata;
}
其中,spi2_set_speed函数用于设置SPI2的传输速度也就是波特率,SPI2的传输速度是通过SPI2->CFG1寄存器的位30-28来设置。形参speed是SPI2时钟分频系数,取值范围是:SPI_BAUDRATEPRESCALER_2~SPI_BAUDRATEPRESCALER_256。 而spi2_read_write_byte函数主要是通过调用HAL库中SPI发送接收函数HAL_SPI_TransmitReceive来实现数据的发送和接收。 2. NRF24L01驱动代码 这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。NRF24L01驱动源码包括两个文件:nrf24l01.c和nrf24l01.h。 我们要在spi.c文件中封装好的函数的基础上进行调用,实现nrf24l01的发送与接收。下面先看一下nrf24l01.h文件中定义的信息,其代码如下: /* NRF24L01 操作引脚 定义(不包含SPI_SCK/MISO/MISO等三根线) */
#define NRF24L01_CE_GPIO_PORT GPIOC
#define NRF24L01_CE_GPIO_PIN GPIO_PIN_0
#define NRF24L01_CE_GPIO_CLK_ENABLE()
do{ __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0)
#define NRF24L01_CSN_GPIO_PORT GPIOE
#define NRF24L01_CSN_GPIO_PIN GPIO_PIN_0
#define NRF24L01_CSN_GPIO_CLK_ENABLE()
do{ __HAL_RCC_GPIOE_CLK_ENABLE(); }while(0)
#define NRF24L01_IRQ_GPIO_PORT GPIOC
#define NRF24L01_IRQ_GPIO_PIN GPIO_PIN_3
#define NRF24L01_IRQ_GPIO_CLK_ENABLE()
do{ __HAL_RCC_GPIOC_CLK_ENABLE(); }while(0)
#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)
#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)
#define NRF24L01_IRQ HAL_GPIO_ReadPin(NRF24L01_IRQ_GPIO_PORT,
NRF24L01_IRQ_GPIO_PIN)
以上除了有NRF24L01的引脚定义及引脚操作函数,此外还有一些NRF24L01寄存器操作命令以及其寄存器地址,由于篇幅太大,所以这里就不列出来了,大家可以去看一下工程文件。
下面看一下NRF24L01的初始化函数,其定义如下:
void nrf24l01_spi_init(void)
{
__HAL_SPI_DISABLE(&g_spi_handle);
g_spi_handle.Init.CLKPolarity = SPI_POLARITY_LOW;
g_spi_handle.Init.CLKPhase = SPI_PHASE_1EDGE;
HAL_SPI_Init(&g_spi_handle);
__HAL_SPI_ENABLE(&g_spi_handle);
}
void nrf24l01_init(void)
{
GPIO_InitTypeDef gpio_init_struct;
NRF24L01_CE_GPIO_CLK_ENABLE();
NRF24L01_CSN_GPIO_CLK_ENABLE();
NRF24L01_IRQ_GPIO_CLK_ENABLE();
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_VERY_HIGH;
HAL_GPIO_Init(NRF24L01_CE_GPIO_PORT, &gpio_init_struct);
gpio_init_struct.Pin = NRF24L01_CSN_GPIO_PIN;
HAL_GPIO_Init(NRF24L01_CSN_GPIO_PORT, &gpio_init_struct);
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_VERY_HIGH;
HAL_GPIO_Init(NRF24L01_IRQ_GPIO_PORT, &gpio_init_struct);
spi2_init();
nrf24l01_spi_init();
NRF24L01_CE(0);
NRF24L01_CSN(1);
}
在初始化函数中,我们主要对该模块用到的管脚进行配置以及从初始化工作以及需要调用spi.c文件中的spi_init函数对SPI1的引脚进行初始化。现在让我们看一下 NRF24L01的工作时序图见下图41.3.2.1所示。
图41.3.2.1 NRF24L01的工作时序图 大家可以比对一下前面章节的SPI工作时序图,符合工作模式1的时序,即在奇数边沿上升沿进行数据的采集。所以我们调用nrf24l01_spi_init函数针对NRF的特点修改SPI的设置。该函数就是将SPI的工作模式配置成串行同步时钟空闲状态为低电平,在奇数边沿数据被采集,也就是前面SPI实验章节中SPI的工作模式0。看看工作时序图的某些标号意义,Cn代表指令位,Sn代表状态寄存器位,Dn代表数据位。 下面介绍一下NRF24L01的读写函数,其代码如下:
static uint8_t nrf24l01_write_reg(uint8_t reg, uint8_t value)
{
uint8_t status;
NRF24L01_CSN(0);
status = spi2_read_write_byte(reg);
spi2_read_write_byte(value);
NRF24L01_CSN(1);
return status;
}
static uint8_t nrf24l01_read_reg(uint8_t reg)
{
uint8_t reg_val;
NRF24L01_CSN(0);
spi2_read_write_byte(reg);
reg_val = spi2_read_write_byte(0XFF);
NRF24L01_CSN(1);
return reg_val;
}
static uint8_t nrf24l01_read_buf(uint8_t reg, uint8_t *pbuf, uint8_t len)
{
uint8_t status, i;
NRF24L01_CSN(0);
status = spi2_read_write_byte(reg);
for (i = 0; i < len; i++)
{
pbuf[i] = spi2_read_write_byte(0XFF);
}
NRF24L01_CSN(1);
return status;
}
static uint8_t nrf24l01_write_buf(uint8_t reg, uint8_t *pbuf, uint8_t len)
{
uint8_t status, i;
NRF24L01_CSN(0);
status = spi2_read_write_byte(reg);
for (i = 0; i < len; i++)
{
spi2_read_write_byte(*pbuf++);
}
NRF24L01_CSN(1);
return status;
}
以上是NRF24L01的写寄存器函数和读寄存器函数,以及扩展的函数:在指定位置写入指定长度的数据函数和指定位置读取指定长度的数据函数。 先讲一下NRF24L01读写寄存器函数实现的具体过程: 先拉低片选线→发送寄存器号→发送数据/接收数据→拉高片选线。 这里提及一下SPI的相关知识:SPI是通过移位寄存器进行数据传输,所以发一字节数据就会收到一个字节数据。那么发数据就可以直接发送数据,接收数据只需要发送0xFF,寄存器会返回要读取的数据。 在指定位置写入指定长度的数据函数和在指定位置读取指定长度的数据函数的实现方式也是通过调用SPI的读写一字节函数实现,这里跟写寄存器和读寄存器函数的实现差不多,这里就不做展开了。 下面看一下这两种模式的初始化过程: Rx模式初始化过程: 1)写Rx节点的地址 2)使能通道x自动应答 3)使能通道x接收地址 4)设置通信频率 5)选择通道x的有效数据宽度 6)配置发射参数(发射功率、无线速率) 7)配置NRF24L01的基本参数以及切换工作模式 其代码如下:
void nrf24l01_rx_mode(void)
{
NRF24L01_CE(0);
nrf24l01_write_buf(NRF_WRITE_REG + RX_ADDR_P0, (uint8_t *)RX_ADDRESS, RX_ADR_WIDTH);
nrf24l01_write_reg(NRF_WRITE_REG + EN_AA, 0x01);
nrf24l01_write_reg(NRF_WRITE_REG + EN_RXADDR, 0x01);
nrf24l01_write_reg(NRF_WRITE_REG + RF_CH, 40);
nrf24l01_write_reg(NRF_WRITE_REG + RX_PW_P0, RX_PLOAD_WIDTH);
nrf24l01_write_reg(NRF_WRITE_REG + RF_SETUP, 0x0f);
nrf24l01_write_reg(NRF_WRITE_REG + CONFIG, 0x0f);
NRF24L01_CE(1);
}
Tx模式初始化过程:
1)写Tx节点的地址
2)写Rx节点的地址,主要为了使能硬件的自动应答
3)使能通道x的自动应答
4)使能通道x接收地址
5)配置自动重发次数
6)配置通信频率
7)选择通道x的有效数据宽度
8)配置发射参数(发射功率、无线速率)
9)配置NRF24L01的基本参数以及切换工作模式
其代码如下:
void nrf24l01_tx_mode(void)
{
NRF24L01_CE(0);
nrf24l01_write_buf(NRF_WRITE_REG + TX_ADDR, (uint8_t *)TX_ADDRESS, TX_ADR_WIDTH);
nrf24l01_write_buf(NRF_WRITE_REG + RX_ADDR_P0, (uint8_t *)RX_ADDRESS, RX_ADR_WIDTH);
nrf24l01_write_reg(NRF_WRITE_REG + EN_AA, 0x01);
nrf24l01_write_reg(NRF_WRITE_REG + EN_RXADDR, 0x01);
nrf24l01_write_reg(NRF_WRITE_REG + SETUP_RETR, 0x1a);
nrf24l01_write_reg(NRF_WRITE_REG + RF_CH, 40);
nrf24l01_write_reg(NRF_WRITE_REG + RF_SETUP, 0x0f);
nrf24l01_write_reg(NRF_WRITE_REG + CONFIG, 0x0e);
NRF24L01_CE(1);
}
以上就是两种模式的配置,看过完整代码的,会发现TX_ADDR和RX_ADDR两个地址是一样的,跟前面说法一致,我们必须保持地址的匹配才能通信成功。以上代码中的发送函数都有一个特点,并不是单纯发送寄存器地址,而是操作指令+寄存器地址,这一点需要记得。NRF24L01的操作指令也有好几个,它是配合寄存器完成特定的操作,其定义如下:
#define NRF_READ_REG 0x00
#define NRF_WRITE_REG 0x20
#define RD_RX_PLOAD 0x61
#define WR_TX_PLOAD 0xA0
#define FLUSH_TX 0xE1
#define FLUSH_RX 0xE2
#define REUSE_TX_PL 0xE3
#define NOP 0xFF
经过上面的发送或者接收模式初始化步骤后,NRF24L01就可以准备启动发送数据或者等待接收数据了。
下面来看一下启动NRF24L01发送一次数据的函数,其定义如下:
uint8_t nrf24l01_tx_packet(uint8_t *ptxbuf)
{
uint8_t sta;
uint8_t rval = 0XFF;
NRF24L01_CE(0);
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);
if (sta & MAX_TX)
{
nrf24l01_write_reg(FLUSH_TX, 0xff);
rval = 1;
}
if (sta & TX_OK)
{
rval = 0;
}
return rval;
}
在这里启动发送数据函数中,具体实现很简单,拉低片选信号→向发送数据寄存器写入数据→拉高片选信号。这里说明一下,在发送完寄存器号后都会返回一个status值,返回的这个值就是前面介绍的STATUS寄存器的内容。在这个基础上就可以知道数据是否发送完成以及现在的状态。 然后介绍一下NRF24L01接收一次数据函数,其定义如下:
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);
if (sta & RX_OK)
{
nrf24l01_read_buf(RD_RX_PLOAD, prxbuf, RX_PLOAD_WIDTH);
nrf24l01_write_reg(FLUSH_RX, 0xff);
rval = 0;
}
return rval;
}
在启动接收的过程中,首先需要判断当前NRF24L01的状态,往后才是真正的读取数据,清除接收寄存器的缓冲,完成数据的接收。这里需要注意的是我们通过RX_PLOAD_WIDTH和TX_PLOAD_WIDTH决定了接收和发送的数据宽度,这也决定每次发送和接收的有效字节数。NRF24L01每次最多传输32个字节,再多的字节传输则需要多次传输。通信双方的发送和接收数据宽度必须一致才能正常通信。 3. main.c代码 在main.c里编写如下代码:
int main(void)
{
uint8_t key, mode;
uint16_t t = 0;
uint8_t tmp_buf[33];
sys_cache_enable();
HAL_Init();
sys_stm32_clock_init(240, 2, 2, 4);
delay_init(480);
usart_init(115200);
mpu_memory_protection();
led_init();
lcd_init();
key_init();
nrf24l01_init();
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())
{
lcd_show_string(30, 130, 200, 16, 16, "NRF24L01 Error", RED);
delay_ms(200);
lcd_fill(30, 130, 239, 130 + 16, WHITE);
delay_ms(200);
}
lcd_show_string(30, 130, 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, 150, 230, 16, 16,
"KEY0:RX_Mode KEY1:TX_Mode", RED);
}
if (t == 200)
{
lcd_fill(10, 150, 230, 150 + 16, WHITE);
t = 0;
}
delay_ms(5);
}
lcd_fill(10, 150, 240, 166, WHITE);
if (mode == 0)
{
lcd_show_string(30, 150, 200, 16, 16, "NRF24L01 RX_Mode", BLUE);
lcd_show_string(30, 170, 200, 16, 16, "Received DATA:", BLUE);
nrf24l01_rx_mode();
while (1)
{
if (nrf24l01_rx_packet(tmp_buf) == 0)
{
tmp_buf[32] = 0;
lcd_show_string(0,190,lcddev.width-1,32,16,(char*)tmp_buf, BLUE);
}
else delay_us(100);
t++;
if (t == 10000)
{
t = 0;
LED0_TOGGLE();
}
}
}
else
{
lcd_show_string(30, 150, 200, 16, 16, "NRF24L01 TX_Mode", BLUE);
nrf24l01_tx_mode();
mode = ' ';
while (1)
{
if (nrf24l01_tx_packet(tmp_buf) == 0)
{
lcd_show_string(30, 170, 239, 32, 16, "Sended DATA:", BLUE);
lcd_show_string(0,190,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, 170, lcddev.width, 170 + 16 * 3, WHITE);
lcd_show_string(30,170,lcddev.width-1,32,16, "Send Failed ",BLUE);
}
LED0_TOGGLE();
delay_ms(200);
};
}
}
程序运行时先通过nrf24l01_cheak函数检测NRF24L01是否存在,如果存在,则让用户选择发送模式还是接收模式,在确定模式之后,设置NRF24L01的工作模式,然后执行相对应的数据发送/接收处理。 41.4 下载验证 将程序下载到开发板后,可以看到LCD显示的内容如图41.4.1所示:
图41.4.1 选择工作模式界面 通过KEY0和KEY1来选择NRF24L01模块所要进入的工作模式,我们两个开发板一个选择发送模式,一个选择接收模式就可以了。 设置好的通信界面如图41.4.2和图41.4.3所示:
图41.4.2 开发板A通信界面(发送)
图41.4.3 开发板B通信界面(接收) 图41.4.2来自于开发板A,工作在发送模式。图41.4.3来自于开发板B,工作在接收模式,A发送,B接收。发送和接收图片的数据不一样,是因为我们拍照的时间不一样导致的。大家看到收发数据是一致,那就说明实验成功。
|