STM32CubeMX系列08——SPI通信(W25Q64、NRF24L01无线模块)

====>>> 文章汇总(有代码汇总) <<<====

1. 准备工作

1.1. 所用硬件

读写EEPROM实验(W25Q64):正点原子Mini开发板,主控STM32F103RCT6

通信实验:再加一个普中的,主控STM32F103ZET6。

1.2. SPI 简介

SPI(Serial Peripheral interface) 串行外围设备接口

  1. 由 Motorola公司开发
  2. 高速的,全双工,同步的通信总线
  3. 需要四根线
  4. 时钟最多可以到 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 简介

原理图
在这里插入图片描述
芯片引脚说明:

  1. CS 片选引脚。低电平表示选中。
  2. DO SPI数据输出接口
  3. WP 硬件写保护引脚,输入高电平可以正常写入数据,输入低电平禁止写入。
  4. GND 公共地
  5. DI SPI数据输入接口
  6. CLK SPI时钟接口
  7. HOLD 状态保存接口,输入低电平禁止操作芯片,输入高电平可正常操作芯片。
  8. VCC 电源接口,2.7-3.6电源

存储说明
W25Q64,其中64表示芯片的存储容量是64M bit,也就是 8M 字节(B)

  1. 整个芯片 8M字节 被分为 128个块,每个块 64kb;
  2. 每个块 64k字节 被分为 16个扇区,每个扇区 4K字节(4096字节) ;
  3. 每个扇区 4K字节 被分为 16个页,每个页 256字节。

2.2. 代码实现

PA2为片选信号,设置其为推挽输出即可。
在这里插入图片描述
SPI 配置
在这里插入图片描述

分频系数为4,因为 SPI 时钟最多可以到 18Mhz,而这里时钟是72Mhz,经过四分频之后刚好是18Mhz。

串口重定向也配置一下,方便观察–>串口重定向配置<–

这部分代码很多,因此,生成工程后。

  1. 在工程文件夹中单独创建一个icode文件,用来存放我们自己的代码。
  2. 在icode文件夹下,再创建一个W25Q64文件夹,存放W25Q64相关代码。
  3. 在W25Q64文件夹中创建w25qxx.cw25qxx.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. 模块简介

简介

  1. 是一种无线通信模块;
  2. 工作在免费开放的2.4GHz频段;
  3. 通信速率可以达到最高2Mbps;
  4. MCU与该模块通信采用 SPI 接口通信。

下图中右侧模块就是。实际上有很多类似的模块,工作原理和代码基本都是一样的,比如左边的模块是个国产的。实测代码都能通用
请添加图片描述
比如这些国产的我都有,几年前他店里刚上架的时候比较便宜,就把所有的都买了一对,HaHaHaHaHaHa!!!
在这里插入图片描述

实际上,我的代码也是在他官网的例程改过来的,它的代码是自己写的,还是用标准库写的,但是现在都用CubeMX,用HAL库写代码,所以我就改了改拿来用。有需要其他型号主控的例程可以去看看。
官网网址(公司记得结下广告费,谢谢!):http://www.gisemi.com/

模块接口(一共八个引脚):

  1. CSN:芯片的片选线,低电平芯片工作;
  2. SCK:芯片控制的时钟线(SPI的时钟);
  3. MISO:芯片控制数据线(SPI的MISO);
  4. MOSI:芯片控制数据线(SPI的MOSI);
  5. IRQ:中断信号,NRF24L01芯片收到数据、或者发送完数据等等一些情况会产生下降沿中断;
  6. 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.hnrf24l01.c文件(两个工程都需要)。
在这里插入图片描述
在Keil中 添加代码及头文件路径(前面很多文章都有写,这里就不放图了)。

3.3.2. 驱动修改

其实修改的内容不多。发送端和接收端都一样。

  1. nrf24L01.h文件中,修改你使用的是哪个SPI。
  2. nrf24L01.h文件中,修改CS、CE、IRQ引脚的定义。如果你在图形化配置时和我用了一样的Label,则无需修改。
  3. 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. 测试

先测试发送端
开发板与串口调试助手连接。
复位开发板。

  1. 刚开始不插上模块,会一直检测模块是否存在,并提示不存在0;
  2. 检测找到后,输出提示;
  3. 之后模块一直发送数据,并给出发送成功提示;
  4. 不管接不接收都会一直发送。

在这里插入图片描述
然后测试接收端
先把发送端断电,只打开接收端,可以看到初始化步骤和上面一样。但是初始化完成之后会一直等待接收数据。
在这里插入图片描述
然后打开发送端,就可以接收到发送端发送出来的数据。
在这里插入图片描述
拔掉发送端模块,再次接收不到数据。

再放一次驱动代码,欢迎点赞!!!===>>>驱动代码(无需积分,直接下载)<<<===

这模块搞了一天半,终于把博客这章写完了。

  • 11
    点赞
  • 38
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值