文章目录
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);
else
HAL_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;
else
return 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个字节
else
pageremain = 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); // 写入整个扇区
}else
W25QXX_Write_NoCheck(pBuffer, WriteAddr, secremain); // 写已经擦除了的,直接写入扇区剩余区间.
if(NumByteToWrite == secremain)
break; // 写入结束了
else // 写入未结束
{
secpos++; // 扇区地址增1
secoff=0; // 偏移位置为0
pBuffer += secremain; // 指针偏移
WriteAddr += secremain; // 写地址偏移
NumByteToWrite -= secremain; // 字节数递减
if(NumByteToWrite > 4096)
secremain = 4096; // 下一个扇区还是写不完
else
secremain = 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 0xE9
void 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( ); // 检测nRF24L01
NRF24L01_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( ); // 检测nRF24L01
NRF24L01_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;
- 检测找到后,输出提示;
- 之后模块一直发送数据,并给出发送成功提示;
- 不管接不接收都会一直发送。
然后测试接收端
先把发送端断电,只打开接收端,可以看到初始化步骤和上面一样。但是初始化完成之后会一直等待接收数据。
然后打开发送端,就可以接收到发送端发送出来的数据。
拔掉发送端模块,再次接收不到数据。
再放一次驱动代码,欢迎点赞!!!===>>>驱动代码(无需积分,直接下载)<<<===
这模块搞了一天半,终于把博客这章写完了。