STM32CubeMX教程20 SPI - W25Q128驱动

news2024/9/22 5:26:13

目录

1、准备材料

2、实验目标

3、实验流程

3.0、前提知识

3.1、CubeMX相关配置

3.1.1、时钟树配置

3.1.2、外设参数配置

3.1.3、外设中断配置

3.2、生成代码

3.2.1、外设初始化调用流程

3.2.2、外设中断调用流程

3.2.3、添加其他必要代码

4、常用函数

5、烧录验证

5.1、实验具体流程

5.2、实验现象

6、注释详解

参考资料


1、准备材料

开发板(正点原子stm32f407探索者开发板V2.4)

STM32CubeMX软件(Version 6.10.0)

野火DAP仿真器

keil µVision5 IDE(MDK-Arm)

CH340G Windows系统驱动程序(CH341SER.EXE)

XCOM V2.6串口助手

逻辑分析仪nanoDLA

2、实验目标

使用STM32CubeMX软件配置STM32F407开发板的SPI1与W25Q128芯片通信,以轮询方式读写W25Q128 FLASH芯片,并通过USART1输出相关信息,具体为使用开发板上的三个用户按键KEY0/1/2,分别实现对W25Q128芯片写数据/读数据/擦除数据的操作,操作过程中与用户的交互由USART1输出信息来实现

3、实验流程

3.0、前提知识

本实验重点是理解标准SPI通信协议,而STM32CubeMX的配置则相对简单,这里不会过于详细全面的介绍SPI通信协议,但是会对所有需要知道的知识做介绍

标准SPI通信协议由时钟信号线SCK、主设备输出从设备输入MOSI和主设备输入从设备输出MISO三根线组成,与I2C通信协议不同,挂载在SPI总线上的外围器件不需要有从设备地址,而是由片选CS/SS信号选择从机设备,当片选信号为低电平时,表示该从设备被选中,此时主设备通过SCK、MOSI与MISO三根线与该从设备之间进行通信和数据传输,如下所示为SPI总线连接图(注释1)

本实验所使用的开发板上有一颗FLASH芯片W25Q128,STM32F407通过PB3(SPI1_SCK)、PB4(SPI1_MISO)和PB5(SPI1_MOSI)三个引脚利用标准SPI协议与其进行通信和数据传输,W25Q128的片选信号选择了MCU的PB14引脚,如下图所示为其硬件原理图

SPI通信协议的时序根据CPOL(时钟极性)和CPHA(时钟相位)两个寄存器位的不同一共有四种组合模式

时钟极性CPOL位用来控制SCK引脚在空闲状态时的电平,当该位为0时则表示空闲时刻SCK为低电平,反之为高电平

时钟相位CPHA位用来控制在SCK信号的第几个边沿处采集信号,当该位为0时表示在SCK型号的第一个边沿处采集信号,反之则表示在第二个边沿处采集信号

如下图所示为根据CPOL和CPHA位取不同值时SPI通信协议的四种时序图(注释2)

使用逻辑分析仪对STM32F407 SPI1通信SCLK、MISO、MOSI和CS四个引脚进行逻辑电平监测,可以发现在执行读取W25Q128芯片ID操作的过程中,其四个引脚的时序与我们所介绍的一致,如下图所示为执行读取W25Q128芯片ID操作所使用的程序、CPOL=0 CPHA=0时SPI通信采集到的时序和CPOL=1 CPHA=1时SPI通信采集到的时序

3.1、CubeMX相关配置

请先阅读“STM32CubeMX教程1 工程建立”实验3.4.1小节配置RCC和SYS

3.1.1、时钟树配置

系统时钟树均设置为STM32F407总线能达到的最高时钟频率,无需启动LSE,与上个实验一致,具体配置如下图所示

3.1.2、外设参数配置

此实验主要是利用SPI通信协议与W25Q128芯片进行通信和数据传输,并且需要串口将读取的数据输出给用户,同时还需要三个用户按键KEY0/1/2/,因此外设需要初始化KEY0/1/2、USART1和SPI1

按键初始化操作请阅读“STM32CubeMX教程3 GPIO输入 - 按键响应”实验

单击Pinout & Configuration页面左边Connectivity/USART1选项,然后按照“STM32CubeMX教程9 USART/UART 异步通信”实验中将USART1配置为异步通信模式,无需开启中断,如下图所示

单击Pinout & Configuration页面左边Connectivity/SPI1选项,Mode选择全双工主机模式,不需要硬件片选,时钟分频选择16分频,根据W25Q128的数据手册(注释3),读数据指令支持的最高频率为33MHz,因此适当降低频率确保通信不会出现错误,其他参数配置默认即可,具体配置如下图所示

然后在右边芯片引脚预览Pinout view中找到W25Q128芯片的片选引脚PB14,左键单击并配置其功能为GPIO_Ouput,然后单击System Core/GPIO,配置PB14引脚默认输出电平高,推挽输出,无上下拉,IO速度非常高,具体配置如下图所示

3.1.3、外设中断配置

本实验无需启用中断,如果需要启用SPI1的中断,请单击System Core/NVIC,然后根据需求勾选SP1全局中断,并选择合适的中断优先级即可,具体配置如下图所示

3.2、生成代码

请先阅读“STM32CubeMX教程1 工程建立”实验3.4.3小节配置Project Manager

单击页面右上角GENERATE CODE生成工程

3.2.1、外设初始化调用流程

在生成的工程代码主函数中新增了MX_SPI1_Init()函数,在该函数中实现了对SPI1的模式及参数配置

在MX_SPI1_Init()函数中调用了HAL_SPI_Init()函数使用配置的参数对SPI1进行了初始化

在HAL_SPI_Init()函数中又调用了HAL_SPI_MspInit()函数对SPI1引脚复用设置,SPI1时钟使能,如果开启了中断该函数中还会有中断相关设置及使能

具体的SPI1初始化函数调用流程如下图所示

3.2.2、外设中断调用流程

本实验无需中断,因此未启动任何SPI1的中断

3.2.3、添加其他必要代码

