细说STM32F407单片机轮询方式读写SPI FLASH W25Q16BV

目录

一、工程配置

1、时钟、DEBUG

2、GPIO

3、SPI2

4、USART6

5、NVIC

二、软件设计

1、FALSH

(1)w25flash.h

[(2) w25flash.c](#(2) w25flash.c)

1)W25Q16基本操作指令

2)计算地址的辅助功能函数

3)器件、块、扇区擦除函数

4)存储区读写函数

2、KEY_LED

3、spi.h

4、spi.c

5、main.c

三、下载与运行


本文旨在说明STM32F407单片机通过SPI2扩展FLASH W25Q16BV,对FLASH进行读ID、写操作、读操作、擦除操作。使用的是旺宝红龙开发板STM32F407ZGT6 KIT V1.0。使用开发板上的按键S2、S3、S4、S5、S6依此执行擦除芯片、擦除BLOCK0、写、读操作、MCU复位。使用开发板上的D1、D2、D3、D4作为动作指示灯。通过串口USART6把操作状态显示到串口助手上。资源利用详见工程配置。

工程里创建FLASH目录,包含w25flash.c、w25flash.h。

工程依旧创建KEY_LED目录,包含keyled.c、keyled.h。关于如何在工程中创建目录并添加文件以及keyled.c、keyled.h详见作者的其他文章。

其它资料可以参考本文作者的其他文章:细说STM32F407单片机SPI基础知识_stm32f407 spi2-CSDN博客 https://wenchm.blog.csdn.net/article/details/144376346

细说Flash存储芯片W25Q128FW和W25Q16BV_flash拓展地址寄存器0xc5-CSDN博客 https://wenchm.blog.csdn.net/article/details/144398492

一、工程配置

1、时钟、DEBUG

外部时钟,25MHz,设置到HCLK=168MHz,PCLK1=42MHz,PCLK2=84MHz,其它,都设置成168MHz。

DEBUG,选择serial wire。

2、GPIO

项目工程使用了开发板的按键S2、S3、S4、S5、S6,和开发板上的LED灯D1、D2、D3、D4。详细配置如下表:

|-------------------|---------|----------|-------------|------------|------------------|----------------|
| 用户标签 | 开发板 | 引脚名称 | 引脚功能 | GPIO模式 | 默认电平 | 上拉 或下拉 |
| LED1 | D1 | PA6 | GPIO_Output | 推挽输出 | High,MCU输出低电平时灯亮 | 上拉 |
| LED2 | D2 | PA4 | GPIO_Output | 推挽输出 | High,MCU输出低电平时灯亮 | 上拉 |
| LED 3 | D3 | PB6 | GPIO_Output | 推挽输出 | High,MCU输出低电平时灯亮 | 上拉 |
| LED 4 | D4 | PC6 | GPIO_Output | 推挽输出 | High,MCU输出低电平时灯亮 | 上拉 |
| KeyUp | S2 | PA0 | GPIO_Input | 输入 | 按键输入低电平 | 上拉 |
| Key Down | S3 | PD3 | GPIO_Input | 输入 | 按键输入低电平 | 上拉 |
| KeyLeft | S4 | PF7 | GPIO_Input | 输入 | 按键输入低电平 | 上拉 |
| Key Right | S5 | PF6 | GPIO_Input | 输入 | 按键输入低电平 | 上拉 |
| | S6 | NRST | GPIO_Input | 输入 | 按键复位 | 上拉 |

3、SPI2

MCU的SPI2与FLASH的连接原理图,详见参考文章。管脚配置见下表:

|-----------|----------------|----------|---------------|--------------------|----------|----------------|
| 用户标签 | 开发板 标识 | 引脚名称 | 引脚功能 | GPIO模式 | 默认电平 | 上拉 或下拉 |
| FLASH_CS | FLASH_CS | PC13 | GPIO_Output片选 | 推挽输出 | High | 上拉 |
| SPI2_SCK | SPI_SCK | PB10 | 时钟 | Alternate Function | High | No |
| SPI2_MOSI | SPI_MOSI | PB15 | MOSI数据线 | Alternate Function | High | No |
| SPI2_MISO | SPI_MISO | PB14 | MISO数据线 | Alternate Function | High | No |

4、USART6

115200、8、None、1,其它参数默认。占用管脚PG14、PG9。

5、NVIC

不设置中断,但把Time Base的优先级修改为0。

二、软件设计

1、FALSH

创建一个名为FLASH的子目录,创建文件w25flash.h和w25flash.c,并将其存放在这个子目录里。 将W25Q16常用的一些功能编写为函数,也就是实现W25Q16常用操作指令,例如擦除芯片、擦除扇区、读取数据、写入数据等。

注意W25Q16驱动程序与SPI接口的HAL驱动程序的区别。SPI的HAL驱动程序实现了SPI接口数据传输的基本功能,是SPI硬件层的驱动;而W25Q16驱动程序则是根据W25Q16的指令定义,实现器件具体功能操作的一系列函数。W25Q16驱动程序要用到SPI硬件层的HAL驱动程序,要通过SPI的HAL驱动程序实现数据帧的收发。

W25Q16驱动程序涉及的硬件接口包括SPI接口和CS信号,驱动程序应该能很容易地移植到其他电路板上,所以在文件开头定义了硬件接口相关的宏。

  • 为CS信号连接的GPIO引脚定义了表示GPIO端口和引脚号的宏CS_PORT和CS_PIN,定义了两个宏函数__Select_Flash()和__Deselect_Flash()用于对CS置位和复位。
  • W25Q16器件连接的SPI接口定义为宏SPI_HANDLE,这里指向文件spi.h中的变量hspi2,也就是表示SPI2接口的外设对象变量。在驱动程序的所有函数内部都使用宏SPI_HANDLE表示SPI2接口。

这样定义硬件接口后,如果要将这个驱动程序移植到其他开发板上操作W25Q16,只需修改这3个宏的定义即可。

文件w25flash.h中的函数分为几组,这些函数就是W25Q16常用指令的实现。文件w25flash.c的全部代码有400多行,这里就不全部显示出来了,只选择其中一些典型的函数进行解释说明。

文件w25flash.h是W25Q16驱动程序的头文件,定义了一些宏和函数。这个文件的完整代码如下:

(1)w25flash.h

cpp 复制代码
/* 文件: w25flash.h
 * 功能描述: Flash 存储器W25Q16的驱动程序
 * 作者:
 * 移植:
 * 修改日期:2019-06-05
 * 移植日期:2024-12-09
 * W25Q16BV 芯片参数: 2M字节,24位地址线
 * 分为32个Block,每个Block 64K字节
 * 一个Block又分为16个Sector,共512个Sector,每个Sector 4K字节
 * 一个Sector又分为16个Page,共8192个Page,每个Page 256字节
 * 写数据操作的基本单元是Page,一次连续写入操作不能超过一个Page的范围。写的Page必须是擦除过的。
 */

#ifndef _W25FLASH_H
#define _W25FLASH_H

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

/* W25Q16硬件接口相关的部分:CS引脚和SPI接口 ,若电路不同,更改这部分配置即可*/
// Flash_CS -->PC13, 片选信号CS操作的宏定义函数
#define CS_PORT GPIOC
#define	CS_PIN GPIO_PIN_13
#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存储芯片W25Q16的存储容量参数================
#define FLASH_PAGE_SIZE 256		//一个Page是256字节

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

#define	FLASH_SECTOR_COUNT 512	//总共512个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();												//SPI接口接收一个字节
HAL_StatusTypeDef SPI_ReceiveBytes(uint8_t* pBuffer, uint16_t byteCount);	//SPI接口接收多个字节

//=========2. W25Qxx 基本控制指令==========
// 0xEF14,表示芯片型号为W25Q16BV, Winbond,0x90指令
// 0xEF16,表示芯片型号为W25Q64JV, Winbond,0x90指令
// 0xEF17,表示芯片型号为W25Q128JV, Winbond,0x90指令
// 根据FLASH数据修改此处,比如:
// 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
// W25Q16无此指令
// 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  绝对编号获取地址,共32个Block
uint32_t Flash_Addr_byBlock(uint8_t BlockNo);
//根据Sector 绝对编号获取地址,共512个Sector
uint32_t Flash_Addr_bySector(uint16_t  SectorNo);
//根据Page  绝对编号获取地址,共8192个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, 擦除整个器件,大约4秒
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,  const uint8_t* pBuffer, uint16_t byteCount);

