文章目录
- 1. 准备工作
- 1.1. 所用硬件
- 1.2. SPI 简介
- 1.3. 生成工程
- 1.3.1. 创建工程选择主控
- 1.3.2. 系统配置
- 1.3.3. 配置工程目录
- 2. 读写EEPROM实验(W25Q64)
- 2.1. W25Q64 简介
- 2.2. 代码实现
- 3. NRF24L01无线模块通信
- 3.1. 模块简介
- 3.2. SPI 配置
- 3.2.1. SPI1 配置
- 3.2.2. SPI2 配置
- 3.3. 代码实现
- 3.3.1. 添加驱动代码
- 3.3.2. 驱动修改
- 3.4. 主函数
- 3.5. 测试
====>>> 文章汇总(有代码汇总) <<<====
1. 准备工作
1.1. 所用硬件
读写EEPROM实验(W25Q64):正点原子Mini开发板,主控STM32F103RCT6
通信实验:再加一个普中的,主控STM32F103ZET6。
1.2. SPI 简介
SPI(Serial Peripheral interface) 串行外围设备接口
- 由 Motorola公司开发
- 高速的,全双工,同步的通信总线
- 需要四根线
- 时钟最多可以到 18Mhz
SPI 接口一般使用 4 条线通信:
- MISO 主设备数据输入,从设备数据输出
- MOSI 主设备数据输出,从设备数据输入
- SCLK 时钟信号,由主设备产生
- CS 从设备片选信号,由主设备控制
SPI 也可以有一对多的情况,根据CS片选信号选择是对哪个从机发送或者接收数据。
1.3. 生成工程
1.3.1. 创建工程选择主控
1.3.2. 系统配置
配置时钟源
配置debug模式(如果需要ST-Link下载及调试可以勾选)
配置时钟树(可以直接在HCLK那里输入72,然后敲回车会自动配置)
1.3.3. 配置工程目录
2. 读写EEPROM实验(W25Q64)
EEPROM (Electrically Erasable Programmable read only memory)是指带电可擦可编程只读存储器。是一种掉电后数据不丢失的存储芯片。
2.1. W25Q64 简介
原理图
芯片引脚说明:
- CS 片选引脚。低电平表示选中。
- DO SPI数据输出接口
- WP 硬件写保护引脚,输入高电平可以正常写入数据,输入低电平禁止写入。
- GND 公共地
- DI SPI数据输入接口
- CLK SPI时钟接口
- HOLD 状态保存接口,输入低电平禁止操作芯片,输入高电平可正常操作芯片。
- VCC 电源接口,2.7-3.6电源
存储说明
W25Q64,其中64表示芯片的存储容量是64M bit,也就是 8M 字节(B)
。
- 整个芯片 8M字节 被分为 128个块,每个块 64kb;
- 每个块 64k字节 被分为 16个扇区,每个扇区 4K字节(4096字节) ;
- 每个扇区 4K字节 被分为 16个页,每个页 256字节。
2.2. 代码实现
PA2为片选信号,设置其为推挽输出即可。
SPI 配置
分频系数为4,因为 SPI 时钟最多可以到 18Mhz,而这里时钟是72Mhz,经过四分频之后刚好是18Mhz。
串口重定向也配置一下,方便观察–>串口重定向配置<–
这部分代码很多,因此,生成工程后。
- 在工程文件夹中单独创建一个
icode
文件,用来存放我们自己的代码。 - 在icode文件夹下,再创建一个
W25Q64文件夹
,存放W25Q64相关代码。 - 在W25Q64文件夹中创建
w25qxx.c
、w25qxx.h
两个文件。
添加源文件和头文件路径
编写代码如下:
w25qxx.c
/** spi1.c** Created on: Oct 29, 2022* Author: Haozi** 使用的芯片为:W25Q64** 芯片容量及地址说明:总容量(8M字节)* 单位 大小 比例 数量* 页 256字节 最小单位* 扇区 4K字节(4096字节) 16页 2048个* 块 64K字节 16个扇区 128个** 芯片引脚说明:* 1. CS 片选引脚。低电平表示选中。* 2. DO SPI数据输出接口* 3. WP 硬件写保护引脚,输入高电平可以正常写入数据,输入低电平禁止写入。* 4. GND 公共地* 5. DI SPI数据输入接口* 6. CLK SPI时钟接口* 7,HOLD 状态保存接口,输入低电平禁止操作芯片,输入高电平可正常操作芯片。* 8. VCC 电源接口,2.7-3.6电源** 本例程,引脚接口。* 1. CS GPIO PA2.------------------- 需要操作的* 2. DO SPI1 MISO ------------------ 需要操作的* 3. SP 接VCC* 4. GND 接地* 5. DI SPI1 MOSI ------------------ 需要操作的* 6. CLK SPI1 CLK ------------------- 需要操作的* 7,HOLD VCC3.3* 8. VCC VCC3.3*/#include "main.h"
#include "stm32f1xx_it.h"
#include "w25qxx.h"
#include "spi.h"// 定义使用的芯片型号
uint16_t W25QXX_TYPE = W25Q64;/** @brief CS使能控制函数** @param a:0为低电平 表示有效* a:其他值为高电平 表示无效*/
void W25QXX_CS(uint8_t a)
{if(a==0)HAL_GPIO_WritePin(SPI1_CS_GPIO_Port, SPI1_CS_Pin, GPIO_PIN_RESET);elseHAL_GPIO_WritePin(SPI1_CS_GPIO_Port, SPI1_CS_Pin, GPIO_PIN_SET);
}/** @brief SPI1总线读写一个字节** @param TxData:写入的字节** @return 读出的字节*/
uint8_t SPI1_ReadWriteByte(uint8_t TxData)
{uint8_t Rxdata;HAL_SPI_TransmitReceive(&hspi1, &TxData, &Rxdata, 1, 1000);return Rxdata;
}/** @brief 读取芯片ID** @note 高8位是厂商代号(本程序不判断厂商代号)、低8位是容量大小* 0XEF13型号为W25Q80* 0XEF14型号为W25Q16* 0XEF15型号为W25Q32* 0XEF16型号为W25Q64* 0XEF17型号为W25Q128* 0XEF18型号为W25Q256** @return 读出的字节*/
uint16_t W25QXX_ReadID(void)
{uint16_t Temp = 0;W25QXX_CS(0);SPI1_ReadWriteByte(0x90); // 发送读取ID命令SPI1_ReadWriteByte(0x00);SPI1_ReadWriteByte(0x00);SPI1_ReadWriteByte(0x00);Temp |= SPI1_ReadWriteByte(0xFF)<<8;Temp |= SPI1_ReadWriteByte(0xFF);W25QXX_CS(1);return Temp;
}/** @brief 读取W25QXX的状态寄存器** @note W25QXX一共有3个状态寄存器* 状态寄存器1:BIT7 6 5 4 3 2 1 0* SPR RV TB BP2 BP1 BP0 WEL BUSY* SPR: 默认0,状态寄存器保护位,配合WP使用* TB,BP2,BP1,BP0: FLASH区域写保护设置* WEL: 写使能锁定* BUSY: 忙标记位(1,忙;0,空闲)* 默认: 0x00* 状态寄存器2:BIT7 6 5 4 3 2 1 0* SUS CMP LB3 LB2 LB1 (R) QE SRP1* 状态寄存器3:BIT7 6 5 4 3 2 1 0* HOLD/RST DRV1 DRV0 (R) (R) WPS (R) (R)** @param regno:状态寄存器号。范:1~3** @return 状态寄存器值*/
uint8_t W25QXX_ReadSR(uint8_t regno)
{uint8_t byte = 0,command = 0;switch(regno){case 1:command = W25X_ReadStatusReg1;break;case 2:command = W25X_ReadStatusReg2;break;case 3:command = W25X_ReadStatusReg3;break;default:command = W25X_ReadStatusReg1;break;}W25QXX_CS(0);SPI1_ReadWriteByte(command);byte = SPI1_ReadWriteByte(0Xff);W25QXX_CS(1);return byte;
}/** @brief 写W25QXX状态寄存器** @note W25QXX一共有3个状态寄存器* 状态寄存器1:BIT7 6 5 4 3 2 1 0* SPR RV TB BP2 BP1 BP0 WEL BUSY* SPR: 默认0,状态寄存器保护位,配合WP使用* TB,BP2,BP1,BP0: FLASH区域写保护设置* WEL: 写使能锁定* BUSY: 忙标记位(1,忙;0,空闲)* 默认: 0x00* 状态寄存器2:BIT7 6 5 4 3 2 1 0* SUS CMP LB3 LB2 LB1 (R) QE SRP1* 状态寄存器3:BIT7 6 5 4 3 2 1 0* HOLD/RST DRV1 DRV0 (R) (R) WPS (R) (R)** @param regno:状态寄存器号。范:1~3* @param sr:写入的值** @return 状态寄存器值*/
void W25QXX_Write_SR(uint8_t regno, uint8_t sr)
{uint8_t command=0;switch(regno){case 1:command=W25X_WriteStatusReg1;break;case 2:command=W25X_WriteStatusReg2;break;case 3:command=W25X_WriteStatusReg3;break;default:command=W25X_WriteStatusReg1;break;}W25QXX_CS(0);SPI1_ReadWriteByte(command);SPI1_ReadWriteByte(sr);W25QXX_CS(1);
}/** @brief W25QXX写使能 将WEL置位*/
void W25QXX_Write_Enable(void)
{W25QXX_CS(0);SPI1_ReadWriteByte(W25X_WriteEnable);W25QXX_CS(1);
}/** @brief W25QXX写禁止 将WEL清零*/
void W25QXX_Write_Disable(void)
{W25QXX_CS(0);SPI1_ReadWriteByte(W25X_WriteDisable);W25QXX_CS(1);
}/** @brief 初始化SPI FLASH的IO口** @return 0:识别成功。1:识别失败*/
uint8_t W25QXX_Init(void)
{uint8_t temp;W25QXX_CS(1);W25QXX_TYPE = W25QXX_ReadID();// SPI FLASH为W25Q256时才用设置为4字节地址模式if(W25QXX_TYPE == W25Q256){// 读取状态寄存器3,判断地址模式temp = W25QXX_ReadSR(3);// 如果不是4字节地址模式,则进入4字节地址模式if((temp&0x01) == 0){W25QXX_CS(0);// 发送进入4字节地址模式指令SPI1_ReadWriteByte(W25X_Enable4ByteAddr);W25QXX_CS(1);}}if(W25QXX_TYPE==W25Q256||W25QXX_TYPE==W25Q128||W25QXX_TYPE==W25Q64||W25QXX_TYPE==W25Q32||W25QXX_TYPE==W25Q16||W25QXX_TYPE==W25Q80)return 0;elsereturn 1;
}/** @brief 读取SPI FLASH。** @note 在指定地址开始读取指定长度的数据。** @param pBuffer 数据存储区* @param ReadAddr 开始读取的地址(24bit)* @param NumByteToRead 要读取的字节数(最大65535)**/
void W25QXX_Read(uint8_t* pBuffer, uint32_t ReadAddr, uint16_t NumByteToRead)
{uint16_t i;W25QXX_CS(0);SPI1_ReadWriteByte(W25X_ReadData);if(W25QXX_TYPE == W25Q256){// 如果是W25Q256的话地址为4字节的,要发送最高8位SPI1_ReadWriteByte((uint8_t)((ReadAddr)>>24));}SPI1_ReadWriteByte((uint8_t)((ReadAddr)>>16)); // 发送24bit地址SPI1_ReadWriteByte((uint8_t)((ReadAddr)>>8));SPI1_ReadWriteByte((uint8_t)ReadAddr);for(i = 0; i < NumByteToRead; i++){pBuffer[i] = SPI1_ReadWriteByte(0XFF); // 循环读数}W25QXX_CS(1);
}/** @brief 等待空闲*/
void W25QXX_Wait_Busy(void)
{while((W25QXX_ReadSR(1)&0x01)==0x01);
}/** @brief SPI在一页(0~65535)内写入少于256个字节的数据** @note 在指定地址开始写入最大256字节的数据** @param pBuffer 数据存储区* @param WriteAddr 开始写入的地址(24bit)* @param NumByteToWrite 要写入的字节数(最大256),该数不应该超过该页的剩余字节数!!!**/
void W25QXX_Write_Page(uint8_t* pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite)
{uint16_t i;W25QXX_Write_Enable();W25QXX_CS(0);SPI1_ReadWriteByte(W25X_PageProgram);//发送写页命令if(W25QXX_TYPE==W25Q256)//如果是W25Q256的话地址为4字节的,要发送最高8位{SPI1_ReadWriteByte((uint8_t)((WriteAddr)>>24));}SPI1_ReadWriteByte((uint8_t)((WriteAddr)>>16));//发送24bit地址SPI1_ReadWriteByte((uint8_t)((WriteAddr)>>8));SPI1_ReadWriteByte((uint8_t)WriteAddr);for(i = 0; i < NumByteToWrite; i++)SPI1_ReadWriteByte(pBuffer[i]);W25QXX_CS(1);W25QXX_Wait_Busy();
}/** @brief 无检验写SPI FLASH** @note 必须确保所写的地址范围内的数据全部为0XFF,否则在非0XFF处写入的数据将失败!* 具有自动换页功能。在指定地址开始写入指定长度的数据,但是要确保地址不越界!** @param pBuffer 数据存储区* @param WriteAddr 开始写入的地址(24bit)* @param NumByteToWrite 要写入的字节数(最大65535)**/
void W25QXX_Write_NoCheck(uint8_t* pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite)
{uint16_t pageremain;// 计算单页剩余的字节数pageremain = 256-WriteAddr%256;if(NumByteToWrite <= pageremain)pageremain = NumByteToWrite; // 不大于256个字节while(1){W25QXX_Write_Page(pBuffer, WriteAddr, pageremain);if(NumByteToWrite == pageremain)break;else{pBuffer += pageremain;WriteAddr += pageremain;NumByteToWrite -= pageremain; // 减去已经写入了的字节数if(NumByteToWrite > 256)pageremain = 256; // 一次可以写入256个字节elsepageremain = NumByteToWrite; // 不够256个字节了}}
}/** @brief 写SPI FLASH** @note 在指定地址开始写入指定长度的数据。相比于上面的函数,该函数带擦除操作!** @param pBuffer 数据存储区* @param WriteAddr 开始写入的地址(24bit)* @param NumByteToWrite 要写入的字节数(最大65535)**/
uint8_t W25QXX_BUFFER[4096];
void W25QXX_Write(uint8_t* pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite)
{uint32_t secpos;uint16_t secoff;uint16_t secremain;uint16_t i;uint8_t* W25QXX_BUF;W25QXX_BUF = W25QXX_BUFFER;secpos = WriteAddr / 4096; // 扇区地址secoff = WriteAddr % 4096; // 在扇区内的偏移secremain = 4096 - secoff; // 扇区剩余空间大小if(NumByteToWrite <= secremain)secremain = NumByteToWrite; // 不大于4096个字节while(1){W25QXX_Read(W25QXX_BUF, secpos*4096, 4096); // 读出整个扇区的内容for(i=0; i<secremain; i++) // 校验数据{if(W25QXX_BUF[secoff+i]!=0XFF)break; // 需要擦除}if(i<secremain) // 需要擦除{W25QXX_Erase_Sector(secpos); // 擦除这个扇区for(i=0; i<secremain; i++) // 复制{W25QXX_BUF[i+secoff] = pBuffer[i];}W25QXX_Write_NoCheck(W25QXX_BUF, secpos*4096, 4096); // 写入整个扇区}elseW25QXX_Write_NoCheck(pBuffer, WriteAddr, secremain); // 写已经擦除了的,直接写入扇区剩余区间.if(NumByteToWrite == secremain)break; // 写入结束了else // 写入未结束{secpos++; // 扇区地址增1secoff=0; // 偏移位置为0pBuffer += secremain; // 指针偏移WriteAddr += secremain; // 写地址偏移NumByteToWrite -= secremain; // 字节数递减if(NumByteToWrite > 4096)secremain = 4096; // 下一个扇区还是写不完elsesecremain = NumByteToWrite; // 下一个扇区可以写完了}}
}/** @brief 擦除整个芯片** @note 等待时间超长...**/
void W25QXX_Erase_Chip(void)
{W25QXX_Write_Enable();W25QXX_Wait_Busy();W25QXX_CS(0);SPI1_ReadWriteByte(W25X_ChipErase);W25QXX_CS(1);W25QXX_Wait_Busy();
}/** @brief 擦除一个扇区** @note 擦除一个扇区的最少时间:150ms** @param Dst_Addr 扇区地址 根据实际容量设置**/
void W25QXX_Erase_Sector(uint32_t Dst_Addr)
{Dst_Addr *= 4096;W25QXX_Write_Enable();W25QXX_Wait_Busy();W25QXX_CS(0);SPI1_ReadWriteByte(W25X_SectorErase);if(W25QXX_TYPE == W25Q256){SPI1_ReadWriteByte((uint8_t)((Dst_Addr)>>24));}SPI1_ReadWriteByte((uint8_t)((Dst_Addr)>>16));SPI1_ReadWriteByte((uint8_t)((Dst_Addr)>>8));SPI1_ReadWriteByte((uint8_t)Dst_Addr);W25QXX_CS(1);W25QXX_Wait_Busy();
}
w23qxx.h
/** spi1.h** Created on: Oct 29, 2022* Author: Haozi*/
#ifndef MYPROJECT_W25Q64_W25QXX_H_
#define MYPROJECT_W25Q64_W25QXX_H_#include "main.h"// 25系列FLASH芯片厂商与容量代号(厂商代号EF)
#define W25Q80 0XEF13
#define W25Q16 0XEF14
#define W25Q32 0XEF15
#define W25Q64 0XEF16
#define W25Q128 0XEF17
#define W25Q256 0XEF18
#define EX_FLASH_ADD 0x000000 // W25Q64的地址是24位宽
extern uint16_t W25QXX_TYPE; // 定义W25QXX芯片型号
extern SPI_HandleTypeDef hspi1;// ********************* 指令表 ************************* //
// 写使能 与 写禁止
#define W25X_WriteEnable 0x06
#define W25X_WriteDisable 0x04
// 读取状态寄存器123的命令
#define W25X_ReadStatusReg1 0x05
#define W25X_ReadStatusReg2 0x35
#define W25X_ReadStatusReg3 0x15
// 写状态寄存器123的命令
#define W25X_WriteStatusReg1 0x01
#define W25X_WriteStatusReg2 0x31
#define W25X_WriteStatusReg3 0x11
// 读取数据指令
#define W25X_ReadData 0x03
#define W25X_FastReadData 0x0B
#define W25X_FastReadDual 0x3B
#define W25X_PageProgram 0x02
#define W25X_BlockErase 0xD8
// 扇区擦除指令
#define W25X_SectorErase 0x20
// 片擦除命令
#define W25X_ChipErase 0xC7
#define W25X_PowerDown 0xB9
#define W25X_ReleasePowerDown 0xAB
#define W25X_DeviceID 0xAB
#define W25X_ManufactDeviceID 0x90
#define W25X_JedecDeviceID 0x9F
// 进入4字节地址模式指令
#define W25X_Enable4ByteAddr 0xB7
#define W25X_Exit4ByteAddr 0xE9void W25QXX_CS(uint8_t a); // W25QXX片选引脚控制
uint8_t SPI1_ReadWriteByte(uint8_t TxData); // SPI1总线底层读写
uint16_t W25QXX_ReadID(void); // 读取FLASH ID
uint8_t W25QXX_ReadSR(uint8_t regno); // 读取状态寄存器
void W25QXX_Write_SR(uint8_t regno,uint8_t sr); // 写状态寄存器
void W25QXX_Write_Enable(void); // 写使能
void W25QXX_Write_Disable(void); // 写保护
uint8_t W25QXX_Init(void); // 初始化W25QXX函数
void W25QXX_Wait_Busy(void); // 等待空闲
// 读取flash
void W25QXX_Read(uint8_t* pBuffer,uint32_t ReadAddr,uint16_t NumByteToRead);
// 写入flash
void W25QXX_Write_Page(uint8_t* pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite);
void W25QXX_Write_NoCheck(uint8_t* pBuffer,uint32_t WriteAddr,uint16_t NumByteToWrite);
void W25QXX_Write(uint8_t* pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite);
// 擦除flash
void W25QXX_Erase_Chip(void); // 整片擦除
void W25QXX_Erase_Sector(uint32_t Dst_Addr); // 扇区擦除#endif /* MYPROJECT_W25Q64_W25QXX_H_ */
在主函数main.c
中测试
/* USER CODE BEGIN Includes */
#include "w25qxx.h"
/* USER CODE END Includes *//* Private function prototypes -----------------------------------------------*/
void SystemClock_Config(void);/*** @brief The application entry point.* @retval int*/
int main(void)
{HAL_Init();/* Configure the system clock */SystemClock_Config();/* Initialize all configured peripherals */MX_GPIO_Init();MX_SPI1_Init();MX_USART1_UART_Init();/* USER CODE BEGIN WHILE */// 初始化W25QXX_Init();// 芯片flash大小uint32_t FLASH_SIZE = 8*1024*1024; // FLASH 大小8M字节printf("------------- 读取芯片ID实验 --------------- \r\n");uint16_t Deceive_ID;Deceive_ID = W25QXX_ReadID();if (Deceive_ID == 0){printf("Read Deceive_ID fail \r\n");} else {printf("Deceive_ID is %X \r\n", Deceive_ID); // 显示芯片ID}printf("------------- 读写 字节实验 --------------- \r\n");uint8_t string[] = {"HAOZI TEST"};uint8_t getStringBuf[sizeof(string)] = {"&&&&&&&&&&"}; // 初始值W25QXX_Write(string, 0, sizeof(string));W25QXX_Read(getStringBuf, 0, sizeof(string));if (getStringBuf[0] == '&'){printf("Read string fail \r\n");} else {printf("Read string is: %s \r\n", getStringBuf);}printf("------------- 读写 浮点数实验 --------------- \r\n");// 浮点数 读写测试union float_union{float float_num; // 浮点数占4个字节double double_num; // 双精度浮点数占8个字节uint8_t buf[8]; // 定义 8个字节 的空间};union float_union write_float_data; // 用来写union float_union read_float_data; // 用来读// 先测试第一个 浮点数write_float_data.float_num = 3.1415f;read_float_data.float_num = 0;W25QXX_Write(write_float_data.buf, 20, 4);W25QXX_Read(read_float_data.buf, 20, 4);if(read_float_data.float_num == 0){printf("Read float fail \r\n");} else {printf("Read float data is %f \r\n", read_float_data.float_num);}// 再测试第二个 双精度浮点数write_float_data.double_num = 3.1415;read_float_data.double_num = 0;W25QXX_Write(write_float_data.buf, 20, 8);W25QXX_Read(read_float_data.buf, 20, 8);if(read_float_data.float_num == 0){printf("Read double fail \r\n");} else {printf("Read double data is %.15f \r\n", read_float_data.double_num);}while (1){}
}
效果验证
编译、烧录
链接串口助手
3. NRF24L01无线模块通信
3.1. 模块简介
简介
- 是一种无线通信模块;
- 工作在免费开放的2.4GHz频段;
- 通信速率可以达到最高2Mbps;
- MCU与该模块通信采用 SPI 接口通信。
下图中右侧模块就是。实际上有很多类似的模块,工作原理和代码基本都是一样的,比如左边的模块是个国产的。实测代码都能通用。
比如这些国产的我都有,几年前他店里刚上架的时候比较便宜,就把所有的都买了一对,HaHaHaHaHaHa!!!
实际上,我的代码也是在他官网的例程改过来的,它的代码是自己写的,还是用标准库写的,但是现在都用CubeMX,用HAL库写代码,所以我就改了改拿来用。有需要其他型号主控的例程可以去看看。
官网网址(公司记得结下广告费,谢谢!):http://www.gisemi.com/
模块接口(一共八个引脚):
- CSN:芯片的片选线,低电平芯片工作;
- SCK:芯片控制的时钟线(SPI的时钟);
- MISO:芯片控制数据线(SPI的MISO);
- MOSI:芯片控制数据线(SPI的MOSI);
- IRQ:中断信号,NRF24L01芯片收到数据、或者发送完数据等等一些情况会产生下降沿中断;
- CE:芯片的模式控制线,决定了芯片的工作状态。
MCU 开发板连接:
通信需要两个开发板和两个模块。
- 开发板1:正点原子Mini开发板,主控STM32F103RCT6。
- 开发板2:普中-准瑞-Z100开发板,主控STM32F103ZET6。
3.2. SPI 配置
因为我这里用的两个不同的主控,所以需要创建两次工程,都和第一章一样,选择不同的主控即可。
如果你用的两个同样的主控,就选择一样的主控。
具体的 SPI 配置不用区分是 用于发送的还是用于接收的,只需要按照原理图把使用的SPI及相关使能引脚配置好就可以了。
3.2.1. SPI1 配置
正点原子Mini开发板 使用的是SPI1进行通信,原理图如图所示。
这里用的是 SPI1 进行通信。
NRF24L01要求时钟速率不能超过8Mhz。
其余三个引脚,IRQ对应的是上拉输入,控制线和片选为推挽输出。
同时,接收把串口重定向也配置一下, 方便观察。
===>>>串口配置<<<===
3.2.2. SPI2 配置
普中-准瑞-Z100开发板 使用的是SPI2进行通信,原理图如图所示。
这里用的是 SPI2 进行通信。
其余三个引脚,IRQ对应的是上拉输入,控制线和片选为推挽输出。
同时,接收把串口重定向也配置一下, 方便观察。
===>>>串口配置<<<===
注意,如果两个使用的是两个不同的SPI,比如这里用的是SPI1和SPI2,因为两个SPI的时钟不一样,因此分频系数也不能一样,要保证配置完之后的频率和其他信息是一样的。
STM32的SPI1在APB2上,SPI2和SPI3在APB1上,APB1的最高频率是36MHz,APB2的最高频率是72MHz。
3.3. 代码实现
3.3.1. 添加驱动代码
生成工程后,把 NRF24L01的驱动代码添加到工程(两个工程都需要)。
===>>>驱动代码(无需积分,直接下载)<<<===
在工程目录创建icode
文件夹,在里面再创建NRF24L01
文件夹,在里面添加nrf24L01.h
、nrf24l01.c
文件(两个工程都需要)。
在Keil中 添加代码及头文件路径(前面很多文章都有写,这里就不放图了)。
3.3.2. 驱动修改
其实修改的内容不多。发送端和接收端都一样。
- 在
nrf24L01.h
文件中,修改你使用的是哪个SPI。 - 在
nrf24L01.h
文件中,修改CS、CE、IRQ引脚的定义。如果你在图形化配置时和我用了一样的Label,则无需修改。 - 在
nrf24L01.c
文件中,修改使用的是哪个SPI(只有图上的一个地方)。
3.4. 主函数
发送端
/* USER CODE BEGIN Includes */
#include "nrf24l01.h"
/* USER CODE END Includes */int main(void)
{/* Reset of all peripherals, Initializes the Flash interface and the Systick. */HAL_Init();/* Configure the system clock */SystemClock_Config();/* Initialize all configured peripherals */MX_GPIO_Init();MX_SPI2_Init();MX_USART1_UART_Init();/* USER CODE BEGIN WHILE */NRF24L01_Gpio_Init( ); // 初始化片选及模式引脚NRF24L01_check( ); // 检测nRF24L01NRF24L01_Init( ); // 初始化模块NRF24L01_Set_Mode( MODE_TX ); // 发送模式uint8_t index = 0;uint8_t txData[12] = {"0.success \r\n"}; // 12字节while (1){// 发送固定字符,2S一包NRF24L01_TxPacket( txData, 12 );// 发送完成之后,给个提示,方便调试printf("txdata is: %d%s", txData[0], &txData[1]);// 修改发送的信息index = index + 1;txData[0] = index;// 延迟一段时间再次发送HAL_Delay(2000);/* USER CODE END WHILE *//* USER CODE BEGIN 3 */}/* USER CODE END 3 */
}
接收端
/* USER CODE BEGIN Includes */
#include "nrf24l01.h"
/* USER CODE END Includes */int main(void)
{HAL_Init();/* Configure the system clock */SystemClock_Config();/* Initialize all configured peripherals */MX_GPIO_Init();MX_SPI1_Init();MX_USART1_UART_Init();/* USER CODE BEGIN WHILE */NRF24L01_Gpio_Init( ); // 初始化片选及模式引脚NRF24L01_check( ); // 检测nRF24L01NRF24L01_Init( ); // 初始化模块NRF24L01_Set_Mode( MODE_RX ); // 接收模式uint8_t reLen = 0; // 接收到的数据长度uint8_t nrf24l01RxBuffer[ 32 ] = { 0 }; // 接收缓存while (1){reLen = NRF24L01_RxPacket( nrf24l01RxBuffer ); // 接收字节if( 0 != reLen ){printf("rxData is: %d%s \r\n", nrf24l01RxBuffer[0], &nrf24l01RxBuffer[1]);}/* USER CODE END WHILE *//* USER CODE BEGIN 3 */}/* USER CODE END 3 */
}
3.5. 测试
先测试发送端
开发板与串口调试助手连接。
复位开发板。
- 刚开始不插上模块,会一直检测模块是否存在,并提示不存在0;
- 检测找到后,输出提示;
- 之后模块一直发送数据,并给出发送成功提示;
- 不管接不接收都会一直发送。
然后测试接收端
先把发送端断电,只打开接收端,可以看到初始化步骤和上面一样。但是初始化完成之后会一直等待接收数据。
然后打开发送端,就可以接收到发送端发送出来的数据。
拔掉发送端模块,再次接收不到数据。
再放一次驱动代码,欢迎点赞!!!===>>>驱动代码(无需积分,直接下载)<<<===
这模块搞了一天半,终于把博客这章写完了。