需要添加W25Q128的驱动文件,注意本实验只使用而不会介绍W25Q128具体驱动文件的原理,具体源代码如下图所示(注释4)

w25flash.c文件

/* 文件: w25flash.c
 * 功能描述: Flash 存储器W25Q128的驱动程序
 * 作者:王维波
 * 修改日期:2019-06-05
 */


#include "w25flash.h"

#define MAX_TIMEOUT   200		//SPI轮询操作时的最大等待时间,ms

//SPI接口发送一个字节,byteData是需要发送的数据
HAL_StatusTypeDef	SPI_TransmitOneByte(uint8_t	byteData)
{
	return HAL_SPI_Transmit(&SPI_HANDLE, &byteData, 1, MAX_TIMEOUT);
}

//SPI接口发送多个字节, pBuffer是发送数据缓存区指针,byteCount是发送数据字节数,byteCount最大256
HAL_StatusTypeDef	SPI_TransmitBytes(uint8_t* pBuffer, uint16_t byteCount)
{
	return HAL_SPI_Transmit(&SPI_HANDLE, pBuffer, byteCount, MAX_TIMEOUT);
}

//SPI接口接收一个字节, 返回接收的一个字节数据
uint8_t	SPI_ReceiveOneByte()
{
	uint8_t	byteData=0;
	HAL_SPI_Receive(&SPI_HANDLE, &byteData, 1, MAX_TIMEOUT);
	return  byteData;
}

//SPI接口接收多个字节, pBuffer是接收数据缓存区指针,byteCount是需要接收数据的字节数
HAL_StatusTypeDef	SPI_ReceiveBytes(uint8_t* pBuffer, uint16_t byteCount)
{
	return HAL_SPI_Receive(&SPI_HANDLE, pBuffer, byteCount, MAX_TIMEOUT);
}

//Command=0x05:  Read Status Register-1,返回寄存器SR1的值
uint8_t Flash_ReadSR1(void)
{  
	uint8_t byte=0;
	__Select_Flash();	//CS=0
	SPI_TransmitOneByte(0x05); //Command=0x05:  Read Status Register-1
	byte=SPI_ReceiveOneByte();
	__Deselect_Flash();	//CS=1
	return byte;   
} 

//Command=0x35:  Read Status Register-2,返回寄存器SR2的值
uint8_t Flash_ReadSR2(void)
{
	uint8_t byte=0;
	__Select_Flash();	//CS=0
	SPI_TransmitOneByte(0x35); //Command=0x35:  Read Status Register-2
	byte=SPI_ReceiveOneByte();	//读取一个字节
	__Deselect_Flash();	//CS=1
	return byte;
}


//Command=0x01:  Write Status Register,	只写SR1的值
//耗时大约10-15ms
void Flash_WriteSR1(uint8_t SR1)
{   
	Flash_Write_Enable();       //必须使 WEL=1

	__Select_Flash();	//CS=0
	SPI_TransmitOneByte(0x01);   //Command=0x01:  Write Status Register,	只写SR1的值
	SPI_TransmitOneByte(0x00);    //SR1的值
//	SPI_WriteOneByte(0x00);    //SR2的值, 只发送SR1的值,而不发送SR2的值, QE和CMP将自动被清零
	__Deselect_Flash();	//CS=1

	Flash_Wait_Busy(); 	   //耗时大约10-15ms
}  

HAL_StatusTypeDef Flash_WriteVolatile_Enable(void)  	//Command=0x50: Write Volatile Enable
{
	__Select_Flash();	//CS=0
	HAL_StatusTypeDef result=SPI_TransmitOneByte(0x50);
	__Deselect_Flash();	//CS=1
	return result;
}


//Command=0x06: Write Enable,    使WEL=1
HAL_StatusTypeDef Flash_Write_Enable(void)
{
	__Select_Flash();	//CS=0
	HAL_StatusTypeDef result=SPI_TransmitOneByte(0x06);  //Command=0x06: Write Enable,    使WEL=1
	__Deselect_Flash();	//CS=1
	Flash_Wait_Busy(); 	//等待操作完成
	return result;
} 

//Command=0x04, Write Disable,	  使WEL=0
HAL_StatusTypeDef Flash_Write_Disable(void)
{  
	__Select_Flash();	//CS=0
	HAL_StatusTypeDef result=SPI_TransmitOneByte(0x04); 	//Command=0x04, Write Disable,	  使WEL=0
	__Deselect_Flash();	//CS=1
	Flash_Wait_Busy(); 	   //
	return result;
} 

//根据Block绝对编号获取地址, 共256个Block, BlockNo 取值范围0-255
//每个块64K字节,16位地址,块内地址范围0x0000-0xFFFF。
uint32_t	Flash_Addr_byBlock(uint8_t	BlockNo)
{
//	uint32_t addr=BlockNo*0x10000;

	uint32_t addr=BlockNo;
	addr=addr<<16; //左移16位,等于乘以0x10000
	return addr;
}

//根据Sector绝对编号获取地址, 共4096个Sector, SectorNo取值范围0-4095
//每个扇区4K字节,12位地址,扇区内地址范围0x000-0xFFF
uint32_t	Flash_Addr_bySector(uint16_t  SectorNo)
{
	if (SectorNo>4095)	//不能超过4095
		SectorNo=0;
//	uint32_t addr=SectorNo*0x1000;

	uint32_t addr=SectorNo;
	addr=addr<<12;		//左移12位,等于乘以0x1000
	return addr;
}

//根据Page绝对编号获取地址,共65536个Page,  PageNo取值范围0-65535
//每个页256字节,8位地址,页内地址范围0x00—0xFF
uint32_t	Flash_Addr_byPage(uint16_t  PageNo)
{
//	uint32_t addr=PageNo*0x100;

	uint32_t addr=PageNo;
	addr=addr<<8;		//左移8位,等于乘以0x100
	return addr;
}

