细说STM32F407单片机轮询方式读写SPI FLASH W25Q16BV
目录
一、工程配置
1、时钟、DEBUG
2、GPIO
3、SPI2
4、USART6
5、NVIC
二、软件设计
1、FALSH
(1)w25flash.h
(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输出低电平时灯亮 | 上拉 |
LED3 | D3 | PB6 | GPIO_Output | 推挽输出 | High,MCU输出低电平时灯亮 | 上拉 |
LED4 | D4 | PC6 | GPIO_Output | 推挽输出 | High,MCU输出低电平时灯亮 | 上拉 |
KeyUp | S2 | PA0 | GPIO_Input | 输入 | 按键输入低电平 | 上拉 |
KeyDown | S3 | PD3 | GPIO_Input | 输入 | 按键输入低电平 | 上拉 |
KeyLeft | S4 | PF7 | GPIO_Input | 输入 | 按键输入低电平 | 上拉 |
KeyRight | 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
/* 文件: 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()逐个页写入数据。
/* 文件: 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关于指示灯的定义,其它不变:
#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
/* 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
/* USER CODE BEGIN 0 */
#include "w25flash.h"
#include <string.h> //用到函数strlen()
#include <stdio.h>
/* USER CODE END 0 */
/* 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
/* USER CODE BEGIN Includes */
#include "w25flash.h" //W25Q16驱动程序.h
#include "keyled.h"
#include <stdio.h>
/* USER CODE END Includes */
/* 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 */
/* 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 */
/* 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键,对芯片进行读操作,并打印;
上述操作结果,如下图: