STM32 | 基于NRF24L01串口透传(不定长数据无线串口双向传输)

nRF24L01配置

写在前,如果你认真一步一步跟着本文走,相信你会清楚nRF24L01的使用,会有想不到的收获,看十篇相关文章,不如认认真真读本文!
本文介绍如何使用nRF24L01实现不定长数据串口透传,换句话讲就是学习如何配置使用nRF24L01。STM32使用nRF24L01的文章很多,但要找到符合我心意的太难了!我要实现的是串口A收到的数据通过nRF24L01传输,由另一块板子的串口B输出。当然这很简单,但是我想要结合串口DMA和SPI DMA。功能是实现了,不过本文先讲讲最基本的nRF24L01的使用。如果本文写得好,可以参与评论或点个赞,希望大家多多支持,谢谢!说说本文关键词吧,为你节省时间!

测试平台:STM32F103C8T6
库版本:官方库3.5版本
关键词:不定长数据 (定长也只是其中一种);双向传输;串口透传;中断方式
代码下载地址: 基于NRF24L01实现串口透传(不定长数据无线串口双向通信).zip (点击链接跳转)
                        (https://download.csdn.net/download/weixin_44524484/12345526)

一、引脚说明

序号引脚功能
1GND接地(0V)
2VCC电源(1.9V~3.6V),超过3.6V将会烧毁模块。推荐电压3.3V左右。
3CE芯片的模式控制线。在 CSN 为低的情况下,CE 协同NRF24L01 的CONFIG 寄存器共同决定NRF24L01 的状态
4CSN芯片(SPI)的片选线,CSN 为低电平芯片工作
5SCK为芯片控制的时钟线(SPI时钟)
6MOSISPI控制数据线,主输出 、从输入
7MISOSPI控制数据线,主输入 、从输出
8IRQ中断信号引脚,中断时变为低电平。引脚会在以下三种情况变低:Tx FIFO 发完数据并且收到ACK(使能ACK情况下)、Rx FIFO 收到数据达到最大重发次数

二、工作方式

nRF24L01工作模式由芯片的模式控制线CE和配置寄存器CONFIG内部PWR_UP、PRIM_RX位共同控制。

NRF24L01所处模式PWR_UP位状态PRIM_RX位状态CE引脚电平FIFO 寄存器状态
接收模式111-
发送模式101数据在TX FIFO 寄存器中
发送模式101→0停留在发送模式,直至数据发送完
待机模式II101TX FIFO 为空
待机模式I1-0无数据传输
掉电模式0---

三、相关寄存器介绍

1、配置寄存器(配置NRF24L01一些工作方式)

寄存器地址:0x00       名称:CONFIG(配置寄存器)

第7位第6位(MASK_RX_DR)第5位(MASK_TX_DS)第4位(MASK_MAX_RT)第3位(EN_CRC)第2位(CRCO)第1位(PWR_UP)第0位(PRIM_RX)
保留(未用)可屏蔽中断RX_RD。‘1’:IRQ 引脚不显示RX_RD中断;‘0’:RX_RD 中断产生时IRQ 引脚电平为低可屏蔽中断TX_DS。‘1’:IRQ 引脚不显示TX_DS 中断;‘0’:TX_DS 中断产生时IRQ 引脚电平为低可屏蔽中断MAX_RT。‘1’:IRQ 引脚不显示TX_DS 中断;‘0’:MAX_RT 中断产生时IRQ 引脚电平为低CRC使能。如果EN_AA 中任意一位为高则EN_CRC 强迫为高。CRC模式。‘0’:8 位CRC 校验;‘1’:16 位CRC 校验NRF24L01上电掉电模式设置位。‘1’:上电;‘0’:掉电NRF24L01接收、发射模式设置位。‘1’:接收模式;;‘0’:发射模式

2、状态寄存器(反应NRF24L01当前工作状态)

寄存器地址:0x07       名称:STATUS(状态寄存器)

第7位第6位(RX_DR)第5位(TX_DS)第4位(MAX_RT)第3~1位(RX_P_NO)第0位(TX_FULL)
保留(未用)接收数据中断位。当接收到有效数据后置 ‘1’ 。 写 ‘1’ 清除中断数据发送完成中断。当数据发送完成后产生中断。如果工作在自动应答模式下,只有当接收到应答信号后此位置 ‘1’ 。写 ‘1’ 清除中断达到最多次重发中断。写 ‘1’ 清除中断。如果MAX_RT中断产生则必须清除后系统才能进行通讯接收数据通道号 位。‘000-101’:数据通道号、110:未使用、111:RX FIFO 寄存器为空TX FIFO 寄存器满标志。 ‘1’ :TX FIFO 寄存器满。‘0’ :TX FIFO 寄存器未满,有可用空间。

3、NRF24L01寄存器操作命令及地址(宏定义)

动态数据的三条重要宏定义:
#define R_RX_PL_WID 0x60 //读取接收数据长度
#define DYNPD 0x1C //动态长度地址;bit7~ 6,只允许’00’;bit5~ 0,分别对应使能动态有效数据长度数据通道5 ~ 0
#define FEATURE 0x1D //功能寄存器地址;bit7~ 3,只允许’0000’;bit2,使能动态有效数据长度;bit1,使能ACK有效数据;bit0,使能’W_TX_PAYLOAD_NOACK’命令

//NRF24L01寄存器操作命令
#define NRF_READ_REG    0x00  //读配置寄存器,低5位为寄存器地址
#define NRF_WRITE_REG   0x20  //写配置寄存器,低5位为寄存器地址
#define R_RX_PL_WID     0x60  //读取接收数据长度
#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  //空操作,可以用来读状态寄存器

//SPI(NRF24L01)寄存器地址
#define CONFIG          0x00  //配置寄存器地址;bit0:1接收模式,0发射模式;bit1:电选择;bit2:CRC模式;bit3:CRC使能;
                              //bit4:中断MAX_RT(达到最大重发次数中断)使能;bit5:中断TX_DS使能;bit6:中断RX_DR使能
#define EN_AA           0x01  //使能自动应答功能  bit0~5,对应通道0~5
#define EN_RXADDR       0x02  //接收地址允许,bit0~5,对应通道0~5
#define SETUP_AW        0x03  //设置地址宽度(所有数据通道):bit1,0:00,3字节;01,4字节;02,5字节;
#define SETUP_RETR      0x04  //建立自动重发;bit3:0,自动重发计数器;bit7:4,自动重发延时 250*x+86us
#define RF_CH           0x05  //RF通道,bit6:0,工作通道频率;
#define RF_SETUP        0x06  //RF寄存器;bit3:传输速率(0:1Mbps,1:2Mbps);bit2:1,发射功率;bit0:低噪声放大器增益
#define STATUS          0x07  //状态寄存器;bit0:TX FIFO满标志;bit3:1,接收数据通道号(最大:6);bit4,达到最多次重发
                              //bit5:数据发送完成中断;bit6:接收数据中断;
#define MAX_TX  		0x10  //达到最大发送次数中断
#define TX_OK   		0x20  //TX发送完成中断
#define RX_OK 		  	0x40  //接收到数据中断

#define OBSERVE_TX      0x08  //发送检测寄存器,bit7:4,数据包丢失计数器;bit3:0,重发计数器
#define CD              0x09  //载波检测寄存器,bit0,载波检测;
#define RX_ADDR_P0      0x0A  //数据通道0接收地址,最大长度5个字节,低字节在前
#define RX_ADDR_P1      0x0B  //数据通道1接收地址,最大长度5个字节,低字节在前
#define RX_ADDR_P2      0x0C  //数据通道2接收地址,最低字节可设置,高字节,必须同RX_ADDR_P1[39:8]相等;
#define RX_ADDR_P3      0x0D  //数据通道3接收地址,最低字节可设置,高字节,必须同RX_ADDR_P1[39:8]相等;
#define RX_ADDR_P4      0x0E  //数据通道4接收地址,最低字节可设置,高字节,必须同RX_ADDR_P1[39:8]相等;
#define RX_ADDR_P5      0x0F  //数据通道5接收地址,最低字节可设置,高字节,必须同RX_ADDR_P1[39:8]相等;
#define TX_ADDR         0x10  //发送地址(低字节在前),ShockBurstTM模式下,RX_ADDR_P0与此地址相等
#define RX_PW_P0        0x11  //接收数据通道0有效数据宽度(1~32字节),设置为0则非法
#define RX_PW_P1        0x12  //接收数据通道1有效数据宽度(1~32字节),设置为0则非法
#define RX_PW_P2        0x13  //接收数据通道2有效数据宽度(1~32字节),设置为0则非法
#define RX_PW_P3        0x14  //接收数据通道3有效数据宽度(1~32字节),设置为0则非法
#define RX_PW_P4        0x15  //接收数据通道4有效数据宽度(1~32字节),设置为0则非法
#define RX_PW_P5        0x16  //接收数据通道5有效数据宽度(1~32字节),设置为0则非法
#define NRF_FIFO_STATUS 0x17  //FIFO状态寄存器;bit0,RX FIFO寄存器空标志;bit1,RX FIFO满标志;bit2,3,保留
                              //bit4,TX FIFO空标志;bit5,TX FIFO满标志;bit6,1,循环发送上一数据包.0,不循环;
#define DYNPD        	0x1C  //动态长度地址;bit7~6,只允许'00';bit5~0,分别对应使能动态有效数据长度数据通道5~0
#define FEATURE        	0x1D  //功能寄存器地址;bit7~3,只允许'0000';bit2,使能动态有效数据长度;
							  //bit1,使能ACK有效数据;bit0,使能'W_TX_PAYLOAD_NOACK'命令

相关的寄存器上述也有解释,使用到的寄存器我在后文也有讲解,如需了解可点击此处跳转查看

四、配置步骤

1、SPI配置

nRF24L01使用的通信方式是SPI,所以先预配置SPI2,这里的SPI2配置仅仅预配置,时钟极性CPOL和时钟相位CPHA还未根据nRF24L01的时序图作修改

//SPI2初始化
void SPI2_Init(void)
{
 	GPIO_InitTypeDef GPIO_InitStructure;
 	SPI_InitTypeDef  SPI_InitStructure;

	RCC_APB2PeriphClockCmd(	RCC_APB2Periph_GPIOB, ENABLE );					//PORTB时钟使能 
	RCC_APB1PeriphClockCmd(	RCC_APB1Periph_SPI2,  ENABLE );					//SPI2时钟使能 	
 
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_13 | GPIO_Pin_14 | GPIO_Pin_15;
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;  						//PB13/14/15复用推挽输出 
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_Init(GPIOB, &GPIO_InitStructure);									//初始化GPIOB

 	GPIO_SetBits(GPIOB,GPIO_Pin_13|GPIO_Pin_14|GPIO_Pin_15);  				//PB13/14/15上拉

	SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; 		//设置SPI单向或者双向的数据模式:SPI设置为双线双向全双工
	SPI_InitStructure.SPI_Mode = SPI_Mode_Master;							//设置SPI工作模式:设置为主SPI
	SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;						//设置SPI的数据大小:SPI发送接收8位帧结构
	SPI_InitStructure.SPI_CPOL = SPI_CPOL_High;								//串行同步时钟的空闲状态为高电平
	SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge;							//串行同步时钟的第二个跳变沿(上升或下降)数据被采样
	SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;								//NSS信号由硬件(NSS管脚)还是软件(使用SSI位)管理:内部NSS信号有SSI位控制
	SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256;	//定义波特率预分频的值:波特率预分频值为256
	SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;						//指定数据传输从MSB位还是LSB位开始:数据传输从MSB位开始
	SPI_InitStructure.SPI_CRCPolynomial = 7;								//CRC值计算的多项式
	SPI_Init(SPI2, &SPI_InitStructure);  									//根据SPI_InitStruct中指定的参数初始化外设SPIx寄存器
 
	SPI_Cmd(SPI2, ENABLE); 													//使能SPI外设
}

为了方便调整SPI2的通信速度,故有以下SPI2速度设置函数:

//SPI 速度设置函数
//SpeedSet:
//SPI_BaudRatePrescaler_2   2分频   
//SPI_BaudRatePrescaler_8   8分频   
//SPI_BaudRatePrescaler_16  16分频  
//SPI_BaudRatePrescaler_256 256分频 
void SPI2_SetSpeed(u8 SPI_BaudRatePrescaler)
{
	assert_param(IS_SPI_BAUDRATE_PRESCALER(SPI_BaudRatePrescaler));
	SPI2->CR1&=0XFFC7;
	SPI2->CR1|=SPI_BaudRatePrescaler;	//设置SPI2速度 
	SPI_Cmd(SPI2,ENABLE);
} 

SPI2的读写函数如下:

//SPIx 读写一个字节
//TxData:要写入的字节
//返回值:读取到的字节
u8 SPI2_ReadWriteByte(u8 TxData)
{		
	u8 retry=0;				 	
	while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_TXE) == RESET) //检查指定的SPI标志位设置与否:发送缓存空标志位
	{
		retry++;
		if(retry>200)return 0;		//超时返回
	}			  
	SPI_I2S_SendData(SPI2, TxData); //通过外设SPIx发送一个数据
	retry=0;

	while (SPI_I2S_GetFlagStatus(SPI2, SPI_I2S_FLAG_RXNE) == RESET)//检查指定的SPI标志位设置与否:接受缓存非空标志位
	{
		retry++;
		if(retry>200) return 0;		  //超时返回
	}	  						    
	return SPI_I2S_ReceiveData(SPI2); //返回通过SPIx最近接收的数据					    
}