#endif

定义了4个SPI基本发送和接收函数,用于传输1字节或多字节,接收1字节或多字节。这几个函数实际上就是调用了SPI的HAL驱动程序中阻塞式数据传输函数HAL_SPI_Transmit()和HAL SPI_Receive()。在封装为W25Q16驱动程序的函数时,内部直接使用宏SPI_HANDLE替代了具体的hspil,使用宏定义常量MAX_TIMEOUT作为超时等待时间,这样可以简化函数的调用,因为这几个基本的传输函数在其他函数里被大量调用。

(2) w25flash.c

1)W25Q16基本操作指令

W25Q16的每个函数对应于W25Q16的一个指令。例如:

读器件ID的函数Flash ReadID()就是实现了指令码为0x90的指令;FlashWrite_Enable()函数实现了"写使能"(指令码0x06)指令;Flash_Wait_Busy()函数读取状态寄存器SR1,判断BUSY位是否为0,直到BUSY为0时才退出。

Flash_Write_Enable()和Flash_Wait_Busy()是在其他指令操作函数里经常用到的,例如擦除芯片、擦除扇区、写数据等操作之前必须执行"写使能"指令。一些比较耗时间的操作执行后,必须等待状态寄存器SR1的BUSY位变为0,也就是需要调用函数Flash_Wait_Busy()。

这些函数的代码解释了通过一个函数实现一个指令操作的方法,它们就是根据指令定义以及相应的指令时序图,通过片选信号CS的控制以及SPI接口的字节数据发送和接收来实现一个指令的操作。例如,函数Flash_ReadID()执行查询芯片的制造商和器件ID的指令,指令码0x90。程序先执行宏函数__Select_Flash()使片选信号CS为低电平,从而开始一次SPI传输。然后,按照指令0x90的定义依次发送4个字节数据0x90、0x00、0x00、0x00,其中0x90是指令码,中间两个0x00是dummy字节,最后一个0x00是特定的。最后,W25Q16会返回2字节数据,依次接收这2字节数据,就能得到制造商和器件ID信息。

函数Flash_Wait_Busy()是用于判断状态寄存器SR1的BUSY位是否为0的,所以需要调用函数Flash_ReadSR1()读取状态寄存器SR1的内容,直到BUSY位变为0时,函数才退出。

2)计算地址的辅助功能函数

W25Q16的一些指令需要使用24位的绝对地址,例如块擦除、读数据等指令都需要提供24位绝对地址。直接记住或推算地址是比较麻烦的,在使用Flash的存储空间时,一般以块、扇区、页为单位进行管理,直接根据块、扇区、页的编号计算地址是比较实用的。所以,我们在驱动程序中定义了几个辅助函数,用于根据块、扇区、页的编号计算24位绝对地址,还可以将24位绝对地址分解为3字节数据,便于在指令中使用。

3)器件、块、扇区擦除函数

定义了器件擦除、块擦除和扇区擦除指令的操作函数,其中块擦除和扇区擦除指令需要起始地址。例如,扇区擦除的函数是Flash_EraseSector()。在擦除操作之前,必须执行"写使能"指令,使状态寄存器SR1的WEL位变为1,并且等待BUSY位变为0的时候,才能开始发送擦除操作指令。擦除指令发送结束后,器件执行擦除操作,在此期间BUSY位为1,需等待BUSY位变为0之后,才能退出函数。

执行擦除操作后的Flash存储区域数据为0xFF。向存储区域写数据时,必须是擦除后的区域才能写入数据,否则写入无效。所以,一个存储区域只能有效写入一次,下次再写入之前必须先擦除。从存储区读出数据的次数是无限制的。

4)存储区读写函数

"读数据"指令(指令码0x03)可以从任何一个24位地址开始读取1字节或连续多字节的数据,由此定义了两个函数Flash_ReadOneByte()和Flash_ReadBytes()。

写数据使用"页编程"指令(指令码0x02),由此定义函数Flash_WriteInPage()用于向一个页内写入数据。

使用函数Flash_ReadBytes(读取数据时,起始地址可以是任何地址,读取的数据长度也可以超过页的容量,也就是可以超过256字节,最多可连续读取65536字节。使用函数Flash_WriteInPage()写入数据时需要注意以下几点。

  • 一次的写数据操作是限定在一个页范围内的,所以一次写入数据长度最多256字节。
  • 起始地址可以是任何地址,但写数据的偏移地址超过页的边界后,会从该页的开始地址继续写。所以,起始地址为页的开始地址时,最多可写入256字节。
  • 写入数据的存储区域必须是擦除过的,也就是存储内容是0xFF,否则写入数据无效。所以一个页只能写入一次,下次再写之前,需要先擦除页。W25Q16擦除的最小单位是扇区。

驱动程序中还有一个函数Flash_WriteSector(),它可以从一个扇区的起始地址开始写入不超过64KB的数据。这个函数内部会先擦除需要用到的扇区,然后将数据按页的大小分解,调用Flash_WriteInPage()逐个页写入数据。

cpp 复制代码
/* 文件: w25flash.c
 * 功能描述: Flash 存储器W25Q16的驱动程序
 * 作者:
 * 移植:
 * 修改日期:2019-06-05
 * 移植日期:2024-12-10
 */


#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绝对编号获取地址, 共32个Block,BlockNo 取值范围0-31
//每个块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绝对编号获取地址, 共512个Sector, SectorNo取值范围0-511
//每个扇区4K字节,12位地址,扇区内地址范围0x000-0xFFF
uint32_t Flash_Addr_bySector(uint16_t SectorNo)
{
	if (SectorNo>511)	//不能超过511
		SectorNo=0;
//	uint32_t addr=SectorNo*0x1000;

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

//根据Page绝对编号获取地址,共8192个Page,  PageNo取值范围0-8191
//每个页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-31,内部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-31
// 一个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,表示芯片型号为W25Q16, Winbond,用过
// 0xC817,表示芯片型号为GD25Q16,ELM,用过
// 0x1C17,表示芯片型号为EN25Q16,台湾EON
// 0xA117,表示芯片型号为FM25Q16,复旦微电子
// 0x2018,表示芯片型号为N25Q16,美光
// 0x2017,表示芯片型号为XM25QH16,武汉新芯,用过

//读取芯片的制造商和器件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个字节 //why byte2?
	__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,  const 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 = (uint8_t*)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);
		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
}

2、KEY_LED

修改参考文章里keyled.h关于指示灯的定义,其它不变:

cpp 复制代码
#ifdef LED1_Pin	 	//LED1的控制
	#define LED1_Toggle()	HAL_GPIO_TogglePin(LED1_GPIO_Port,LED1_Pin)					//输出翻转
	#define LED1_ON()		HAL_GPIO_WritePin(LED1_GPIO_Port,LED1_Pin,GPIO_PIN_RESET) 	//输出0,亮
	#define LED1_OFF() 		HAL_GPIO_WritePin(LED1_GPIO_Port,LED1_Pin,GPIO_PIN_SET)		//输出1,灭
#endif

#ifdef LED2_Pin 	//LED2的控制
	#define LED2_Toggle()	HAL_GPIO_TogglePin(LED2_GPIO_Port,LED2_Pin)					//输出翻转
	#define LED2_ON()		HAL_GPIO_WritePin(LED2_GPIO_Port,LED2_Pin,GPIO_PIN_RESET)	//输出0,亮
	#define LED2_OFF()		HAL_GPIO_WritePin(LED2_GPIO_Port,LED2_Pin,GPIO_PIN_SET)		//输出1,灭
#endif
//新增LED3、LED4
#ifdef LED3_Pin 	//LED3的控制
	#define LED3_Toggle()	HAL_GPIO_TogglePin(LED3_GPIO_Port,LED3_Pin)					//输出翻转
	#define LED3_ON()		HAL_GPIO_WritePin(LED3_GPIO_Port,LED3_Pin,GPIO_PIN_RESET)	//输出0,亮
	#define LED3_OFF()		HAL_GPIO_WritePin(LED3_GPIO_Port,LED3_Pin,GPIO_PIN_SET)		//输出1,灭
#endif

#ifdef LED4_Pin 	//LED4的控制
	#define LED4_Toggle()	HAL_GPIO_TogglePin(LED4_GPIO_Port,LED4_Pin)					//输出翻转
	#define LED4_ON()		HAL_GPIO_WritePin(LED4_GPIO_Port,LED4_Pin,GPIO_PIN_RESET)	//输出0,亮
	#define LED4_OFF()		HAL_GPIO_WritePin(LED4_GPIO_Port,LED4_Pin,GPIO_PIN_SET)		//输出1,灭
#endif

3、spi.h

cpp 复制代码
/* USER CODE BEGIN Private defines */
void Flash_TestReadStatus(void);
void Flash_TestWrite(void);
void Flash_TestRead(void);

/* USER CODE END Private defines */

4、spi.c

cpp 复制代码
/* USER CODE BEGIN 0 */
#include "w25flash.h"
#include <string.h>	//用到函数strlen()
#include <stdio.h>
/* USER CODE END 0 */
cpp 复制代码
/* USER CODE BEGIN 1 */
//读取DeviceID,状态寄存器 SR1和SR2
void Flash_TestReadStatus(void)
{
//读DeviceID,SR1,SR2
	uint16_t devID = Flash_ReadID();	//读取器件ID
	printf("Device ID = %X\r\n",devID);	//大写的十六进制

	printf("The chip is: ");
	switch (devID)
	{
	case 0xEF14:
		printf("W25Q16BV \r\n");
		break;
	case 0xEF16:
		printf("W25Q64JV \r\n");
		break;
	case 0xEF17:
		printf("W25Q128JV \r\n");
		break;
	case 0xC817:
		printf("GD25Q128 \r\n");
		break;
	case 0x1C17:
		printf("EN25Q128 \r\n");
		break;
	case 0x2018:
		printf("N25Q128 \r\n");
		break;
	case 0x2017:
		printf("XM25QH128 \r\n");
		break;
	case 0xA117:
		printf("FM25Q128 \r\n");
		break;
	default:
		printf("Unknown type \r\n");
	}

	uint8_t SR1 = Flash_ReadSR1();		//Read SR1=0x00
	printf("Status Reg1 = %X\r\n",SR1);	//Hex显示

	uint8_t SR2 = Flash_ReadSR2();		//Read SR2=0x00
	printf("Status Reg2 = %X\r\n",SR2);	//Hex显示
}


//测试写入Page0 和Page1
//注意:一个Page写入之前必须是被擦除过的,写入之后就不能再重复写��??
void Flash_TestWrite(void)
{
	uint8_t blobkNo = 0;
	uint16_t sectorNo = 0;
	uint16_t pageNo = 0;
	uint32_t memAddress = 0;

	//写入Page0 两个string
	memAddress = Flash_Addr_byBlockSectorPage(blobkNo, sectorNo,pageNo);	//Page0 address
	uint8_t	bufStr1[] = "Hello from beginning";
	uint16_t len = 1+strlen((char*)bufStr1); 		//Include'\0'
	Flash_WriteInPage(memAddress,bufStr1,len);   	//Write data at the beginning of Page0.
	printf("Write in Page0:0 %s\r\n",bufStr1);

	uint8_t	bufStr2[] = "Hello in page";
	len = 1+strlen((char*)bufStr2); 				//include '\0'
	Flash_WriteInPage(memAddress+100,bufStr2,len);	//Offset address 100 within Page0
	printf("Write in Page0:100 %s\r\n",bufStr2);		//display string

//写入Page 1
	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 address
	Flash_WriteInPage(memAddress, bufPage, FLASH_PAGE_SIZE);//写一个Page
	printf("Write 0-255 in Page1 \r\n");
}

//test for reading Page0 and Page1
void Flash_TestRead(void)
{
	uint8_t	blobkNo = 0;
	uint16_t sectorNo = 0;
	uint16_t pageNo = 0;
//读取Page 0
	uint8_t bufStr[50];							//Data read from Page0
	uint32_t memAddress = Flash_Addr_byBlockSectorPage(blobkNo,sectorNo,pageNo);
	Flash_ReadBytes(memAddress,bufStr,50);		//read 50 Bytes
	printf("Read from Page0:0 %s\r\n",(char*)bufStr);	//display string

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

//读取Page 1
	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);
}
/* USER CODE END 1 */