//根据Block编号和内部Sector编号计算地址,一个Block有16个Sector
//BlockNo取值范围0-255,  内部SubSectorNo取值范围0-15
uint32_t	Flash_Addr_byBlockSector(uint8_t BlockNo, uint8_t SubSectorNo)
{
	if (SubSectorNo>15)	 //不能超过15
		SubSectorNo=0;

//	uint32_t addr=BlockNo*0x10000;	//先计算Block的起始地址
	uint32_t addr=BlockNo;
	addr=addr<<16;	//先计算Block的起始地址

//	uint32_t offset=SubSectorNo*0x1000;	//计算Sector的偏移地址
	uint32_t offset=SubSectorNo;	//计算Sector的偏移地址
	offset=offset<<12;	//计算Sector的偏移地址

	addr += offset;

	return addr;
}

//根据Block编号,内部Sector编号,内部Page编号获取地址
//BlockNo取值范围0-255
//一个Block有16个Sector, 内部SubSectorNo取值范围0-15
//一个Sector有16个Page , 内部SubPageNo取值范围0-15
uint32_t	Flash_Addr_byBlockSectorPage(uint8_t BlockNo, uint8_t SubSectorNo, uint8_t  SubPageNo)
{
	if (SubSectorNo>15)	//不能超过15
		SubSectorNo=0;

	if (SubPageNo>15)	//不能超过15
		SubPageNo=0;

//	uint32_t addr=BlockNo*0x10000;		//先计算Block的起始地址
	uint32_t addr=BlockNo;
	addr=addr<<16;		//先计算Block的起始地址

//	uint32_t offset=SubSectorNo*0x1000;	//计算Sector的偏移地址
	uint32_t offset=SubSectorNo;	//计算Sector的偏移地址
	offset=offset<<12;	//计算Sector的偏移地址
	addr += offset;

//	offset=SubPageNo*0x100;	//计算Page的偏移地址
	offset=SubPageNo;
	offset=offset<<8;	//计算Page的偏移地址

	addr += offset;		//Page的起始地址
	return addr;
}

//将24位地址分解为3个字节
//globalAddr是全局24位地址, 返回 addrHigh高字节,addrMid中间字节,addrLow低字节
void  Flash_SpliteAddr(uint32_t globalAddr, uint8_t* addrHigh, uint8_t* addrMid,uint8_t* addrLow)
{
	*addrHigh= (globalAddr>>16);	//addrHigh=高字节

	globalAddr =globalAddr & 0x0000FFFF;	//屏蔽高字节
	*addrMid= (globalAddr>>8);	//addrMid=中间字节

	*addrLow =globalAddr & 0x000000FF;	//屏蔽中间字节, 只剩低字节,addrLow=低字节
}


//读取芯片ID
//返回值如下:				   
// 0xEF17,表示芯片型号为W25Q128, Winbond,用过
// 0xC817,表示芯片型号为GD25Q128,ELM,用过
// 0x1C17,表示芯片型号为EN25Q128,台湾EON
// 0xA117,表示芯片型号为FM25Q128,复旦微电子
// 0x2018,表示芯片型号为N25Q128,美光
// 0x2017,表示芯片型号为XM25QH128,武汉新芯,用过

//读取芯片的制造商和器件ID,高字节是Manufacturer ID,低字节是Device ID
uint16_t Flash_ReadID(void)
{
	uint16_t Temp = 0;
	__Select_Flash();	//CS=0

	SPI_TransmitOneByte(0x90);		 //指令码,0x90=Manufacturer/Device ID
	SPI_TransmitOneByte(0x00);		 //dummy
	SPI_TransmitOneByte(0x00);		 //dummy
	SPI_TransmitOneByte(0x00);		 //0x00
	Temp =SPI_ReceiveOneByte()<<8; //Manufacturer ID
	Temp|=SPI_ReceiveOneByte();	 	 //Device ID, 与具体器件相关

	__Deselect_Flash();	//CS=1
	
	return Temp;
}

// 参数High32和Low32分别返回64位序列号的高32位和低32位的值
// 函数返回值为64位序列号的值
uint64_t  Flash_ReadSerialNum(uint32_t* High32,  uint32_t* Low32)//读取64位序列号,
{
	uint8_t Temp = 0;
	uint64_t SerialNum=0;
	uint32_t High=0,Low=0;

	__Select_Flash();	//CS=0
	SPI_TransmitOneByte(0x4B);		//发送指令码, 4B=read Unique ID
	SPI_TransmitOneByte(0x00);		//发送4个Dummy字节数据
	SPI_TransmitOneByte(0x00);
	SPI_TransmitOneByte(0x00);
	SPI_TransmitOneByte(0x00);

	for(uint8_t i=0; i<4; i++)  //高32位
	{
		Temp =SPI_ReceiveOneByte();
		High = (High<<8);
		High = High | Temp;  //按位或
	}

	for(uint8_t i=0; i<4; i++)	//低32位
	{
		Temp =SPI_ReceiveOneByte();
		Low = (Low<<8);
		Low = Low | Temp;  //按位或
	}
	__Deselect_Flash();	//CS=1

	*High32 = High;
	*Low32=Low;

	SerialNum = High;
	SerialNum = SerialNum<<32;  //高32位
	SerialNum=SerialNum | Low;

	return SerialNum;
}


//在任意地址读取一个字节的数据,返回读取的字节数据
// globalAddr是24位全局地址
uint8_t Flash_ReadOneByte(uint32_t globalAddr)
{
	uint8_t byte2, byte3, byte4;
	Flash_SpliteAddr(globalAddr, &byte2, &byte3, &byte4);	//24位地址分解为3个字节

	__Select_Flash();	//CS=0
	SPI_TransmitOneByte(0x03);      //Command=0x03, read data
	SPI_TransmitOneByte(byte2);		//发送24位地址
	SPI_TransmitOneByte(byte3);
	SPI_TransmitOneByte(byte4);
	byte2 = SPI_ReceiveOneByte();	//接收1个字节
	__Deselect_Flash();	//CS=1

	return byte2;
}