至此,SPI2预配置完毕!但要使STM32F103与nRF24L01通信还需修改配置SPI2的时钟极性CPOL和时钟相位CPHA,除此,还需配置CE、CSN、IRQ引脚

2、nRF24L01管脚初始化

前面我们已经完成了SPI2的基本配置,对于SPI的配置到处都有例程,直接拿来用即可,也可以把我上面的代码复制到你的“SPI.c”文件中,然后把函数添加进“.h”文件中以便调用。要注意的是,如果你使用SPI1要修改为SPI1的配置。配置好后只需调用SPI2_Init();即可对SPI2进行初始化。
前面只配置了CLK、MISO、MOSI,NSS信号由是软件(使用SSI位)控制,所以还要配置CSN、CE、IRQ管脚,下面是nRF24L01模块的IO配置。

相应管脚连接如下:(可自行修改)

管脚GPIO口
CEPC8
CSNPC7
IRQPC6

(1)首先先在“24l01.h”头文件中把前面的寄存器命令和地址添加进去,再补充以下一些宏定义。

//24L01操作线
#define NRF24L01_CE   	PCout(8) //24L01片选信号
#define NRF24L01_CSN  	PCout(7) //SPI片选信号	   
#define NRF24L01_IRQ 	PCin(6)  //IRQ主机数据输入
//24L01发送接收数据宽度定义
#define TX_ADR_WIDTH    4   	 //4字节的地址宽度
#define RX_ADR_WIDTH    4   	 //4字节的地址宽度
#define TX_PLOAD_WIDTH  32  	 //32字节的用户数据宽度
#define RX_PLOAD_WIDTH  32  	 //32字节的用户数据宽度
//频道选择 							   	   
#define CHANAL 			0x50	 //通信频道

如果想问PCout()、PCin()是什么?其实也不是什么,就是一个“sys.h”里的宏定义,实现51类似的GPIO控制功能。
“sys.h”里的宏定义我这里也给出:

//位带操作,实现51类似的GPIO控制功能
//具体实现思想,参考<<CM3权威指南>>第五章(87页~92页).
//IO口操作宏定义
#define BITBAND(addr, bitnum) ((addr & 0xF0000000)+0x2000000+((addr &0xFFFFF)<<5)+(bitnum<<2)) 
#define MEM_ADDR(addr)  *((volatile unsigned long  *)(addr)) 
#define BIT_ADDR(addr, bitnum)   MEM_ADDR(BITBAND(addr, bitnum)) 
//IO口地址映射
#define GPIOA_ODR_Addr    (GPIOA_BASE+12) //0x4001080C 
#define GPIOB_ODR_Addr    (GPIOB_BASE+12) //0x40010C0C 
#define GPIOC_ODR_Addr    (GPIOC_BASE+12) //0x4001100C 
#define GPIOD_ODR_Addr    (GPIOD_BASE+12) //0x4001140C 
#define GPIOE_ODR_Addr    (GPIOE_BASE+12) //0x4001180C 
#define GPIOF_ODR_Addr    (GPIOF_BASE+12) //0x40011A0C    
#define GPIOG_ODR_Addr    (GPIOG_BASE+12) //0x40011E0C    