5、main.c

cpp 复制代码
/* USER CODE BEGIN Includes */
#include "w25flash.h"	//W25Q16驱动程序.h
#include "keyled.h"
#include <stdio.h>
/* USER CODE END Includes */
cpp 复制代码
  /* USER CODE BEGIN 2 */
  //Read and display the values ​​of DeviceID, status register SR1 and SR2.
  Flash_TestReadStatus();

  // MCU output low level LED light is on
  LED1_OFF();
  LED2_OFF();
  LED3_OFF();
  LED4_OFF();
  /* USER CODE END 2 */
cpp 复制代码
/* USER CODE BEGIN 3 */
	KEYS curKey=ScanPressedKey(KEY_WAIT_ALWAYS);
	switch(curKey)
	{
	  case KEY_UP:	        //S2
	  	printf("Erasing chip, about 30sec...\r\n");
	  	Flash_EraseChip();
	  	printf("Chip is erased.\r\n");
        LED1_ON();
	  	LED2_OFF();
	  	LED3_OFF();
	  	LED4_OFF();
	  	break;

	  case KEY_DOWN:	    //S3
	  	printf("Erasing Block 0(256 pages)...\r\n");
	  	uint32_t globalAddr=0x000000;
	  	Flash_EraseBlock64K(globalAddr);
	  	printf("Block 0 is erased.\r\n");
        LED1_OFF();
	  	LED2_ON();
	  	LED3_OFF();
	  	LED4_OFF();
	  	break;

	  case KEY_LEFT:		//S4
	  	Flash_TestWrite();	//测试写入Page0 and Page1
        LED1_OFF();
	  	LED2_OFF();
	  	LED3_ON();
	  	LED4_OFF();
	  	break;

	  case KEY_RIGHT:		//S5
	  	Flash_TestRead();	//测试读取Page0 and Page1
        LED1_OFF();
	  	LED2_OFF();
	  	LED3_OFF();
	  	LED4_ON();
	  	break;
	  default:
        LED1_OFF();
		LED2_OFF();
		LED3_OFF();
		LED4_OFF();
	  	break;
	}

	printf("** Reselect menu or reset **\r\n");
	HAL_Delay(500);		    //Delay to eliminate key jitter.
  }
  /* USER CODE END 3 */
cpp 复制代码
/* USER CODE BEGIN 4 */

//串口打印
int __io_putchar(int ch)
{
	HAL_UART_Transmit(&huart6, (uint8_t*)&ch, 1, 0xFFFF);
	return ch;
}
/* USER CODE END 4 */

三、下载与运行

下载后,自动打印芯片ID为EF14,并打印状态寄存器为0;

按下S2键,对芯片整体格式化,并打印;

按下S3键,对BLOCK0格式化,并打印;

按下S4键, 对芯片进行写操作,并打印;

按下S5键,对芯片进行读操作,并打印;

上述操作结果,如下图:

相关推荐
良许Linux1 小时前
32岁入行STM32迟吗?
stm32·单片机·嵌入式硬件
m0_466607702 小时前
【STM32CubeMX】ST官网MCU固件库下载及安装
stm32·单片机·嵌入式硬件
Wallace Zhang6 小时前
STM32F103_Bootloader程序开发11 - 实现 App 安全跳转至 Bootloader
stm32·嵌入式硬件·安全
GodKK老神灭6 小时前
STM32 CCR寄存器
stm32·单片机·嵌入式硬件
杰克逊的日记9 天前
MCU编程
单片机·嵌入式硬件
Python小老六9 天前
单片机测ntc热敏电阻的几种方法(软件)
数据库·单片机·嵌入式硬件
懒惰的bit9 天前
STM32F103C8T6 学习笔记摘要(四)
笔记·stm32·学习
HX科技10 天前
STM32给FPGA的外挂FLASH进行升级
stm32·嵌入式硬件·fpga开发·flash·fpga升级
Suagrhaha10 天前
驱动入门的进一步深入
linux·嵌入式硬件·驱动
国科安芯10 天前
基于ASP4644多通道降压技术在电力监测系统中集成应用与发展前景
嵌入式硬件·硬件架构·硬件工程