完善资料让更多小伙伴认识你,还能领取20积分哦, 立即完善>
本帖最后由 正点原子运营官 于 2020-4-13 11:25 编辑
1)实验平台:正点原子STM32mini开发板 2)摘自《正点原子STM32 不完全手册(HAL 库版)》关注官方微信号公众号,获取更多资料:正点原子 第二十九章 无线通信实验 ALIENTKEMiniSTM32 开发板带有一个 2.4G 无线模块(NRF24L01 模块)通信接口,采用 8 脚插针方式与开发板连接。本章我们将以 NRF24L01 模块为例向大家介绍如何在 ALIENTEK MiniSTM32 开发板上实现无线通信。在本章中,我们将使用两块 MiniSTM32 开发板,一块用 于发送收据,另外一块用于接收,从而实现无线数据传输。本章分为如下几个部分: 29.1 NRF24L01 无线模块简介 29.2 硬件设计 29.3 软件设计 29.4 下载验证 29.1 NRF24L01 无线模块简介 NRF24L01 无线模块,采用的芯片是 NRF24L01,该芯片的主要特点如下: 1)2.4G 全球开放的 ISM 频段,免许可证使用。 2)最高工作速率 2Mbps,高校的 GFSK 调制,抗干扰能力强。 3)125 个可选的频道,满足多点通信和调频通信的需要。 4)内置 CRC 检错和点对多点的通信地址控制。 5)低工作电压(1.9~3.6V)。 6)可设置自动应答,确保数据可靠传输。 该芯片通过 SPI 与外部 MCU 通信,最大的 SPI 速度可以达到 10Mhz。本章我们用到的模块是深圳云佳科技生产的 NRF24L01,该模块已经被很多公司大量使用,成熟度和稳定性都是相当不错的。该模块的外形和引脚图如图 29.1.1 所示: 图 29.1.1 NRF24L01 无线模块外观引脚图 模块 VCC 脚的电压范围为 1.9~3.6V,建议不要超过 3.6V,否则可能烧坏模块,一般用 3.3V电压比较合适。除了 VCC 和 GND 脚,其他引脚都可以和 5V 单片机的 IO 口直连,正是因为其兼容 5V 单片机的 IO,故使用上具有很大优势。 关于 NRF24L01 的详细介绍,请参考 NRF24L01 的技术手册。 29.2 硬件设计 本章实验功能简介:开机的时候先检测 NRF24L01 模块是否存在,在检测到 NRF24L01 模块之后,根据 KEY0 和 KEY1 的设置来决定模块的工作模式,在设定好工作模式之后,就会 不停的发送/接收数据,同样用 DS0 来指示程序正在运行。 所要用到的硬件资源如下: 1) 指示灯 DS0 2) KEY0 和 KEY1 按键 3) TFTLCD 模块 4) NRF24L01 模块 NRF24L01 模块属于外部模块,这里我们仅介绍开发板上 NRF24L01 模块接口和 STM32的连接情况,他们的连接关系如图 29.2.1 所示: 图 29.2.1 NRF24L01 模块接口与 STM32 连接原理图 这里NRF24L01也是使用的SPI1,和W25Q64以及SD卡等共用一个SPI接口,所以在使用 的时候,他们分时复用SPI1。本章我们需要把SD卡和W25Q64的片选信号置高,以防止这两个器件对NRF24L01的通信造成干扰。 由于无线通信实验是双向的,所以至少要有两个模块同时工作才可以,这里我们使用2套 ALIENTEK MiniSTM32开发板来向大家演示。 29.3 软件设计 打开本章实验工程可以看到,我们在工程中添加了 spi 底层驱动函数,因为 NRF24L01 是 SPI 通信接口。同时,我们增加了 24l01.c 源文件以及包含了对应的头文件用来编写 NRF24L01底层驱动函数。 打开 24l01.c 文件,代码如下: const u8 TX_ADDRESS[TX_ADR_WIDTH]={0x34,0x43,0x10,0x10,0x01}; //发送地址 const u8 RX_ADDRESS[RX_ADR_WIDTH]={0x34,0x43,0x10,0x10,0x01}; //发送地址 //针对 NRF24L01 修改 SPI1 驱动 void NRF24L01_SPI_Init(void) { __HAL_SPI_DISABLE(&SPI1_Handler); //先关闭 SPI1 SPI1_Handler.Init.CLKPolarity=SPI_POLARITY_LOW; //串行同步时钟的空闲状态为低电平 SPI1_Handler.Init.CLKPhase=SPI_PHASE_1EDGE; //串行同步时钟的第 1 个跳变沿(上升或下降)数据被采样 HAL_SPI_Init(&SPI1_Handler); __HAL_SPI_ENABLE(&SPI1_Handler); //使能 SPI1 } //初始化 24L01 的 IO 口 void NRF24L01_Init(void) { GPIO_InitTypeDef GPIO_Initure; __HAL_RCC_GPIOA_CLK_ENABLE(); //开启 GPIOA 时钟 __HAL_RCC_GPIOC_CLK_ENABLE(); //开启 GPIOC 时钟 //PA2,3,4 初始化设置:推挽输出 GPIO_Initure.Pin=GPIO_PIN_2|GPIO_PIN_3|GPIO_PIN_4; GPIO_Initure.Mode=GPIO_MODE_OUTPUT_PP; //推挽输出 GPIO_Initure.Pull=GPIO_PULLUP; //上拉 GPIO_Initure.Speed=GPIO_SPEED_FREQ_HIGH;//高速 HAL_GPIO_Init(GPIOA,&GPIO_Initure); //初始化 //PC4 推挽输出 GPIO_Initure.Pin=GPIO_PIN_4; //PC4 HAL_GPIO_Init(GPIOC,&GPIO_Initure); //初始化 //PA1 上拉输入 GPIO_Initure.Pin=GPIO_PIN_1; //PA1 GPIO_Initure.Mode=GPIO_MODE_INPUT; //输入 HAL_GPIO_Init(GPIOA,&GPIO_Initure); //初始化 HAL_GPIO_WritePin(GPIOA,GPIO_PIN_1,GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOA,GPIO_PIN_2,GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOA,GPIO_PIN_3,GPIO_PIN_SET); HAL_GPIO_WritePin(GPIOA,GPIO_PIN_4,GPIO_PIN_SET); SPI1_Init(); //初始化 SPI1 NRF24L01_SPI_Init(); //针对 NRF 的特点修改 SPI 的设置 NRF24L01_CE=0; //使能 24L01 NRF24L01_CSN=1; //SPI 片选取消 } //检测 24L01 是否存在 //返回值:0,成功;1,失败 u8 NRF24L01_Check(void) { u8 buf[5]={0XA5,0XA5,0XA5,0XA5,0XA5}; u8 i; SPI2_SetSpeed(SPI_BAUDRATEPRESCALER_8); //spi 速度为 10.5Mhz((24L01 的最大 SPI 时钟为 10Mhz,这里大一点没关系) NRF24L01_Write_Buf(NRF_WRITE_REG+TX_ADDR,buf,5);//写入 5 个字节的地址. NRF24L01_Read_Buf(TX_ADDR,buf,5); //读出写入的地址 for(i=0;i<5;i++)if(buf!=0XA5)break; if(i!=5)return 1;//检测 24L01 错误 return 0; //检测到 24L01 } //SPI 写寄存器 //reg:指定寄存器地址 //value:写入的值 u8 NRF24L01_Write_Reg(u8 reg,u8 value) { u8 status; 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; 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; NRF24L01_CSN=0; //使能 SPI 传输 status=SPI2_ReadWriteByte(reg); //发送寄存器值(位置),并读取状态值 for(u8_ctr=0;u8_ctr //关闭 SPI 传输 return status; //返回读到的状态值 } //在指定位置写指定长度的数据 //reg:寄存器(位置) //*pBuf:数据指针 //len:数据长度 //返回值,此次读到的状态寄存器值 u8 NRF24L01_Write_Buf(u8 reg, u8 *pBuf, u8 len) { u8 status,u8_ctr; NRF24L01_CSN=0; //使能 SPI 传输 status = SPI2_ReadWriteByte(reg); //发送寄存器值(位置),并读取状态值 for(u8_ctr=0; u8_ctr return status; //返回读到的状态值 } //启动 NRF24L01 发送一次数据 //txbuf:待发送数据首地址 //返回值:发送完成状况 u8 NRF24L01_TxPacket(u8 *txbuf) { u8 sta; SPI2_SetSpeed(SPI_BAUDRATEPRESCALER_8); //spi 速度为 6.75Mhz(24L01 的最大 SPI 时钟为 10Mhz) NRF24L01_CE=0; NRF24L01_Write_Buf(WR_TX_PLOAD,txbuf,TX_PLOAD_WIDTH); //写数据到 TX BUF 32 个字节 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 寄存器 return MAX_TX; } if(sta&TX_OK) //发送完成 { return TX_OK; } return 0xff;//其他原因发送失败 } //启动 NRF24L01 发送一次数据 //txbuf:待发送数据首地址 //返回值:0,接收完成;其他,错误代码 u8 NRF24L01_RxPacket(u8 *rxbuf) { u8 sta; SPI2_SetSpeed(SPI_BAUDRATEPRESCALER_8); //spi 速度为 6.75Mhz(24L01 的最大 SPI 时钟为 10Mhz) 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,rxbuf,RX_PLOAD_WIDTH);//读取数据 NRF24L01_Write_Reg(FLUSH_RX,0xff); //清除 RX FIFO 寄存器 return 0; } return 1;//没收到任何数据 } //该函数初始化 NRF24L01 到 RX 模式 //设置 RX 地址,写 RX 数据宽度,选择 RF 频道,波特率和 LNA HCURR //当 CE 变高后,即进入 RX 模式,并可以接收数据了 void NRF24L01_RX_Mode(void) { NRF24L01_CE=0; NRF24L01_Write_Buf(NRF_WRITE_REG+RX_ADDR_P0,(u8*)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 通信频率 NRF24L01_Write_Reg(NRF_WRITE_REG+RX_PW_P0,RX_PLOAD_WIDTH); //选择通道 0 的有效数据宽度 NRF24L01_Write_Reg(NRF_WRITE_REG+RF_SETUP,0x0f); //设置 TX 发射参数,0db 增益,2Mbps,低噪声增益开启 NRF24L01_Write_Reg(NRF_WRITE_REG+CONFIG, 0x0f); //配置基本工作模式的参数;PWR_UP,EN_CRC,16BIT_CRC,接收模式 NRF24L01_CE=1; //CE 为高,进入接收模式 } //该函数初始化 NRF24L01 到 TX 模式 //设置 TX 地址,写 TX 数据宽度,设置 RX 自动应答的地址,填充 TX 发送数据, //选择 RF 频道,波特率和 LNA HCURR //PWR_UP,CRC 使能 //当 CE 变高后,即进入 RX 模式,并可以接收数据了 //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_Buf(NRF_WRITE_REG+RX_ADDR_P0,(u8*)RX_ADDRESS, RX_ADR_WIDTH); //设置 TX 节点地址,主要为了使能 ACK 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+SETUP_RETR,0x1a); //设置自动重发间隔时间:500us + 86us;最大自动重发次数:10 次 NRF24L01_Write_Reg(NRF_WRITE_REG+RF_CH,40); //设置 RF 通道为 40 NRF24L01_Write_Reg(NRF_WRITE_REG+RF_SETUP,0x0f); //设置 TX 发射参数,0db 增益,2Mbps,低噪声增益开启 NRF24L01_Write_Reg(NRF_WRITE_REG+CONFIG,0x0e); //配置基本工作模式的参数;PWR_UP,EN_CRC,16BIT_CRC,接收模式,开启所有中断 NRF24L01_CE=1;//CE 为高,10us 后启动发送 } 此部分代码我们不多介绍,在这里强调一个要注意的地方,在 NRF24L01_Init 函数里面, 我们调用了 SPI1_Init()函数,该函数我们在第十五章曾有提到,在第十五章的设置里面,SCK 空闲时为高,但是 NRF24L01 的 SPI 通信时序如图 29.3.1 所示: 图 29.3.1 NRF24L01 读写操作时序 上图中 Cn 代表指令位,Sn 代表状态寄存器位,Dn 代表数据位。从图中可以看出,SCK 空 闲的时候是低电平的,而数据在 SCK 的上升沿被读写。所以,我们需要设置 SPI 的 CPOL 和 CPHA 均为 0,来满足 NRF24L01 对 SPI 操作的要求。这里主要是修改了下面两行代码; SPI1_Handler.Init.CLKPolarity=SPI_POLARITY_LOW; //串行同步时钟的空闲状态为低电平 SPI1_Handler.Init.CLKPhase=SPI_PHASE_1EDGE; //串行同步时钟的第 1 个跳变沿(上升或下降)数据被采样 接下来我们看看 24l01.h 头文件部分内容: #ifndef __24L01_H #define __24L01_H #include "sys.h" //NRF24L01 寄存器操作命令 #define READ_REG 0x00 //读配置寄存器,低 5 位为寄存器地址 ......//省略部分定义 #define FIFO_STATUS 0x17 //FIFO 状态寄存器;bit0,RX FIFO 寄存器空标志; //bit1,RX FIFO 满标志;bit2,3,保留 bit4,TX FIFO 空标志;bit5,TX FIFO 满标志; //bit6,1, 循环发送上一数据包.0,不循环; //24L01 操作线 #define NRF24L01_CE PAout(4) //24L01 片选信号 #define NRF24L01_CSN PCout(4) //SPI 片选信号 #define NRF24L01_IRQ PAin(1) //IRQ 主机数据输入 //24L01 发送接收数据宽度定义 #define TX_ADR_WIDTH 5 //5 字节的地址宽度 #define RX_ADR_WIDTH 5 //5 字节的地址宽度 #define TX_PLOAD_WIDTH 32 //32 字节的用户数据宽度 #define RX_PLOAD_WIDTH 32 //32 字节的用户数据宽度 void NRF24L01_Init(void); //初始化 void NRF24L01_RX_Mode(void); //配置为接收模式 void NRF24L01_TX_Mode(void); //配置为发送模式 u8 NRF24L01_Write_Buf(u8 reg, u8 *pBuf, u8 u8s); //写数据区 u8 NRF24L01_Read_Buf(u8 reg, u8 *pBuf, u8 u8s); //读数据区 u8 NRF24L01_Read_Reg(u8 reg); //读寄存器 u8 NRF24L01_Write_Reg(u8 reg, u8 value); //写寄存器 u8 NRF24L01_Check(void); //检查 24L01 是否存在 u8 NRF24L01_TxPacket(u8 *txbuf); //发送一个包的数据 u8 NRF24L01_RxPacket(u8 *rxbuf); //接收一个包的数据 #endif 部分代码,主要定义了一些 24L01 的命令字(这里我们省略了一部分),以及函数声明,这 里还通过 TX_PLOAD_WIDTH 和 RX_PLOAD_WIDTH 决定了发射和接收的数据宽度,也就是我们每次 发射和接受的有效字节数。NRF24L01 每次最多传输 32 个字节,再多的字节传输则需要多次传 送。特别提醒:两个 NRF24L01 模块,互相通信时,他们对应的发射和接收数据宽度必须一致, 否则将无法正常通信! 最后我们看看主函数: int main(void) { u8 key,mode; u16 t=0; u8 tmp_buf[33]; HAL_Init(); //初始化 HAL 库 Stm32_Clock_Init(RCC_PLL_MUL9); //设置时钟,72M delay_init(72); //初始化延时函数 uart_init(115200); //初始化串口 LED_Init(); //初始化 LED KEY_Init(); //初始化按键 LCD_Init(); //初始化 LCD NRF24L01_Init(); //初始化 NRF24L01 POINT_COLOR=RED; LCD_ShowString(30,50,200,16,16,"Mini STM32"); LCD_ShowString(30,70,200,16,16,"NRF24L01 TEST"); LCD_ShowString(30,90,200,16,16,"ATOM@ALIENTEK"); LCD_ShowString(30,110,200,16,16,"2019/11/15"); while(NRF24L01_Check()) { LCD_ShowString(30,130,200,16,16,"NRF24L01 Error"); delay_ms(200); LCD_Fill(30,130,239,130+16,WHITE); delay_ms(200); } LCD_ShowString(30,130,200,16,16,"NRF24L01 OK"); 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_ShowString(10,150,230,16,16,"KEY0:RX_Mode KEY1:TX_Mode"); //闪烁显示提示信息 if(t==200) { LCD_Fill(10,150,230,150+16,WHITE); t=0; } delay_ms(5); } LCD_Fill(10,150,240,166,WHITE);//清空上面的显示 POINT_COLOR=BLUE;//设置字体为蓝色 if(mode==0)//RX 模式 { LCD_ShowString(30,150,200,16,16,"NRF24L01 RX_Mode"); LCD_ShowString(30,170,200,16,16,"Received DATA:"); NRF24L01_RX_Mode(); while(1) { if(NRF24L01_RxPacket(tmp_buf)==0)//一旦接收到信息,则显示出来. { tmp_buf[32]=0;//加入字符串结束符 LCD_ShowString(0,190,lcddev.width-1,32,16,tmp_buf); }else delay_us(100); t++; if(t==10000)//大约 1s 钟改变一次状态 { t=0; LED0=!LED0; } }; }else//TX 模式 { LCD_ShowString(30,150,200,16,16,"NRF24L01 TX_Mode"); NRF24L01_TX_Mode(); mode=' ';//从空格键开始 while(1) { if(NRF24L01_TxPacket(tmp_buf)==TX_OK) { LCD_ShowString(30,170,239,32,16,"Sended DATA:"); LCD_ShowString(0,190,lcddev.width-1,32,16,tmp_buf); 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_ShowString(30,170,lcddev.width-1,32,16,"Send Failed "); }; LED0=!LED0; delay_ms(1500); }; } } 以上代码,我们就实现了 29.2 节所介绍的功能,程序运行时先通过 NRF24L01_Check 函 数检测 NRF24L01 是否存在,如果存在,则让用户选择发送模式(KEY1)还是接收模式(KEY0),在确定模式之后,设置 NRF24L01 的工作模式,然后执行相应的数据发送/接收处理。至此,我们整个实验的软件设计就完成了。 29.4 下载验证 在代码编译成功之后,我们通过下载代码到 ALIENTEK MiniSTM32 开发板上,可以看到LCD 显示如图 29.4.1 所示的内容(默认 NRF24L01 已经接上了): 图 29.4.1 选择工作模式界面 通过 KEY0 和 KEY1 来选择 NRF24L01 模块所要进入的工作模式,我们两个开发板一个选择发送,一个选择接收就可以了。 设置好后通信界面如图 29.4.2 所示: 图 29.4.2 通信界面 上图中,左侧的图片来自开发板 A,工作在发送模式。右侧的图片来自开发板 B,工作在接收模式,A 发送,B 接收。图中左右图片的数据不一样,是因为我们拍照的时间不一样导致的。 |
|
相关推荐
|
|
使用stm32l451片子,对ad7606进行3通道100k采样值跳动问题?
135 浏览 0 评论
STM32进行DMA数据转运,为什么只有A组最后一位数据被转运到B组去?
1337 浏览 2 评论
1650 浏览 3 评论
1258 浏览 0 评论
嵌入式开发 Win11安装ST-Link 提示 设备描述符请求失败 怎么处理?
1391 浏览 0 评论
小黑屋| 手机版| Archiver| 电子发烧友 ( 湘ICP备2023018690号 )
GMT+8, 2024-8-20 02:57 , Processed in 0.375601 second(s), Total 33, Slave 26 queries .
Powered by 电子发烧友网
© 2015 www.ws-dc.com
关注我们的微信
下载发烧友APP
电子发烧友观察
版权所有 © 湖南华秋数字科技有限公司
电子发烧友 (电路图) 湘公网安备 43011202000918 号 电信与信息服务业务经营许可证:合字B2-20210191 工商网监 湘ICP备2023018690号