#define GPIOA_IDR_Addr    (GPIOA_BASE+8) //0x40010808 
#define GPIOB_IDR_Addr    (GPIOB_BASE+8) //0x40010C08 
#define GPIOC_IDR_Addr    (GPIOC_BASE+8) //0x40011008 
#define GPIOD_IDR_Addr    (GPIOD_BASE+8) //0x40011408 
#define GPIOE_IDR_Addr    (GPIOE_BASE+8) //0x40011808 
#define GPIOF_IDR_Addr    (GPIOF_BASE+8) //0x40011A08 
#define GPIOG_IDR_Addr    (GPIOG_BASE+8) //0x40011E08 
 
//IO口操作,只对单一的IO口!
//确保n的值小于16!
#define PAout(n)   BIT_ADDR(GPIOA_ODR_Addr,n)  //输出 
#define PAin(n)    BIT_ADDR(GPIOA_IDR_Addr,n)  //输入 

#define PBout(n)   BIT_ADDR(GPIOB_ODR_Addr,n)  //输出 
#define PBin(n)    BIT_ADDR(GPIOB_IDR_Addr,n)  //输入 

#define PCout(n)   BIT_ADDR(GPIOC_ODR_Addr,n)  //输出 
#define PCin(n)    BIT_ADDR(GPIOC_IDR_Addr,n)  //输入 

#define PDout(n)   BIT_ADDR(GPIOD_ODR_Addr,n)  //输出 
#define PDin(n)    BIT_ADDR(GPIOD_IDR_Addr,n)  //输入 

#define PEout(n)   BIT_ADDR(GPIOE_ODR_Addr,n)  //输出 
#define PEin(n)    BIT_ADDR(GPIOE_IDR_Addr,n)  //输入

#define PFout(n)   BIT_ADDR(GPIOF_ODR_Addr,n)  //输出 
#define PFin(n)    BIT_ADDR(GPIOF_IDR_Addr,n)  //输入

#define PGout(n)   BIT_ADDR(GPIOG_ODR_Addr,n)  //输出 
#define PGin(n)    BIT_ADDR(GPIOG_IDR_Addr,n)  //输入

要注意的是,这样宏定义后就是方便置 ‘1’ 或复 ‘0’

  • NRF24L01_CE=1; 等价于 PCout(8)=1; 等价于 GPIO_SetBits(GPIOC,GPIO_Pin_8);
  • NRF24L01_CE=0; 等价于 PCout(8)=0; 等价于 GPIO_ResetBits(GPIOC,GPIO_Pin_8);
  • NRF24L01_IRQ 等价于 PCin(6) 就是读取PC6的电平状态

在“24l01.c”里还需定义以下全局常量(发送及接收地址,其实两地址一样),目的是为了方便后面的调用。

const u8 TX_ADDRESS[TX_ADR_WIDTH] = {0x4c,0x45,0x44,0x52}; 	//发送地址
const u8 RX_ADDRESS[RX_ADR_WIDTH] = {0x4c,0x45,0x44,0x52};	//接收地址

接下来正式配置nRF24L01的管脚,以下是NRF24L01的IO口初始化函数。

//初始化24L01的IO口
void NRF24L01_Init_IO(void)
{ 	
	GPIO_InitTypeDef GPIO_InitStructure;
    SPI_InitTypeDef  SPI_InitStructure;

	//使能GPIOC端口时钟  //PC8-CE  PC7-CSN  PC6-IRQ
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOC, ENABLE);	
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);			//使能复用
  	
	/*配置SPI_NRF_SPI的CE引脚,和SPI_NRF_SPI的 CSN 引脚*/
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; 	 		//推挽输出
 	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_7|GPIO_Pin_8;		//PC7 8 推挽 	  
 	GPIO_Init(GPIOC, &GPIO_InitStructure);						//初始化指定IO
    GPIO_ResetBits(GPIOC,GPIO_Pin_7|GPIO_Pin_8);				//PC7,8下拉
  
	/*配置SPI_NRF_SPI的IRQ引脚*/
	GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_6;   
	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; 				//PC6 输入  
	GPIO_Init(GPIOC, &GPIO_InitStructure);	

    SPI2_Init();    			//初始化SPI2

	SPI_Cmd(SPI2, DISABLE); 	// SPI外设不使能

	SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex;  //SPI设置为双线双向全双工
	SPI_InitStructure.SPI_Mode = SPI_Mode_Master;						//SPI主机
    SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b;					//发送接收8位帧结构
	SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low;							//时钟悬空低
	SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge;						//数据捕获于第1个时钟沿
	SPI_InitStructure.SPI_NSS = SPI_NSS_Soft;							//NSS信号由软件控制
	SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_8;	//定义波特率预分频的值:波特率预分频值为16
	SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB;					//数据传输从MSB位开始
	SPI_InitStructure.SPI_CRCPolynomial = 7;							//CRC值计算的多项式
	SPI_Init(SPI2, &SPI_InitStructure);  								//根据SPI_InitStruct中指定的参数初始化外设SPIx寄存器
 
	SPI_Cmd(SPI2, ENABLE); 							//使能SPI外设
	SPI2_SetSpeed(SPI_BaudRatePrescaler_4);			//设置时钟4分频,我这测试可用,如果错误建议使用SPI_BaudRatePrescaler_8
	NRF24L01_CE=0; 									//使能24L01
	NRF24L01_CSN=1;									//SPI片选取消
}

上述代码中需注意的是SPI频率,nRF24L01模块最高频率可达10MHz,但是建议不要超过8MHz,SPI2用的时钟是36MHz,所以我使用SPI2_SetSpeed(SPI_BaudRatePrescaler_4);函数进行4分频,这里是9MHz。SPI1用的系统时钟是72MHz,应该进行8分频,根据实际情况作修改。
到此,NRF24L01模块的管脚基本配置完成,要提醒的是本人使用的是中断方式,所以还应该初始化外部中断,这个我放到后面写。

3、nRF24L01读写函数

上述已经完成nRF24L01的GPIO配置,但要想和nRF24L01进行正常的通讯,还需要写四个与读写相关函数,其实就是对SPI读写函数“再包装”。我们进行SPI通信的时候还要对CSN片选信号进行拉低,而不是直接一个SPI2_ReadWriteByte();函数就可以进行SPI通信,和串口的两根线通信有些区别。接下来我们需要完成两个读写nRF24L01寄存器函数(其实就是字节的SPI读写)以及nRF24L01与STM32之间的两个SPI收发数据函数(其实就是字节的SPI读写)。

//SPI写寄存器
//reg:指定寄存器地址
//value:写入的值
u8 NRF24L01_Write_Reg(u8 reg, u8 value)
{
	u8 status;
	while(!PCin(7));					//等待上一次24L01收发数据完成
  	NRF24L01_CSN = 0;                 	//使能SPI传输
  	status = SPI2_ReadWriteByte(reg);	//发送寄存器号 
  	SPI2_ReadWriteByte(value);      	//写入寄存器的值
  	NRF24L01_CSN = 1;                 	//禁止SPI传输	   
  	return(status);       				//返回状态值
}

//读取SPI寄存器值
//reg:要读的寄存器
u8 NRF24L01_Read_Reg(u8 reg)
{
	u8 reg_val;
	while(!PCin(7));					//等待上一次24L01收发数据完成
 	NRF24L01_CSN = 0;          			//使能SPI传输		
  	SPI2_ReadWriteByte(reg);   			//发送寄存器号
 	reg_val = SPI2_ReadWriteByte(0XFF);	//读取寄存器内容
 	NRF24L01_CSN = 1;          			//禁止SPI传输		    
 	return(reg_val);           			//返回状态值
}	

//在指定位置读出指定长度的数据
//reg:寄存器(位置)
//*pBuf:数据指针
//len:数据长度
//返回值,此次读到的状态寄存器值 
u8 NRF24L01_Read_Buf(u8 reg,u8 *pBuf,u8 len)
{
	u8 status, u8_ctr=u8_ctr;
	while(!PCin(7));							//等待上一次24L01收发数据完成
  	NRF24L01_CSN = 0;           				//使能SPI传输
  	status = SPI2_ReadWriteByte(reg);			//发送寄存器值(位置),并读取状态值
 	for(u8_ctr=0; u8_ctr<len; u8_ctr++)
 		pBuf[u8_ctr] = SPI2_ReadWriteByte(0XFF);//读出数据
	NRF24L01_CSN = 1;       					//关闭SPI传输
	return status;        						//返回读到的状态值
}

//在指定位置写指定长度的数据
//reg:寄存器(位置)
//*pBuf:数据指针
//len:数据长度
//返回值,此次读到的状态寄存器值
u8 NRF24L01_Write_Buf(u8 reg, u8 *pBuf, u8 len)
{
	u8 status,u8_ctr=u8_ctr;
	while(!PCin(7));						//等待上一次24L01收发数据完成
 	NRF24L01_CSN = 0;          				//使能SPI传输
 	status = SPI2_ReadWriteByte(reg);		//发送寄存器值(位置),并读取状态值
 	for(u8_ctr=0; u8_ctr<len; u8_ctr++)
  		SPI2_ReadWriteByte(*pBuf++); 		//写入数据
	NRF24L01_CSN = 1;       				//关闭SPI传输
	return status;          				//返回读到的状态值
}

虽然上述代码都有注释,但我还是简单介绍一下。
与网上其他例程不同的是我加了句while(!PCin(7));文章最前面我有说过,我最后要实现DMA方式的,如果上一次数据还没传输完成,那再次传输数据必然出错。所以这里用了个最简单地方法,每次数据传送完成都需要拉高CSN,那我只需要在拉低CSN之前保证CSN为高电平状态即可,故当PC7为低电平(CSN为低)时,等待CSN为高再拉低CSN。这里先了解一下while(!PCin(7));的意义,当然,本次暂时不介绍DMA方式,可以暂且注释这句也是OK的。

要注意的是这四个函数使用的时候,发送寄存器号并不是单纯地发送【寄存器地址】,而是【操作指令 + 寄存器地址】在发送完寄存器号后都会返回一个status值,返回的这个值就是前面介绍的STATUS寄存器的内容。

提示一下,SPI是通过移位寄存器实现数据传输,所以发一个字节数据就会收一个字节数据。故发送数据接直接发送数据,接收数据就发送0XFF,寄存器会返回要读取的数据。四个函数实现的操作主要是先拉低CSN–发送寄存器号–发送数据/接收数据–拉高CSN,具体参考如下数据手册的SPI读写时序。
nRF24L01的SPI读写时序图

4、nRF24L01模式初始化

给代码之前,先简单了解下nRF24L01的接收模式和发送模式。前面已经给过nRF24L01的工作方式,忘记的回到文章前面去看看。这里我列出采用ShockBurst 模式的接收模式和发送模式的初始化配置过程,熟悉了 nRF24L01 以后可以采用别的通信方式。

Rx 模式初始化过程:

初始化步骤24L01相关寄存器
1)写Rx 节点的地址RX_ADDR_P0
2)使能AUTOACK EN_AA
3)使能PIPE 0EN_RXADDR
4)选择通信频率RF_CH
5)选择通道0 有效数据宽度Rx_Pw_P0
6)配置发射参数(低噪放大器增益、发射功率、无线速率)RF_SETUP
7)配置24L01 的基本参数以及切换工作模式 CONFIG

Tx 模式初始化过程:

初始化步骤24L01相关寄存器
1)写Tx 节点的地址 TX_ADDR
2)写Rx 节点的地址(主要是为了使能Auto Ack)RX_ADDR_P0
3)使能AUTOACK EN_AA
4)使能PIPE 0EN_RXADDR
5)配置自动重发次数 SETUP_RETR
6)选择通信频率RF_CH
7 ) 选择通道0 有效数据宽度Rx_Pw_P0
8)配置发射参数(低噪放大器增益、发射功率、无线速率)RF_SETUP
9)配置24L01 的基本参数以及切换工作模式 CONFIG

通过上面很容易发现,发送模式和接收模式的配置很相似,只有几句有区别(红色标出)。所以我要做的就是能省事就省事,有些寄存器操作一次即可,没必要发送模式(Tx)和接收模式(Rx)相互切换的时候重新配置相同的步骤。所以我便封装了一个nRF24L01的模式初始化函数。

//该函数初始化NRF24L01
void NRF24L01_Init(void)
{  
	NRF24L01_CE = 0;	
	NRF24L01_Write_Buf(NRF_WRITE_REG+RX_ADDR_P0, (u8*)RX_ADDRESS, RX_ADR_WIDTH);	//写RX节点地址
	NRF24L01_Write_Buf(NRF_WRITE_REG+TX_ADDR, (u8*)TX_ADDRESS,TX_ADR_WIDTH);		//写TX节点地址
	NRF24L01_Write_Reg(NRF_WRITE_REG+EN_AA, 0x00);									//关闭通道0的自动应答    
	NRF24L01_Write_Reg(NRF_WRITE_REG+EN_RXADDR, 0x01);								//使能通道0的接收地址 
	NRF24L01_Write_Reg(NRF_WRITE_REG+SETUP_RETR, 0x00);								//不重发
	NRF24L01_Write_Reg(NRF_WRITE_REG+SETUP_AW, 0x02);								//地址宽度4
	NRF24L01_Write_Reg(NRF_WRITE_REG+RF_CH, CHANAL);	  							//设置RF通信频率			    
	NRF24L01_Write_Reg(NRF_WRITE_REG+RF_SETUP, 0x0f);								//设置TX发射参数,0db增益,2Mbps,低噪声增益开启
	NRF24L01_Write_Reg(NRF_WRITE_REG+DYNPD, 0x01);	    							//使能动态长度0
	NRF24L01_Write_Reg(NRF_WRITE_REG+FEATURE, 0x04);	 							//使能动态长度
	NRF24L01_Write_Reg(NRF_WRITE_REG+STATUS, 0xff); 								//清除状态标志
	NRF24L01_RX_Mode();																//初始化为接收模式
}

相关寄存器配置在前面头文件中的宏定义中也有解释,下面我还是详细列出使用到的寄存器及其相应位功能。由于个人对照数据手册翻译,不一定翻译正确,但不影响理解和使用。
注意:两个比较重要的寄存器(状态寄存器配置寄存器)在前面已经介绍。更具体请自行查看数据手册。

寄存器地址:0x01       名称:EN_AA(自动应答功能寄存器)

第7~6位(Reserved)第5位(ENAA_P5)第4位(ENAA_P4)第3位(ENAA_P3)第2位(ENAA_P2)第1位(ENAA_P1)第0位(ENAA_P0)
保留(未用),仅允许’00’数据通道5自动应答允许。
‘1’ :允许;
‘0’ :不允许
数据通道4自动应答允许。
‘1’ :允许;
‘0’ :不允许
数据通道3自动应答允许。
‘1’ :允许;
‘0’ :不允许
数据通道2自动应答允许。
‘1’ :允许;
‘0’ :不允许
数据通道1自动应答允许。
‘1’ :允许;
‘0’ :不允许
数据通道0自动应答允许。
‘1’ :允许;
‘0’ :不允许

寄存器地址:0x02       名称:EN_RXADDR(接收地址允许寄存器)

第7~6位(Reserved)第5位(ERX_P5)第4位(ERX_P4)第3位(ERX_P3)第2位(ERX_P2)第1位(ERX_P1)第0位(ERX_P0)
保留(未用),仅允许’00’接收数据通道5允许。
‘1’ :允许;
‘0’ :不允许
接收数据通道4允许。
‘1’ :允许;
‘0’ :不允许
接收数据通道3允许。
‘1’ :允许;
‘0’ :不允许
接收数据通道2允许。
‘1’ :允许;
‘0’ :不允许
接收数据通道1允许。
‘1’ :允许;
‘0’ :不允许
接收数据通道0允许。
‘1’ :允许;
‘0’ :不允许

寄存器地址:0x03       名称:SETUP_AW(地址宽度设置寄存器)

第7~2位(Reserved)第1~0位(AW)
保留(未用),仅允许’000000’接收/发射地址宽度设置位。
‘00’:无效;         
‘01’:3字节宽度;
‘10’:4字节宽度;
‘11’:5字节宽度   

寄存器地址:0x04       名称:SETUP_RETR(自动重发配置寄存器)

第7~4位(ARD)第3~0位(ARC)
自动重发延时设置位。
‘0000’:等待250+86us;
‘0001’:等待500+86us:
‘0010’:等待750+86us;
……
‘1111’:等待4000+86us;
(延时时间是指一包数据发送完成到下一包数据开始发射之间的时间间隔)
自动重发数值设置位。
‘0000’:禁止自动重发;
‘0001’:自动重发 1 次;
……
‘1111’:自动重发15次

寄存器地址:0x05       名称:RF_CH(工作频率设置寄存器)

第7位(Reserved)第6~0位(RF_CH)
保留(未用),仅允许’0’7位值可以设置,128个频段,实际是125个频段,可以任意设定但是接收与发送必须一致。

寄存器地址:0x06       名称:RF_SETUP(发射参数设置寄存器)

第7~5位(Reserved)第4位(PLL_LOCK)第3位(PLL_LOCK)第2~1位(RF_PWR)第0位(LNA_HCURR)
保留(未用),仅允许’000’PLL_LOCK允许,仅应用于测试模式,默认‘0’数据传输速率设置位。
‘0’:1Mbps
‘1’:2Mbps
发射功率设置。
‘00’:-18dBm
‘01’:-12dBm
‘10’:-6dBm  
‘11’:0dBm   
低噪声放大增益,默认‘1’

寄存器地址:0x1C       名称:DYNPD(动态长度允许寄存器)

第7~6位(Reserved)第5位(DPL_P5)第4位(DPL_P4)第3位(DPL_P3)第2位(DPL_P2)第1位(DPL_P1)第0位(DPL_P0)
保留(未用),仅允许’00’动态有效数据长度数据通道5允许位
’1’ :允许;
‘0’ :不允许
动态有效数据长度数据通道4允许位
’1’ :允许;
‘0’ :不允许
动态有效数据长度数据通道3允许位
’1’ :允许;
‘0’ :不允许
动态有效数据长度数据通道2允许位
’1’ :允许;
‘0’ :不允许
动态有效数据长度数据通道1允许位
’1’ :允许;
‘0’ :不允许
动态有效数据长度数据通道0允许位
’1’ :允许;
‘0’ :不允许

寄存器地址:0x1D       名称:FEATURE(功能寄存器)

第7~3位(Reserved)第2位(EN_DPL)第1位(EN_ACK_PAY)第0位(EN_DYN_ACK)
保留(未用),仅允许’00000’动态有效数据长度允许位
’1’ :允许;
‘0’ :不允许
使能ACK有效数据
’1’ :允许;
‘0’:不允许
使能’W_TX_PAYLOAD_NOACK’命令
’1’ :允许;
‘0’:不允许

至此,nRF24L01的初始化函数已完成。聪明的人应该会问为什么初始化完成没有拉高CE,其实并不是没有拉高CE,可以看到最后一句 NRF24L01_RX_Mode(); 初始化为接收模式,配置完相应的模式后就会拉高CE,这个函数后面会介绍。nRF24L01模块默认初始化为接收模式,当要发送数据的时候再转为发送模式。上述代码已经写得很详细了,学习过程中只需要对照流程把相应的寄存器配置好即可。

5、接收模式配置

对照“Rx模式初始化过程”,可以发现只剩下步骤“配置24L01 的基本参数以及切换工作模式”。在待机模式下,若PRIM_RX = 1且CE = 1,会进入接收模式,接收数据该步骤主要就是先清空 RX FIFO 寄存器,再配置CONFIG寄存器,简简单单,直接上代码。

//该函数初始化NRF24L01到RX模式
//当CE变高后,即进入RX模式,并可以接收数据了		   
void NRF24L01_RX_Mode(void)
{
	NRF24L01_CE = 0;
	NRF24L01_Write_Reg(FLUSH_RX, 0xff);				//清接收FIFO
	NRF24L01_Write_Reg(NRF_WRITE_REG+CONFIG, 0x0f);	//配置基本工作模式的参数;PWR_UP,EN_CRC,16BIT_CRC,接收模式 
	NRF24L01_CE = 1; 								//CE为高,进入接收模式 
}
  • FLUSH_RX:清除 RX FIFO 寄存器,应用于接收模式下。写0XFF清除。在传输应答信号过程中不应执行此指令。也就是说,若传输应答信号过程中执行此指令的话将使得应答信号不能被完整的传输。
  • 在待机模式下,若PRIM_RX = 1且CE = 1,会进入接收模式,接收数据。PRIM_RX 位在CONFIG(配置寄存器)的第0位中配置。详细回到前面查看配置寄存器
  • 拉低CE是为了切换工作模式。如果为发送模式或待机模式II,此时CE为高,只有拉低才能对寄存器进行操作,配置完再拉高。

6、发送模式配置

在待机模式下,若PRIM_RX = 0、CE = 1且TX FIFO寄存器非空,会进入发射模式,发送数据。根据前文“Tx模式初始化过程”,和接收模式类似,我们需要清空 TX FIFO 寄存器,再配置CONFIG寄存器。需要注意的是,配置发送模式必须写一次发送地址

//该函数初始化NRF24L01到TX模式		   
//CE为高大于10us,则启动发送.	 
void NRF24L01_TX_Mode(void)
{
	NRF24L01_CE = 0;
	NRF24L01_Write_Buf(NRF_WRITE_REG+TX_ADDR, (u8*)TX_ADDRESS,TX_ADR_WIDTH);//写TX节点地址
	NRF24L01_Write_Reg(FLUSH_TX, 0xff);										//清发送FIFO	
    NRF24L01_Write_Reg(NRF_WRITE_REG+CONFIG, 0x0e);		//配置基本工作模式的参数;PWR_UP,EN_CRC,16BIT_CRC,接收模式,开启所有中断
}
  • FLUSH_TX:清除 TX FIFO 寄存器,应用于发射模式下。写0XFF清除。
  • 在待机模式下,若PRIM_RX = 0、CE = 1且TX FIFO寄存器非空,会进入发射模式,发送数据。PRIM_RX 位在CONFIG(配置寄存器)的第0位中配置。详细回到前面查看配置寄存器
  • 拉低CE是为了切换工作模式。如果为接收模式,此时CE为高,只有拉低才能对寄存器进行操作,配置完并且等发送数据的长度以字节计数从 MCU 写入TX FIFO 寄存器后再拉高10us就会进入发送模式。

7、双向通信

双向通信的方法:A、B两个nRF24L01模块默认都处于接收模式下,当A模块要发送数据的时候就切换为发送模式,当发完数据就马上切换为接收模式。B模块的工作方式也一样,这样就能实现双向通信。
一种特殊情况:如果两个模块都处于发送模式,这样就没有接收端了。这种现象我们可以称为碰撞现象,其实就是数据丢失了,两个端都不会接收到数据。不过由于在空中传输时间很短,如果不是两个模块同时频繁发送数据,一般情况下不会出现。不过作死就不一样了,你硬要设置两个模块频繁同时发送,我悄悄测试了两个模块每间隔1ms同时发送数据,确实会出现碰撞现象。
两种不完全解决办法:(因为不管怎样都有可能会发生,只能尽量避免,不能完全解决)
(一)本文介绍的是采用ShockBurst 模式,你可以配置成增强型ShockBurst 模式,好处就是可以重发丢失数据包和产生应答信号。此模式下由于空中传输时间很短,极大的降低了无线传输中的碰撞现象。配置方法就是将前文nRF24L01初始化配置部分的配置参数修改一下,使能自应答,设置重发次数。虽然这种模式下还是挺容易发生(这里的容易还是作死的情况下测试的)碰撞现象。
(二)采用检测信号通道占用+延时发送的形式。具体可自行了解,这里不再讲解。
nRF24L01+如何检测信道被占用-RSSI寄存器实战分享
nRF24L01+组网方式及防撞(防冲突)机制的实战分享

