STM32CubeMX教程20 SPI - W25Q128驱动

目录

1、准备材料

2、实验目标

3、实验流程

3.0、前提知识

3.1、CubeMX相关配置

3.1.0、工程基本配置

3.1.1、时钟树配置

3.1.2、外设参数配置

3.1.3、外设中断配置

3.2、生成代码

3.2.0、配置Project Manager页面

3.2.1、外设初始化调用流程

3.2.2、外设中断调用流程

3.2.3、添加其他必要代码

4、常用函数

5、烧录验证

6、注释详解

参考资料


读者可访问 GitHub - lc-guo/STM32CubeMX-Series-Tutorial 获取原始工程代码


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相关配置

3.1.0、工程基本配置

打开STM32CubeMX软件,单击ACCESS TO MCU SELECTOR选择开发板MCU(选择你使用开发板的主控MCU型号),选中MCU型号后单击页面右上角Start Project开始工程,具体如下图所示

开始工程之后在配置主页面System Core/RCC中配置HSE/LSE晶振,在System Core/SYS中配置Debug模式,具体如下图所示

详细工程建立内容读者可以阅读“STM32CubeMX教程1 工程建立

3.1.1、时钟树配置

系统时钟使用8MHz外部高速时钟HSE,HCLK、PCLK1和PCLK2均设置为STM32F407能达到的最高时钟频率,具体如下图所示

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、生成代码

3.2.0、配置Project Manager页面

单击进入Project Manager页面,在左边Project分栏中修改工程名称、工程目录和工具链,然后在Code Generator中勾选“Gnerate peripheral initialization as a pair of 'c/h' files per peripheral”,最后单击页面右上角GENERATE CODE生成工程,具体如下图所示

详细Project Manager配置内容读者可以阅读“STM32CubeMX教程1 工程建立”实验3.4.3小节

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、烧录验证

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

6、注释详解

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

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

注释3W25Q128FV Datasheet

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

参考资料

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

  • 34
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
根据引用\[1\]中提供的信息,可以使用STM32的HAL库来模拟SPI方式驱动W25Q128存储芯片。首先需要进行模拟SPI方式的IO配置,然后使用相应的驱动代码来实现功能。 W25Q128是一款SPI接口的NOR Flash芯片,具有128 Mbit的存储空间,相当于16M字节。NOR Flash是一种常用的用于存储数据的半导体器件,具有容量大、可重复擦写、按扇区/块擦除、掉电后数据可继续保存的特性。Flash的物理特性是只能写0,不能直接写1,写1需要进行擦除操作。 根据引用\[3\]中的实验,可以通过硬件接线将W25Q128模块与STM32连接起来,其中VCC接3.3V,CS接PA4,CLK接PA5,DO接PA6,DI接PA7。然后可以使用CubeMX进行相应的配置。 要获取W25Q128的ID,可以使用SPI通信协议来读取芯片的ID寄存器。具体的代码实现可以参考引用\[1\]中提供的驱动代码。 #### 引用[.reference_title] - *1* [STM32CubeMX | STM32使用HAL库模拟SPI方式驱动W25Q128存芯片](https://blog.csdn.net/qq153471503/article/details/106895933)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control,239^v3^insert_chatgpt"}} ] [.reference_item] - *2* *3* [STM32SPIW25Q128](https://blog.csdn.net/weixin_49001476/article/details/130909856)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^control,239^v3^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值