目录
STM32CubeMX软件(Version 6.10.0)
keil μVision5 IDE(MDK-Arm)
CH340G Windows系统驱动程序(CH341SER.EXE)
逻辑分析仪nanoDLA
使用STM32CubeMX软件配置STM32F407开发板的SPI1与W25Q128芯片通信,以轮询方式读写W25Q128 FLASH芯片,并通过USART1输出相关信息,具体为使用开发板上的三个用户按键KEY0/1/2,分别实现对W25Q128芯片写数据/读数据/擦除数据的操作,操作过程中与用户的交互由USART1输出信息来实现
本实验重点是理解标准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通信采集到的时序
请先阅读“STM32CubeMX教程1 工程建立”实验3.4.1小节配置RCC和SYS
系统时钟树均设置为STM32F407总线能达到的最高时钟频率,无需启动LSE,与上个实验一致,具体配置如下图所示
此实验主要是利用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速度非常高,具体配置如下图所示
本实验无需启用中断,如果需要启用SPI1的中断,请单击System Core/NVIC,然后根据需求勾选SP1全局中断,并选择合适的中断优先级即可,具体配置如下图所示
请先阅读“STM32CubeMX教程1 工程建立”实验3.4.3小节配置Project Manager
单击页面右上角GENERATE CODE生成工程
在生成的工程代码主函数中新增了MX_SPI1_Init()函数,在该函数中实现了对SPI1的模式及参数配置
在MX_SPI1_Init()函数中调用了HAL_SPI_Init()函数使用配置的参数对SPI1进行了初始化
在HAL_SPI_Init()函数中又调用了HAL_SPI_MspInit()函数对SPI1引脚复用设置,SPI1时钟使能,如果开启了中断该函数中还会有中断相关设置及使能
具体的SPI1初始化函数调用流程如下图所示
本实验无需中断,因此未启动任何SPI1的中断
需要添加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);
/*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)
“初始化三个按键 -> 配置USART1 -> 配置SPI1 -> 在工程中添加w25flash.c/w25flash.h驱动文件 -> spi.c文件中实现写入/读取测试函数Flash_TestWrite()/Flash_TestRead() -> 主循环中根据按键响应不同操作”
烧录程序,开发板上电后首先读取FLASH芯片的ID,并通过串口显示给用户,然后输出操作提示,按下KEY0按键会擦除块0内容,擦除后按下KEY1按键读取内容会发现全是FF,然后按下KEY2按键将数据写入,此时再按下KEY1按键读取内容会发现和我们写入的内容一致,如下图所示为整个过程串口详细输出信息
注释1:图片来源多路SPI从设备连接方法--技术天地
注释4:驱动代码来源STM32Cube高效开发教程(基础篇)