8、检测nRF24L01连接状态

完成了模式的配置,现已经可以在接收模式(Rx)和发送模式(Tx)之间进行切换了。不过为了方便检查nRF24L01有没有跟STM32正常连接,还是写个检测函数比较好。

//检测24L01是否存在
//返回值:0,成功;1,失败	
u8 NRF24L01_Check(void)
{
	u8 buf[5] = {0XA5,0XA5,0XA5,0XA5,0XA5};
	u8 buf1[5]=	{0};
	u8 i;
	NRF24L01_Write_Buf(NRF_WRITE_REG+TX_ADDR, buf, 5);	//写入5个字节的地址
	NRF24L01_Read_Buf(TX_ADDR,buf1, 5); 				//读出写入的地址
		for(i=0; i<5; i++) if(buf1[i] != 0XA5) break;	//判断读出地址是否是写入地址
	if(i != 5) return 1;								//检测24L01错误	
	return 0;		 									//检测到24L01
}

9、nRF24L01发送数据

模式都已配置完成,万事具备,只欠发送和接收函数。发送函数很简单,拉低CE—通过SPI向nRF24L01的发送数据寄存器写数据—切换到发送模式(拉高CE10us)。当发送完数据后,此时状态寄存器的 TX_DS 位置高,IRQ 引脚产生中断。

//发送数据
//*data_buffer:数据指针
//Nb_bytes:数据长度
void NRF_Send_Data(u8 *data_buffer, u8 Nb_bytes)
{
	NRF24L01_CE = 0;
	NRF24L01_TX_Mode();										//切换至发送模式
  	NRF24L01_Write_Buf(WR_TX_PLOAD, data_buffer, Nb_bytes);	//向发送寄存器写数据
  	NRF24L01_CE = 1;
	delay_us(10);											//延时10us
}

要注意的是,如果 CE 置低,则系统进入待机模式 I。如果不设置 CE 为低,则系统会发送 TX FIFO 寄存器中下一包数据。如果 TX FIFO 寄存器为空并且 CE 为高则系统进入待机模式 II 。如果系统在待机模式 II,当 CE 置低后系统立即进入待机模式 I。

10、nRF24L01接收数据

nRF24L01处在接收模式下,会检测空中信息,接收到有效的数据包(地址匹配、CRC 检验正确),数据存储在 RX_FIFO 中,同时状态寄存器的 RX_DR 位置高,并产生中断,随后 MCU 可将接收到的数据从 RX FIFO 寄存器中读出。

读取接收数据步骤:(因为是动态长度数据,需先得数据长度)
1. 读R_RX_PL_WID寄存器得到接收到的数据长度rf_len;
2. 通过SPI读取RD_RX_PLOAD寄存器,把接收到的数据存的数组rf_rxbuf中。

	rf_len = NRF24L01_Read_Reg(R_RX_PL_WID);			//读寄存器得到接收到的数据长度
	NRF24L01_Read_Buf(RD_RX_PLOAD, rf_rxbuf, rf_len);	//把接收到的数据存的数组rf_rxbuf中

因为接收数据是MCU在外部中断(由IRQ 引脚产生的中断)中读取的,两条代码所以就不封装成函数了,产生接收中断的时候就这样使用来读取接收数据。

11、外部中断6配置(IRQ中断)

因为使用的是中断方式,所以还需要配置外部中断,我这里使用的是外部中断6
nRF24L01 的中断引脚(IRQ)为低电平触发,当状态寄存器中TX_DS(数据发送完成中断位)、RX_DR(接收数据中断位) 或MAX_RT(达到最多次重发中断位)为高时触发中断。当MCU 给中断源写‘1’时,中断引脚被禁止。可屏蔽中断可以被IRQ 中断屏蔽。通过设置可屏蔽中断位为高,则中断响应被禁止。默认状态下所有的中断源是被禁止的。


u8 rf_rxbuf[RX_PLOAD_WIDTH];	//接收数据组(32位)
u8 rf_flag;						//接收标志
u8 rf_len;						//接收长度

void EXTIX_Init(void)
{ 
	EXTI_InitTypeDef EXTI_InitStructure;
	NVIC_InitTypeDef NVIC_InitStructure;

	RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);	//使能复用功能时钟

	//nRF24L01 NVIC配置
	NVIC_InitStructure.NVIC_IRQChannel=EXTI9_5_IRQn;		//打开EXTI9_5的全局中断
	NVIC_InitStructure.NVIC_IRQChannelSubPriority=3;		//响应优先级为3
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=3;	//抢占优先级为3
	NVIC_InitStructure.NVIC_IRQChannelCmd=ENABLE;			//使能
	NVIC_Init(&NVIC_InitStructure);							//NVIC初始化
	
	//将GPIO管脚与外部中断线连接
	GPIO_EXTILineConfig(GPIO_PortSourceGPIOC,GPIO_PinSource6);//选择GPIOC_Pin6管脚用作外部中断线路
	
	//nRF24L01 EXIT配置
	EXTI_InitStructure.EXTI_Line=EXTI_Line6;				//外部中断通道6
	EXTI_InitStructure.EXTI_Mode=EXTI_Mode_Interrupt;		//中断模式
	EXTI_InitStructure.EXTI_Trigger=EXTI_Trigger_Falling;	//下降沿触发
	EXTI_InitStructure.EXTI_LineCmd=ENABLE;					//使能
	EXTI_Init(&EXTI_InitStructure);							//EXTI初始化
	EXTI_ClearITPendingBit(EXTI_Line6);						//清除中断线6
}

//外部中断6服务程序 
void EXTI9_5_IRQHandler(void)
{
	u8 status;//,i;
	//判断是否是线路6引起的中断
	if(EXTI_GetITStatus(EXTI_Line6) != RESET)					//RF有中断了           
	{
		if(GPIO_ReadInputDataBit(GPIOC, GPIO_Pin_6) == 0)
		{ 
		  status = NRF24L01_Read_Reg(NRF_READ_REG+STATUS);		//读状态寄存器完了判断是哪种中断状态           
	      if(status & 0x40)				//bit6:数据接收中断                             
	      {
			rf_len = NRF24L01_Read_Reg(R_RX_PL_WID);			//读寄存器得到接收到的数据长度
	        NRF24L01_Read_Buf(RD_RX_PLOAD, rf_rxbuf, rf_len);	//把接收到的数据存的数组rf_rxbuf中  
	        rf_flag = 1;				//RF接收标志位置1
	        for(i=0; i<rf_len; i++)
			{
				while(USART_GetFlagStatus(USART1,USART_FLAG_TC) != SET);
				USART_SendData(USART1, rf_rxbuf[i]);
			}
			//printf("%s\r\n",rf_rxbuf);	//串口1输出接收数据
			rf_len = 0;					//接收长度清零
			rf_flag = 0;				//接收标志位清零
	      }
	      else if(status &0x20)			//TX发送完成中断
		  {
	        NRF24L01_RX_Mode();			//转为接收模式                                                 
	      }
	      NRF24L01_Write_Reg(NRF_WRITE_REG+STATUS, status);		//清除状态标志        
	    }        
	    EXTI_ClearITPendingBit(EXTI_Line6);                   	//清除中断线6
  } 
}

需要注意的是,这里我没有开启MAX_RT中断位,所以没写相应的处理代码;对于TX_DS(数据发送完成中断位),发送完数据后我们需要转入接收模式,以保证能够双向通信。而对于printf(); 其实是串口1输出,已经重定义过了,不懂没关系,百度一下,你就知道,当然后面我也会给出。由于我在测试的时候发现用printf("%s\r\n",rf_rxbuf);输出时,第一次输出的数据会出现数据输出不完整,所以我便用了个循环让串口1输出。至此,从nRF24L01接收到数据传输给MCU再由串口1输出这个传输方向已完成。

12、串口1(USART1)配置

串口USART的配置也是初学STM32必须的,所以这里没必要讲太多。先给出printf重定义部分代码。两种方式,一种是不需要选择“Use MicroLIB”,一种是选择使用“Use MicroLIB”。“Use MicroLIB”选项在“魔术棒”—target里选择。