//从任何地址开始读取指定长度的数据
//globalAddr:开始读取的地址(24bit), pBuffer:数据存储区指针,byteCount:要读取的字节数
void Flash_ReadBytes(uint32_t globalAddr, uint8_t* pBuffer, uint16_t byteCount)
{ 
	uint8_t byte2, byte3, byte4;
	Flash_SpliteAddr(globalAddr, &byte2, &byte3, &byte4);	//24位地址分解为3个字节

	__Select_Flash();	//CS=0
	SPI_TransmitOneByte(0x03);      //Command=0x03, read data
	SPI_TransmitOneByte(byte2);		//发送24位地址
	SPI_TransmitOneByte(byte3);
	SPI_TransmitOneByte(byte4);
	SPI_ReceiveBytes(pBuffer, byteCount);	//接收byteCount个字节数据
	__Deselect_Flash();	//CS=1
}  

//Command=0x0B,  高速连续读取flash多个字节,任意全局地址, 速度大约是常规读取的2倍
void Flash_FastReadBytes(uint32_t globalAddr, uint8_t* pBuffer,  uint16_t byteCount)
{
// 	uint16_t i;
	uint8_t byte2, byte3, byte4;
	Flash_SpliteAddr(globalAddr, &byte2, &byte3, &byte4);	//24位地址分解为3个字节

	__Select_Flash();	//CS=0

	SPI_TransmitOneByte(0x0B);      //Command=0x0B, fast read data
	SPI_TransmitOneByte(byte2);		//发送24位地址
	SPI_TransmitOneByte(byte3);
	SPI_TransmitOneByte(byte4);
	SPI_TransmitOneByte(0x00);		//Dummy字节

	SPI_ReceiveBytes(pBuffer, byteCount);	//接收byteCount个字节数据
	__Deselect_Flash();	//CS=1

}

//Command=0xC7: Chip Erase, 擦除整个器件
// 擦除后,所有存储区内容为0xFF,耗时大约25秒
void Flash_EraseChip(void)
{                                   
	Flash_Write_Enable();   //使 WEL=1
	Flash_Wait_Busy();   	//等待空闲

	__Select_Flash();		//CS=0
	SPI_TransmitOneByte(0xC7);  // Command=0xC7: Chip Erase, 擦除整个器件
	__Deselect_Flash();		//CS=1

	Flash_Wait_Busy();   //等待芯片擦除结束,大约25秒
}   

// Command=0x02: Page program, 对一个页(256字节)编程, 耗时大约3ms,
// globalAddr是写入初始地址,全局地址
// pBuffer是要写入数据缓冲区指针,byteCount是需要写入的数据字节数
// 写入的Page必须是前面已经擦除过的,如果写入地址超出了page的边界,就从Page的开头重新写
void Flash_WriteInPage(uint32_t globalAddr, uint8_t* pBuffer, uint16_t byteCount)
{
	uint8_t byte2, byte3, byte4;
	Flash_SpliteAddr(globalAddr, &byte2, &byte3, &byte4);	//24位地址分解为3个字节

	Flash_Write_Enable();   //SET WEL
 	Flash_Wait_Busy();

	__Select_Flash();	//CS=0
	SPI_TransmitOneByte(0x02);      //Command=0x02: Page program 对一个扇区编程
	SPI_TransmitOneByte(byte2);	//发送24位地址
	SPI_TransmitOneByte(byte3);
	SPI_TransmitOneByte(byte4);
	SPI_TransmitBytes(pBuffer, byteCount); 		//发送byteCount个字节的数据
//	for(uint16_t i=0; i<byteCount; i++)
//	{
//		byte2=pBuffer[i];
//		SPI_WriteOneByte(byte2);	//要写入的数据
//	}
	__Deselect_Flash();	//CS=1

	Flash_Wait_Busy(); 	   //耗时大约3ms
}

//从某个Sector的起始位置开始写数据,数据可能跨越多个Page,甚至跨越Sector,不必提前擦除
// globalAddr是写入初始地址,全局地址,是扇区的起始地址,
// pBuffer是要写入数据缓冲区指针
// byteCount是需要写入的数据字节数,byteCount不能超过64K,也就是一个Block(16个扇区)的大小,但是可以超过一个Sector(4K字节)
// 如果数据超过一个Page,自动分成多个Page,调用EN25Q_WriteInPage分别写入
void Flash_WriteSector(uint32_t globalAddr,  uint8_t* pBuffer, uint16_t byteCount)
{
//需要先擦除扇区,可能是重复写文件
	uint8_t secCount= (byteCount / FLASH_SECTOR_SIZE);	//数据覆盖的扇区个数
	if ((byteCount % FLASH_SECTOR_SIZE) >0)
		secCount++;

	uint32_t startAddr=globalAddr;
	for (uint8_t k=0; k<secCount; k++)
	{
		Flash_EraseSector(startAddr);	//擦除扇区
		startAddr += FLASH_SECTOR_SIZE;	//移到下一个扇区
	}

//分成Page写入数据,写入数据的最小单位是Page
	uint16_t  leftBytes=byteCount % FLASH_PAGE_SIZE;  //非整数个Page剩余的字节数,即最后一个Page写入的数据
	uint16_t  pgCount=byteCount/FLASH_PAGE_SIZE;  //前面整数个Page
	uint8_t* buff=pBuffer;
	for(uint16_t i=0; i<pgCount; i++)	//写入前面pgCount个Page的数据,
	{
		Flash_WriteInPage(globalAddr, buff, FLASH_PAGE_SIZE);  //写一整个Page的数据
		globalAddr += FLASH_PAGE_SIZE;	//地址移动一个Page
		buff += FLASH_PAGE_SIZE;		//数据指针移动一个Page大小
	}

	if (leftBytes>0)
		Flash_WriteInPage(globalAddr, buff, leftBytes);  //最后一个Page,不是一整个Page的数据
}

//Command=0xD8: Block Erase(64KB) 擦除整个Block, globalAddr是全局地址
//清除后存储区内容全部为0xFF,  耗时大概150ms
void Flash_EraseBlock64K(uint32_t globalAddr)
{
 	Flash_Write_Enable();   //SET WEL
 	Flash_Wait_Busy();

	uint8_t byte2, byte3, byte4;
	Flash_SpliteAddr(globalAddr, &byte2, &byte3, &byte4);	//24位地址分解为3个字节

	__Select_Flash();	//CS=0

	SPI_TransmitOneByte(0xD8);      //Command=0xD8, Block Erase(64KB)
	SPI_TransmitOneByte(byte2);	//发送24位地址
	SPI_TransmitOneByte(byte3);
	SPI_TransmitOneByte(byte4);

	__Deselect_Flash();	//CS=1
	Flash_Wait_Busy(); 	   //耗时大概150ms
}


