1、准备材料

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

STM32CubeMX软件(Version 6.10.0

野火DAP仿真器

keil µVision5 IDE(MDK-Arm

ST-LINK/V2驱动

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教程19 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高效开发教程(基础篇)

更多内容请浏览 STM32CubeMX+STM32F4系列教程文章汇总贴

STM32CubeMX教程20 SPI - W25Q128驱动的更多相关文章

  1. linux内核SPI总线驱动分析(一)(转)

    linux内核SPI总线驱动分析(一)(转) 下面有两个大的模块: 一个是SPI总线驱动的分析            (研究了具体实现的过程) 另一个是SPI总线驱动的编写(不用研究具体的实现过程) ...

  2. Linux内核中SPI总线驱动分析

    本文主要有两个大的模块:一个是SPI总线驱动的分析 (研究了具体实现的过程): 另一个是SPI总线驱动的编写(不用研究具体的实现过程). 1 SPI概述 SPI是英语Serial Peripheral ...

  3. [译]Vulkan教程(20)重建交换链

    [译]Vulkan教程(20)重建交换链 Swap chain recreation 重建交换链 Introduction 入门 The application we have now success ...

  4. 《物联网框架ServerSuperIO教程》-19.设备驱动和OPC Client支持mysql、oracle、sqlite、sqlserver的持久化。v3.6.4版本发布

    19.设备驱动和OPC Client支持mysql.oracle.sqlite.sqlserver的持久化 19.1     概述 ServerSuperIO支持设备驱动和OPC Client采集的数 ...

  5. SPI OLED 驱动

    根据之前说过的 SPI 驱动的框架,在我们添加 SPI 设备驱动的时候需要与 SPI Master 完成匹配,通过 spi_register_board_info 进行注册. 构造设备 static ...

  6. SPI protocol驱动编写实例

    内核版本:3.9.5 Linux中SPI驱动有俩个部分组成:controller驱动,直接和底层硬件打交道,protocol驱动,针对特定的设备,也是我们要做的. 这里只考虑SPI protocol驱 ...

  7. [转帖]Linux教程(20)- Linux中的Shell变量

    Linux教程(20)- Linux中的Shell变量 2018-08-24 11:30:16 钱婷婷 阅读数 37更多 分类专栏: Linux教程与操作 Linux教程与使用   版权声明:本文为博 ...

  8. Directx11教程(20) 一个简单的水面

    原文:Directx11教程(20) 一个简单的水面 nnd,以前发的这篇教程怎么没有了?是我自己误删除了,还是被系统删除了? 找不到存稿了,没有心情再写一遍了.      简单说一下,本篇教程就是实 ...

  9. RT Thread的SPI设备驱动框架的使用以及内部机制分析

    注释:这是19年初的博客,写得很一般,理解不到位也不全面.19年末得空时又重新看了RTThread的SPI和GPIO,这次理解得比较深刻.有时间时再整理上传. -------------------- ...

  10. 《连载 | 物联网框架ServerSuperIO教程》- 3.设备驱动介绍

    1.C#跨平台物联网通讯框架ServerSuperIO(SSIO)介绍 <连载 | 物联网框架ServerSuperIO教程>1.4种通讯模式机制. <连载 | 物联网框架Serve ...

随机推荐

  1. 年度盘点,四年的精华合集「GitHub 热点速览」

    今年是 GPT 年,无论是 GitHub 还是朋友圈还是技术平台,即便你不关心 GPT 的发展情况,同大模型.AI 相关的项目总能进入你的信息流.到这期为止,热度速览也连载了四年,从一开始习惯看 Gi ...

  2. 如何通过port-forward命令在本地访问 k8s 集群服务

    公众号「架构成长指南」,专注于生产实践.云原生.分布式系统.大数据技术分享 概述 在我们访问k8s中的pod服务时,一般通过node port映射pod端口进行访问,还有一种是通过ingress或者i ...

  3. spring是否线程安全

    spring 管理的bean默认是单例的,可通过 scope 属性设置scope="singleton" 默认是单例,可修改为scope="prototype" ...

  4. springboot整合mybatis步骤思路

    /** * springboot整合mybatis步骤思路 * 依赖导入 * 建表 * 实体类 * mapper配置文件 * mapper接口 * yaml配置 * properties配置数据库连接 ...

  5. ASR项目实战-交付过程中遇到的疑似内存泄漏问题

    基于Kaldi实现语音识别时,需要引入一款名为OpenFST的开源软件,本文中提到的内存问题,即和这款软件相关. 考虑到过程比较曲折,内容相对比较长,因此先说结论. 在做长时间的语音识别时,集成了Ka ...

  6. 在Linux上部署.net Core 步骤以及遇到的一些问题

    Linux安装部署手册 一.安装.NET Core SDK centos 7 系统命令为: sudo rpm -Uvh https://packages.microsoft.com/config/ce ...

  7. 玩转Python:数据可视化,一个很高级的交互式Python库,附代码

    在数据科学和分析的世界里,将数据可视化是至关重要的一步,它能帮助我们更好地理解数据,发现潜在的模式和关系.Python 提供了多种可视化工具,HvPlot 是其中一个出色的库,专为简单且高效的交互式可 ...

  8. gentoo安装gcc出现error: C compiler cannot create executables

    安装程序  systemd 过程中,出现了error: C compiler cannot create executables 这类错误,经过检查,由于没有配置本地编译器的结果. 输入命令如下: g ...

  9. 5分钟就能实现的API监控,有什么理由不做呢?基调听云

    API深度影响着你的应用 今天的数字应用世界其实是一个以API为中心的世界,我们只是没有意识到这些API的重要性.比如在电子商务交易.社交媒体等对交互高度依赖的领域,可以说API决定了应用的质量一点也 ...

  10. [LitCTF 2023]1zjs

    打开环境: 一个魔方♂ 习惯性打开 F12,之后发现有个./dist/index.umd.js Ctrl u 打开 把这个蓝色的点开 0.o? 这里眼神好的话就能看到有个" /f@k3f1a ...