// 	 
//如果使用ucos,则包括下面的头文件即可.
#if SYSTEM_SUPPORT_OS
#include "includes.h"					//ucos 使用	  
#endif
//
//加入以下代码,支持printf函数,而不需要选择use MicroLIB	  
#if 1
#pragma import(__use_no_semihosting)             
//标准库需要的支持函数                 
struct __FILE 
{ 
	int handle; 

}; 

FILE __stdout;       
//定义_sys_exit()以避免使用半主机模式    
_sys_exit(int x) 
{ 
	x = x; 
} 
//重定义fputc函数 
int fputc(int ch, FILE *f)
{      
	while((USART1->SR&0X40)==0);//循环发送,直到发送完毕   
    USART1->DR = (u8) ch;      
	return ch;
}
#endif 

/*使用microLib的方法*/
 /* 
int fputc(int ch, FILE *f)
{
	USART_SendData(USART1, (uint8_t) ch);

	while (USART_GetFlagStatus(USART1, USART_FLAG_TC) == RESET) {}	
   
    return ch;
}
int GetKey (void)  { 

    while (!(USART1->SR & USART_FLAG_RXNE));

    return ((int)(USART1->DR & 0x1FF));
}
*/

串口1这里我先定义了3个u8变量,和外部中断一样。要注意的是我声明了使用外部函数extern void NRF_Send_Data(u8 *data_buffer, u8 Nb_bytes);。因为不是USART1.c里的函数,所以要进行声明。

u8 rxd1_buf[32];			//接收数组
u8 rxd1_len;				//接收数据长度
u8 rxd1_flag;				//接收标志

extern void NRF_Send_Data(u8	*data_buffer, u8 Nb_bytes);		//发送数据

//串口1初始化
void uart_init(u32 bound)
{
  	//GPIO端口设置
	GPIO_InitTypeDef GPIO_InitStructure;
	USART_InitTypeDef USART_InitStructure;
	NVIC_InitTypeDef NVIC_InitStructure;
	 
	RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);		//使能USART1,GPIOA时钟
		
	//USART1_TX   GPIOA.9
	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; 											//PA.9
	GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
 	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;										//复用推挽输出
  	GPIO_Init(GPIOA, &GPIO_InitStructure);												//初始化GPIOA.9
   
  	//USART1_RX	  GPIOA.10初始化
  	GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10;											//PA10
  	GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING;								//浮空输入
  	GPIO_Init(GPIOA, &GPIO_InitStructure);												//初始化GPIOA.10  

  	//Usart1 NVIC 配置
 	NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn;
	NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3;							//抢占优先级3
	NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2;									//子优先级2
	NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;										//IRQ通道使能
	NVIC_Init(&NVIC_InitStructure);														//根据指定的参数初始化VIC寄存器
  
   	//USART 初始化设置
	USART_InitStructure.USART_BaudRate = bound;											//串口波特率
	USART_InitStructure.USART_WordLength = USART_WordLength_8b;							//字长为8位数据格式
	USART_InitStructure.USART_StopBits = USART_StopBits_1;								//一个停止位
	USART_InitStructure.USART_Parity = USART_Parity_No;									//无奇偶校验位
	USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;		//无硬件数据流控制
	USART_InitStructure.USART_Mode = USART_Mode_Rx | USART_Mode_Tx;						//收发模式

  	USART_Init(USART1, &USART_InitStructure); 											//初始化串口1
 	USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);										//开启串口接受中断
	USART_ITConfig(USART1, USART_IT_IDLE, ENABLE);										//开启检测串口空闲状态中断
	USART_Cmd(USART1, ENABLE);                    										//使能串口1 
}

//串口1中断服务程序
void USART1_IRQHandler(void)                	
{
	u8 clear = clear;	//这样定义避免弹出clear定义了没使用的警告
	if(USART_GetITStatus(USART1, USART_IT_RXNE) != RESET)		//接收中断
	{
		rxd1_buf[rxd1_len++] = USART1->DR;						//把接收到的数据存到rxd1_buf数组
  }
	else if(USART_GetITStatus(USART1, USART_IT_IDLE) != RESET)	//串口线空闲了说明一包数据接收完了
	{
		clear = USART1->SR;										//读寄存器就等于清寄存器
		clear = USART1->DR;										//读寄存器就等于清寄存器
		rxd1_flag = 1;											//串口接收标志位置1
		NRF_Send_Data(rxd1_buf, rxd1_len);						//NRF24L01发送数据
		rxd1_len = 0;											//接收数据长度置0
		rxd1_flag = 0;											//串口接收标志位置0
	}
	USART_ClearITPendingBit(USART1,USART_IT_ORE);				//清除USART1_ORE标志位
}

至此,从串口1接收到数据传输给MCU再由nRF24L01发送这个传输方向已完成。整个串口透传传输也全部完成。

13、延时函数

延时函数不是我主要介绍的内容,但却是很常用,怕有些人不知道怎么写或者怎么实现较准确的延时,我这里便提供正点原子的代码。

(1)delay.c

#include "delay.h"
// 	 
//如果需要使用OS,则包括下面的头文件即可.
#if SYSTEM_SUPPORT_OS
#include "includes.h"					//ucos 使用	  
#endif
/

static u8  fac_us=0;							//us延时倍乘数			   
static u16 fac_ms=0;							//ms延时倍乘数,在ucos下,代表每个节拍的ms数
	
#if SYSTEM_SUPPORT_OS							//如果SYSTEM_SUPPORT_OS定义了,说明要支持OS了(不限于UCOS).
//当delay_us/delay_ms需要支持OS的时候需要三个与OS相关的宏定义和函数来支持
//首先是3个宏定义:
//    delay_osrunning:用于表示OS当前是否正在运行,以决定是否可以使用相关函数
//delay_ostickspersec:用于表示OS设定的时钟节拍,delay_init将根据这个参数来初始哈systick
// delay_osintnesting:用于表示OS中断嵌套级别,因为中断里面不可以调度,delay_ms使用该参数来决定如何运行
//然后是3个函数:
//  delay_osschedlock:用于锁定OS任务调度,禁止调度
//delay_osschedunlock:用于解锁OS任务调度,重新开启调度
//    delay_ostimedly:用于OS延时,可以引起任务调度.

//本例程仅作UCOSII和UCOSIII的支持,其他OS,请自行参考着移植
//支持UCOSII
#ifdef 	OS_CRITICAL_METHOD						//OS_CRITICAL_METHOD定义了,说明要支持UCOSII				
#define delay_osrunning		OSRunning			//OS是否运行标记,0,不运行;1,在运行
#define delay_ostickspersec	OS_TICKS_PER_SEC	//OS时钟节拍,即每秒调度次数
#define delay_osintnesting 	OSIntNesting		//中断嵌套级别,即中断嵌套次数
#endif

//支持UCOSIII
#ifdef 	CPU_CFG_CRITICAL_METHOD					//CPU_CFG_CRITICAL_METHOD定义了,说明要支持UCOSIII	
#define delay_osrunning		OSRunning			//OS是否运行标记,0,不运行;1,在运行
#define delay_ostickspersec	OSCfg_TickRate_Hz	//OS时钟节拍,即每秒调度次数
#define delay_osintnesting 	OSIntNestingCtr		//中断嵌套级别,即中断嵌套次数
#endif


//us级延时时,关闭任务调度(防止打断us级延迟)
void delay_osschedlock(void)
{
#ifdef CPU_CFG_CRITICAL_METHOD   				//使用UCOSIII
	OS_ERR err; 
	OSSchedLock(&err);							//UCOSIII的方式,禁止调度,防止打断us延时
#else											//否则UCOSII
	OSSchedLock();								//UCOSII的方式,禁止调度,防止打断us延时
#endif
}

//us级延时时,恢复任务调度
void delay_osschedunlock(void)
{	
#ifdef CPU_CFG_CRITICAL_METHOD   				//使用UCOSIII
	OS_ERR err; 
	OSSchedUnlock(&err);						//UCOSIII的方式,恢复调度
#else											//否则UCOSII
	OSSchedUnlock();							//UCOSII的方式,恢复调度
#endif
}