//擦除一个扇区(4KB字节),Command=0x20, Sector Erase(4KB)
//globalAddr: 扇区的绝对地址,24位地址0x00XXXXXX
//擦除后,扇区内全部内容为0xFF, 耗时大约30ms,
void Flash_EraseSector(uint32_t globalAddr)
{  
 	Flash_Write_Enable();   //SET WEL
 	Flash_Wait_Busy();
	uint8_t byte2, byte3, byte4;
	Flash_SpliteAddr(globalAddr, &byte2, &byte3, &byte4);	//24位地址分解为3个字节

	__Select_Flash();	//CS=0

	SPI_TransmitOneByte(0x20);      //Command=0x20, Sector Erase(4KB)
	SPI_TransmitOneByte(byte2);		//发送24位地址
	SPI_TransmitOneByte(byte3);
	SPI_TransmitOneByte(byte4);

	__Deselect_Flash();		//CS=1
	Flash_Wait_Busy(); 	   //大约30ms
}

//检查寄存器SR1的BUSY位,直到BUSY位为0
uint32_t Flash_Wait_Busy(void)
{   
	uint8_t	SR1=0;
	uint32_t  delay=0;
	SR1=Flash_ReadSR1();	//读取状态寄存器SR1
	while((SR1 & 0x01)==0x01)
	{
		HAL_Delay(1);	//延时1ms
		delay++;
		SR1=Flash_ReadSR1();	//读取状态寄存器SR1
	}
	return delay;
}

//进入掉电模式
//Command=0xB9: Power Down
void Flash_PowerDown(void)
{ 
	__Select_Flash();	//CS=0
	SPI_TransmitOneByte(0xB9);  //Command=0xB9: Power Down
	__Deselect_Flash();	//CS=1
    HAL_Delay(1); //等待TPD
}   

//唤醒
//Command=0xAB: Release Power Down
void Flash_WakeUp(void)
{  
	__Select_Flash();	//CS=0
	SPI_TransmitOneByte(0xAB);		//Command=0xAB: Release Power Down
	__Deselect_Flash();	//CS=1
	HAL_Delay(1);     //等待TRES1
}

w25flash.h文件

/* 文件: w25flash.h
 * 功能描述: Flash 存储器W25Q128的驱动程序
 * 作者:王维波
 * 修改日期:2019-06-05
 * W25Q128 芯片参数: 16M字节,24位地址线
 * 分为256个Block,每个Block 64K字节
 * 一个Block又分为16个Sector,共4096个Sector,每个Sector 4K字节
 * 一个Sector又分为16个Page,共65536个Page,每个Page 256字节
 * 写数据操作的基本单元是Page,一次连续写入操作不能超过一个Page的范围。写的Page必须是擦除过的。
 */

#ifndef _W25FLASH_H
#define _W25FLASH_H

#include 	"stm32f4xx_hal.h"
#include	"spi.h"		//使用其中的变量 hspi1,表示SPI1接口

/*  W25Q128硬件接口相关的部分:CS引脚和SPI接口 ,若电路不同,更改这部分配置即可   */
// Flash_CS -->PB14, 片选信号CS操作的宏定义函数
#define CS_PORT		GPIOB
#define	CS_PIN		GPIO_PIN_14
#define	SPI_HANDLE		hspi1		//SPI接口对象,使用spi.h中的变量 hspi1

#define	__Select_Flash()		HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_RESET)	//CS=0
#define	__Deselect_Flash()		HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_SET)	//CS=1

//===========Flash存储芯片W25Q128的存储容量参数================
#define		FLASH_PAGE_SIZE			256		//一个Page是256字节

#define		FLASH_SECTOR_SIZE		4096	//一个Sector是4096字节

#define		FLASH_SECTOR_COUNT		4096	//总共4096个 Sector

//=======1. SPI 基本发送和接收函数,阻塞式传输============
HAL_StatusTypeDef	SPI_TransmitOneByte(uint8_t	byteData);	//SPI接口发送一个字节
HAL_StatusTypeDef	SPI_TransmitBytes(uint8_t* pBuffer, uint16_t byteCount);	//SPI接口发送多个字节

uint8_t	SPI_ReceiveOneByte(void);	//SPI接口接收一个字节
HAL_StatusTypeDef	SPI_ReceiveBytes(uint8_t* pBuffer, uint16_t byteCount);	//SPI接口接收多个字节

//=========2. W25Qxx 基本控制指令==========
// 0xEF17,表示芯片型号为W25Q128, Winbond,用过
// 0xC817,表示芯片型号为GD25Q128,ELM,用过
// 0x1C17,表示芯片型号为EN25Q128,台湾EON
// 0xA117,表示芯片型号为FM25Q128,复旦微电子
// 0x2018,表示芯片型号为N25Q128,美光
// 0x2017,表示芯片型号为XM25QH128,武汉新芯,用过

uint16_t  Flash_ReadID(void); 	// Command=0x90, Manufacturer/Device ID

uint64_t  Flash_ReadSerialNum(uint32_t* High32,  uint32_t* Low32); //Command=0x4B, Read Unique ID, 64-bit

HAL_StatusTypeDef Flash_WriteVolatile_Enable(void);  	//Command=0x50: Write Volatile Enable

HAL_StatusTypeDef Flash_Write_Enable(void);  	//Command=0x06: Write Enable,    使WEL=1
HAL_StatusTypeDef Flash_Write_Disable(void);	//Command=0x04, Write Disable,	  使WEL=0

uint8_t	 Flash_ReadSR1(void);  	//Command=0x05:  Read Status Register-1,	返回寄存器SR1的值
uint8_t	 Flash_ReadSR2(void);  	//Command=0x35:  Read Status Register-2,	返回寄存器SR2的值

void Flash_WriteSR1(uint8_t SR1);  //Command=0x01:  Write Status Register,	只写SR1的值,禁止写状态寄存器

uint32_t Flash_Wait_Busy(void);  	//读状态寄存器SR1,等待BUSY变为0,返回值是等待时间
void Flash_PowerDown(void);   		//Command=0xB9: Power Down
void Flash_WakeUp(void);  			//Command=0xAB: Release Power Down

//========3. 计算地址的辅助功能函数========
//根据Block  绝对编号获取地址,共256个Block
uint32_t	Flash_Addr_byBlock(uint8_t BlockNo);
//根据Sector 绝对编号获取地址,共4096个Sector
uint32_t	Flash_Addr_bySector(uint16_t  SectorNo);
//根据Page  绝对编号获取地址,共65536个Page
uint32_t	Flash_Addr_byPage(uint16_t  PageNo);

//根据Block编号,和内部Sector编号计算地址,一个Block有16个Sector,
uint32_t	Flash_Addr_byBlockSector(uint8_t BlockNo, uint8_t SubSectorNo);
//根据Block编号,内部Sector编号,内部Page编号计算地址
uint32_t	Flash_Addr_byBlockSectorPage(uint8_t BlockNo, uint8_t SubSectorNo, uint8_t  SubPageNo);
//将24位地址分解为3个字节
void		Flash_SpliteAddr(uint32_t globalAddr, uint8_t* addrHigh, uint8_t* addrMid,uint8_t* addrLow);

//=======4. chip、Block,Sector擦除函数============
//Command=0xC7: Chip Erase, 擦除整个器件,大约25秒
void Flash_EraseChip(void);

//Command=0xD8: Block Erase(64KB) 擦除整个Block, globalAddr是全局地址,耗时大约150ms
void Flash_EraseBlock64K(uint32_t globalAddr);

//Command=0x20: Sector Erase(4KB) 扇区擦除, globalAddr是扇区的全局地址,耗时大约30ms
void Flash_EraseSector(uint32_t globalAddr);

//=========5. 数据读写函数=============
//Command=0x03,  读取一个字节,任意全局地址
uint8_t Flash_ReadOneByte(uint32_t globalAddr);

//Command=0x03,  连续读取多个字节,任意全局地址
void Flash_ReadBytes(uint32_t globalAddr, uint8_t* pBuffer,  uint16_t byteCount);

//Command=0x0B,  高速连续读取多个字节,任意全局地址, 速度大约是常规读取的2倍
void Flash_FastReadBytes(uint32_t globalAddr, uint8_t* pBuffer,  uint16_t byteCount);

//Command=0x02: Page program 对一个Page写入数据(最多256字节), globalAddr是初始位置的全局地址,耗时大约3ms
void Flash_WriteInPage(uint32_t globalAddr, uint8_t* pBuffer, uint16_t byteCount);

//从某个Sector的起始地址开始写数据,数据可能跨越多个Page,甚至跨越Sector,总字节数byteCount不能超过64K,也就是一个Block的大小
void Flash_WriteSector(uint32_t globalAddr,  uint8_t* pBuffer, uint16_t byteCount);

#endif

向工程中添加.c/.h文件的步骤请阅读“STM32CubeMX教程17 I2C - MPU6050驱动”实验3.2.3小节

在主函数中添加操作提示信息和按键操作逻辑程序,具体如下图所示

源代码如下

/*主函数主循环外代码*/
uint16_t ID = Flash_ReadID();
printf("W25Q128 ID:0x%x\r\n",ID);
printf("---------------------\r\n");
printf("KEY2: Flash_Write\r\n");
printf("KEY1: Flash_Read\r\n");
printf("KEY0: Flash_Erase\r\n");
printf("---------------------\r\n");

/*主函数主循环内代码*/
/*按键KEY2被按下*/
if(HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin) == GPIO_PIN_RESET)
{
    HAL_Delay(50);
    if(HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin) == GPIO_PIN_RESET)
    {
        Flash_TestWrite();
        while(!HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin));
    }
}
/*按键KEY1被按下*/
if(HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin) == GPIO_PIN_RESET)
{
    HAL_Delay(50);
    if(HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin) == GPIO_PIN_RESET)
    {
        Flash_TestRead();
        while(!HAL_GPIO_ReadPin(KEY1_GPIO_Port,KEY1_Pin));
    }
}
/*按键KEY0被按下*/
if(HAL_GPIO_ReadPin(KEY0_GPIO_Port,KEY0_Pin) == GPIO_PIN_RESET)
{
    HAL_Delay(50);
    if(HAL_GPIO_ReadPin(KEY0_GPIO_Port,KEY0_Pin) == GPIO_PIN_RESET)
    {
        printf("---------------------\r\n");
        printf("Erasing Block 0(256 pages)...\r\n");
        uint32_t globalAddr=0;
        Flash_EraseBlock64K(globalAddr);
        printf("Block 0 is erased.\r\n");
        printf("---------------------\r\n");
        while(!HAL_GPIO_ReadPin(KEY0_GPIO_Port,KEY0_Pin));
    }
}

在spi.c中实现W25Q128的写入/读取测试函数Flash_TestWrite()/Flash_TestRead(),具体源代码如下所示(注释4)

/*spi.c中包含的头文件*/
#include "w25flash.h"
#include "string.h"
#include "stdio.h"