//调用OS自带的延时函数延时
//ticks:延时的节拍数
void delay_ostimedly(u32 ticks)
{
#ifdef CPU_CFG_CRITICAL_METHOD
	OS_ERR err; 
	OSTimeDly(ticks,OS_OPT_TIME_PERIODIC,&err);	//UCOSIII延时采用周期模式
#else
	OSTimeDly(ticks);							//UCOSII延时
#endif 
}
 
//systick中断服务函数,使用ucos时用到
void SysTick_Handler(void)
{	
	if(delay_osrunning==1)						//OS开始跑了,才执行正常的调度处理
	{
		OSIntEnter();							//进入中断
		OSTimeTick();       					//调用ucos的时钟服务程序               
		OSIntExit();       	 					//触发任务切换软中断
	}
}
#endif

			   
//初始化延迟函数
//当使用OS的时候,此函数会初始化OS的时钟节拍
//SYSTICK的时钟固定为HCLK时钟的1/8
//SYSCLK:系统时钟
void delay_init()
{
#if SYSTEM_SUPPORT_OS  							//如果需要支持OS.
	u32 reload;
#endif
	SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8);	//选择外部时钟  HCLK/8
	fac_us=SystemCoreClock/8000000;				//为系统时钟的1/8  
#if SYSTEM_SUPPORT_OS  							//如果需要支持OS.
	reload=SystemCoreClock/8000000;				//每秒钟的计数次数 单位为K	   
	reload*=1000000/delay_ostickspersec;		//根据delay_ostickspersec设定溢出时间
												//reload为24位寄存器,最大值:16777216,在72M下,约合1.86s左右	
	fac_ms=1000/delay_ostickspersec;			//代表OS可以延时的最少单位	   

	SysTick->CTRL|=SysTick_CTRL_TICKINT_Msk;   	//开启SYSTICK中断
	SysTick->LOAD=reload; 						//每1/delay_ostickspersec秒中断一次	
	SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk;   	//开启SYSTICK    

#else
	fac_ms=(u16)fac_us*1000;					//非OS下,代表每个ms需要的systick时钟数   
#endif
}								    

#if SYSTEM_SUPPORT_OS  							//如果需要支持OS.
//延时nus
//nus为要延时的us数.		    								   
void delay_us(u32 nus)
{		
	u32 ticks;
	u32 told,tnow,tcnt=0;
	u32 reload=SysTick->LOAD;					//LOAD的值	    	 
	ticks=nus*fac_us; 							//需要的节拍数	  		 
	tcnt=0;
	delay_osschedlock();						//阻止OS调度,防止打断us延时
	told=SysTick->VAL;        					//刚进入时的计数器值
	while(1)
	{
		tnow=SysTick->VAL;	
		if(tnow!=told)
		{	    
			if(tnow<told)tcnt+=told-tnow;		//这里注意一下SYSTICK是一个递减的计数器就可以了.
			else tcnt+=reload-tnow+told;	    
			told=tnow;
			if(tcnt>=ticks)break;				//时间超过/等于要延迟的时间,则退出.
		}  
	};
	delay_osschedunlock();						//恢复OS调度									    
}
//延时nms
//nms:要延时的ms数
void delay_ms(u16 nms)
{	
	if(delay_osrunning&&delay_osintnesting==0)	//如果OS已经在跑了,并且不是在中断里面(中断里面不能任务调度)	    
	{		 
		if(nms>=fac_ms)							//延时的时间大于OS的最少时间周期 
		{ 
   			delay_ostimedly(nms/fac_ms);		//OS延时
		}
		nms%=fac_ms;							//OS已经无法提供这么小的延时了,采用普通方式延时    
	}
	delay_us((u32)(nms*1000));					//普通方式延时  
}
#else //不用OS时
//延时nus
//nus为要延时的us数.		    								   
void delay_us(u32 nus)
{		
	u32 temp;	    	 
	SysTick->LOAD=nus*fac_us; 					//时间加载	  		 
	SysTick->VAL=0x00;        					//清空计数器
	SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk ;	//开始倒数	  
	do
	{
		temp=SysTick->CTRL;
	}while((temp&0x01)&&!(temp&(1<<16)));		//等待时间到达   
	SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk;	//关闭计数器
	SysTick->VAL =0X00;      					 //清空计数器	 
}
//延时nms
//注意nms的范围
//SysTick->LOAD为24位寄存器,所以,最大延时为:
//nms<=0xffffff*8*1000/SYSCLK
//SYSCLK单位为Hz,nms单位为ms
//对72M条件下,nms<=1864 
void delay_ms(u16 nms)
{	 		  	  
	u32 temp;		   
	SysTick->LOAD=(u32)nms*fac_ms;				//时间加载(SysTick->LOAD为24bit)
	SysTick->VAL =0x00;							//清空计数器
	SysTick->CTRL|=SysTick_CTRL_ENABLE_Msk ;	//开始倒数  
	do
	{
		temp=SysTick->CTRL;
	}while((temp&0x01)&&!(temp&(1<<16)));		//等待时间到达   
	SysTick->CTRL&=~SysTick_CTRL_ENABLE_Msk;	//关闭计数器
	SysTick->VAL =0X00;       					//清空计数器	  	    
} 
#endif 

(2)delay.h

#ifndef __DELAY_H
#define __DELAY_H 			   
#include "sys.h" 

void delay_init(void);
void delay_ms(u16 nms);
void delay_us(u32 nus);

#endif

五、主函数部分

前面的配置就是整个基于NRF24L01串口透传的流程,要提醒一下,我使用的中断比较多,所以一定要理清楚中断的抢占优先级和响应优先级,后期要是介绍如何实现DMA方式的时候会有更多的中断,所以头脑一定要清晰。主函数部分主要就是引入头文件,以及一些初始化。

int main(void)
{
	//u8 i;
	delay_init();	    	 		//延时函数初始化	  
	NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);//设置中断优先级分组为组2:2位抢占优先级,2位响应优先级
	uart_init(115200);	 			//串口初始化为115200
	NRF24L01_Init_IO();    			//初始化NRF24L01的IO
 	NRF24L01_Init();    			//初始化NRF24L01 
	EXTIX_Init();					//外部中断初始化
	while(NRF24L01_Check())			//如果检测不到2401串口就间隔1s打印提示信息
	{
		printf("NRF24L01 Error\r\n");
		delay_ms(1000);
	}	
	printf("NRF24L01 SUCCESS\r\n");
	
	while (1)
  	{
		/*
		if(rxd1_flag == 1) 
		{
			NRF_Send_Data(rxd1_buf, rxd1_len);
			rxd1_len = 0;
			rxd1_flag = 0;
		}
		if(rf_flag == 1)
		{
	   		for(i=0; i<rf_len; i++)
			{
				while(USART_GetFlagStatus(USART1,USART_FLAG_TC) != SET);
				USART_SendData(USART1, rf_rxbuf[i]);
			}
			rf_len = 0;
			rf_flag = 0;
		}
  		*/
	}
}

聪明人会发现,我在while循环里注释了一些代码,这些代码是用于查询方式的,查询方式下一定要注意回到串口接收中断外部中断6修改中断标志位

六、啰嗦一下

写了这么多,就是希望大家学习nRF24L01的时候能少走一些弯路,少跳进一些坑。本人也是第一次写博文,如有错误之处欢迎指出!也欢迎大家一起学习!本文介绍的我个人认为还是非常详细的,尽管有些代码是我在学习过程中复制其他人的代码,但是最后都是经过自己的理解和思考。学习的过程就是Ctrl+CCtrl+V,但是一定要转变成自己的知识,这也是我边给代码,边讲解配置和使用的初衷,所以我并不是像其他人一样直接上代码,结果只知道复制、粘贴,错误后不知道错在哪。最后说一下,如果有需要我可以上传代码给大家,如果本文效果好,后期我会写如何实现串口DMA—SPI DMA方式实现串口透传,相信大家百度nRF24L01 SPI DMA使用的时候查不到什么东西,对于nRF24L01如何实现SPI DMA传输也找不到什么文章,当然大佬请无视。

  • 95
    点赞
  • 259
    收藏
    觉得还不错? 一键收藏
  • 20
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 20
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值