/*spi.c中的函数定义*/
//测试写入Page0和Page1
//注意:一个Page写入之前必须是被擦除过的,写入之后就不能再重复写
void Flash_TestWrite(void)
{
	uint8_t blobkNo = 0;
	uint16_t sectorNo = 0;
	uint16_t pageNo = 0;
	uint32_t memAddress = 0;
	
	printf("---------------------\r\n");
	//写入Page0两个字符串
	memAddress = Flash_Addr_byBlockSectorPage(blobkNo, sectorNo, pageNo);		//Page0的地址
	uint8_t	bufStr1[] = "Hello from beginning";
	uint16_t len = 1 + strlen("Hello from beginning"); 											//包括结束符'\0'
	Flash_WriteInPage(memAddress, bufStr1, len);   													//在Page0的起始位置写入数据
	printf("Write in Page0:0\r\n%s\r\n", bufStr1);

	uint8_t	bufStr2[]="Hello in page";
	len = 1 + strlen("Hello in page"); 																			//包括结束符'\0'
	Flash_WriteInPage(memAddress+100, bufStr2, len);   											//Page0内偏移100
	printf("Write in Page0:100\r\n%s\r\n", bufStr2);

	//写入Page1中0-255数字
	uint8_t	bufPage[FLASH_PAGE_SIZE];																				//EN25Q_PAGE_SIZE=256
	for (uint16_t i=0;i<FLASH_PAGE_SIZE;i++)
		bufPage[i] = i;																												//准备数据
	pageNo = 1; 																														//Page 1
	memAddress = Flash_Addr_byBlockSectorPage(blobkNo, sectorNo, pageNo);		//page1的地址
	Flash_WriteInPage(memAddress, bufPage, FLASH_PAGE_SIZE);   							//写一个Page
	printf("Write 0-255 in Page1\r\n");
	printf("---------------------\r\n");
}

//测试读取Page0 和 Page1的内容
void Flash_TestRead(void)
{
	uint8_t blobkNo=0;
	uint16_t sectorNo=0;
	uint16_t pageNo=0;
	
	printf("---------------------\r\n");
	//读取Page0
	uint8_t bufStr[50];																											//Page0读出的数据
	uint32_t memAddress = Flash_Addr_byBlockSectorPage(blobkNo, sectorNo,pageNo);
	Flash_ReadBytes(memAddress, bufStr, 50);																//读取50个字符
	printf("Read from Page0:0\r\n%s\r\n",bufStr);

	Flash_ReadBytes(memAddress+100, bufStr, 50);														//地址偏移100后的50个字字节
	printf("Read from Page0:100\r\n%s\r\n",bufStr);

	//读取Page1
	uint8_t	randData = 0;
	pageNo = 1;
	memAddress = Flash_Addr_byBlockSectorPage(blobkNo, sectorNo,pageNo);

	randData = Flash_ReadOneByte(memAddress+12);														//读取1个字节数据,页内地址偏移12
	printf("Page1[12] = %d\r\n",randData);

	randData = Flash_ReadOneByte(memAddress+136);														//页内地址偏移136
	printf("Page1[136] = %d\r\n",randData);

	randData = Flash_ReadOneByte(memAddress+210);														//页内地址偏移210
	printf("Page1[210] = %d\r\n",randData);
	printf("---------------------\r\n");
}

/*spi.h中的函数声明*/
void Flash_TestWrite(void);
void Flash_TestRead(void);

4、常用函数

/*SPI发送数据函数*/
HAL_StatusTypeDef HAL_SPI_Transmit(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout)

/*SPI接收数据函数*/
HAL_StatusTypeDef HAL_SPI_Receive(SPI_HandleTypeDef *hspi, uint8_t *pData, uint16_t Size, uint32_t Timeout)

5、烧录验证

5.1、实验具体流程

“初始化三个按键 -> 配置USART1 -> 配置SPI1 -> 在工程中添加w25flash.c/w25flash.h驱动文件 -> spi.c文件中实现写入/读取测试函数Flash_TestWrite()/Flash_TestRead() -> 主循环中根据按键响应不同操作”

5.2、实验现象

烧录程序,开发板上电后首先读取FLASH芯片的ID,并通过串口显示给用户,然后输出操作提示,按下KEY0按键会擦除块0内容,擦除后按下KEY1按键读取内容会发现全是FF,然后按下KEY2按键将数据写入,此时再按下KEY1按键读取内容会发现和我们写入的内容一致,如下图所示为整个过程串口详细输出信息

6、注释详解

注释1:图片来源多路SPI从设备连接方法--技术天地

注释2:图片来源STM32Cube高效开发教程(基础篇)

注释3:W25Q128FV Datasheet

注释4:驱动代码来源STM32Cube高效开发教程(基础篇)

参考资料

STM32Cube高效开发教程(基础篇)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1365747.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

互联网干洗店洗鞋店搭建一套私域小程序有哪些优势?

在快节奏的现代生活中&#xff0c;我们常常面临衣物堆积如山、时间却捉襟见肘的困境。 干洗店在中国各大城市随处可见&#xff0c;假如每位顾客每月都需要一套干洗服务&#xff0c;那么一个50万人口的城市每月就有50万套干洗需求。若每家店日均处理100套衣物&#xff0c;那么至…

AE (4)_ 直方图调整的理论

#灵感# 在短暂的高通平台调试中&#xff0c;很看重直方图调整的理解。后来其它平台&#xff0c;不怎么调整这个了。但还是记录一下。 我个人还是倾向 招式简单&#xff0c;但应用到极致。 绝大部分内容来自&#xff1a;刘斯宁&#xff0c;Image Enhancement - CLAHE - 知乎 (z…

图论算法(数学建模)算法以后更新

无权值&#xff0c;无向&#xff0c;当成1就行 有向 有向赋权 顶点度的概念 Dijkstra算法 Dijkstra算法能求-一个顶点到另一-顶点最短路径。它是由Di jkstra于1959年提出的。实际它能出始点到其它所有顶点的最短路径Dijkstra算法是一种标号法:给赋权图的每一一个顶点记一个数&a…

特斯拉难挽倒退?比亚迪为中国汽车市场改写历史

对于电动汽车这个新兴产业&#xff0c;特斯拉长期以来一直处于领头羊的位置&#xff0c;近年来也面临诸多测试。去年底欧洲报道特斯拉在瑞典遭遇罢工冲击&#xff0c;运营陷入诸多困扰&#xff0c;实在出人意料。更让人讶异的是&#xff0c;年终宣布新王者比亚迪在全球销量首次…

【前端设计】文字聚光灯

欢迎来到前端设计专栏&#xff0c;本专栏收藏了一些好看且实用的前端作品&#xff0c;使用简单的html、css语法打造创意有趣的作品&#xff0c;为网站加入更多高级创意的元素。 案例 文字聚光灯效果可以用于网站标题 html <!DOCTYPE html> <html lang"en&quo…

Unity组件开发--短连接HTTP

1.网络请求管理器 using LitJson; using Cysharp.Threading.Tasks; using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.Networking; using UnityEngine.Events;using System.Web; using System.Text; using Sy…

SSM框架学习笔记01 | 注解开发

文章目录 1. 注解形式定义bean2.纯注解开发3.bean管理4. 依赖注入5. 第三方bean管理总结 1. 注解形式定义bean Compoenet ControllerServiceRepository 配合代码块 <context:component-scan /> 使用 2.纯注解开发 Configuration ComponentScan AnnotationConfigApplicati…

【开源】基于JAVA的教学过程管理系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 教师端2.2 学生端2.3 微信小程序端2.3.1 教师功能如下2.3.2 学生功能如下 三、系统展示 四、核心代码4.1 查询签到4.2 签到4.3 查询任务4.4 查询课程4.5 生成课程成绩 六、免责说明 一、摘要 1.1 项目介绍 基于JAVAVu…

国科大计算机体系结构期末考试——更新中

题型一、第二章的画图 给一个逻辑表达式&#xff0c;画出晶体管级别的电路图 cmos电路的基本电路&#xff1a; 与非门的功能是对多个输入信号进行逻辑与操作&#xff0c;然后对结果进行取反。 或非门的功能是对多个输入信号进行逻辑或操作&#xff0c;然后对结果进行取反。 …

链表:两个一组,反转链表

1、针对单链表&#xff0c;当我们进行操作时&#xff0c;如果需要进行反转或者进行其他操作时&#xff0c;有链表断开的情况&#xff0c;不妨考虑下使用辅助指针来记录断开后的链表位置&#xff0c;将需要处理的数据处理好后&#xff0c;可以使用此辅助指针找到链表的位置 #in…

服务器故障与管理口与raid

一&#xff0c;服务器常见故障 1&#xff0c;系统不停重启进入不了系统 排查是否是硬件故障&#xff0c;系统盘是否损坏&#xff08;硬盘灯红色&#xff0c;黄色&#xff0c;绿色&#xff09; 查看系统第一启动项是那种方式(硬盘 网络网卡 光驱 U盘) bios 是否双系统&#x…

使用命令行方式搭建uni-app + Vue3 + Typescript + Pinia + Vite + Tailwind CSS + uv-ui开发脚手架

使用命令行方式搭建uni-app Vue3 Typescript Pinia Vite Tailwind CSS uv-ui开发脚手架 项目代码以上传至码云&#xff0c;项目地址&#xff1a;https://gitee.com/breezefaith/uniapp-vue3-ts-scaffold 文章目录 使用命令行方式搭建uni-app Vue3 Typescript Pinia V…

C# ReaderWriterLock类学习

前言 今天这篇文章我们来学习一下ReaderWriterLock类&#xff0c;ReaderWriterLock类定义了实现单写程序和多读程序语义的锁。这个类主要用于文件操作&#xff0c;即多个线程可以读取文件&#xff0c;但只能用一个线程来更新文件。使用ReaderWriterLock类时&#xff0c;任意数…

并发(9)

目录 50.AQS的核心思想是什么&#xff1f; 51.AQS有哪些核心方法&#xff1f; 52.AQS定义什么样的资源获取方式&#xff1f; 53.AQS底层使用了什么样的设计模式&#xff1f; 54.什么是可重入&#xff0c;什么是可重入锁&#xff1f;他用来解决什么问题&#xff1f; 55.Ree…

wireshark抓包分析HTTP协议,HTTP协议执行流程,

「作者主页」&#xff1a;士别三日wyx 「作者简介」&#xff1a;CSDN top100、阿里云博客专家、华为云享专家、网络安全领域优质创作者 「推荐专栏」&#xff1a;对网络安全感兴趣的小伙伴可以关注专栏《网络安全入门到精通》 使用WireShark工具抓取「HTTP协议」的数据包&#…

超自动化助力企业财务转型升级

在快节奏的财务规划与分析环境中&#xff0c;传统的预算方法虽长期以来一直是企业制定有效决策的支柱&#xff0c;但已不足以驾驭当今复杂的商业环境。不断的经济变化、市场的不确定性以及利益相关者的需求增加促使企业寻求更敏捷的解决方案。如今&#xff0c;部分企业开始尝试…

关键字、标志符、变量、基本数据类型

1、关键字 1.1、定义 定义&#xff1a;被JAVA语言赋予了特殊含义&#xff0c;用作专门用途的字符串&#xff08;或单词&#xff09; 特点&#xff1a;全部关键字都是小写字母 上源码&#xff1a; 代码中定义类的关键字class&#xff0c;定义一个订单控制器类 ​​​​​​​…

try catch 应该在 for 循环里面还是外面

正文 首先 &#xff0c; 话说在前头&#xff0c; 没有什么 在里面 好 和在外面好 或者 不好的 一说。 本篇文章内容&#xff1a; 使用场景 性能分析 个人看法 1. 使用场景 为什么要把 使用场景 摆在第一个 &#xff1f; 因为本身try catch 放在 for循环 外面 和里面 …

7N65-ASEMI高压NPN型MOS管7N65

编辑&#xff1a;ll 7N65-ASEMI高压NPN型MOS管7N65 型号&#xff1a;7N65 品牌&#xff1a;ASEMI 连续漏极电流(Id)&#xff1a;4A 漏源电压(Vdss)&#xff1a;650V 栅极阈值电压&#xff1a;30V 单脉冲雪崩能量&#xff1a;150mJ 集电极电流&#xff08;脉冲&#xff…

【分布式微服务专题】SpringSecurity快速入门

目录 前言阅读对象阅读导航前置知识笔记正文一、Spring Security介绍1.1 什么是Spring Security1.2 它是干什么的1.3 Spring Security和Shiro比较 二、快速开始2.1 用户认证2.1.1 设置用户名2.1.1.1 基于application.yml配置文件2.1.1.2 基于Java Config配置方式 2.1.2 设置加密…