开发板(正点原子stm32f407探索者开发板V2.4)
STM32CubeMX软件(Version 6.10.0)
野火DAP仿真器
keil µVision5 IDE(MDK-Arm)
ST-LINK/V2驱动
XCOM V2.6串口助手
逻辑分析仪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软件,单击ACCESS TO MCU SELECTOR选择开发板MCU(选择你使用开发板的主控MCU型号),选中MCU型号后单击页面右上角Start Project开始工程,具体如下图所示
开始工程之后在配置主页面System Core/RCC中配置HSE/LSE晶振,在System Core/SYS中配置Debug模式,具体如下图所示
详细工程建立内容读者可以阅读“STM32CubeMX教程1 工程建立”
系统时钟使用8MHz外部高速时钟HSE,HCLK、PCLK1和PCLK2均设置为STM32F407能达到的最高时钟频率,具体如下图所示
此实验主要是利用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全局中断,并选择合适的中断优先级即可,具体配置如下图所示
单击进入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小节
在生成的工程代码主函数中新增了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教程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);
/*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)
烧录程序,开发板上电后首先读取FLASH芯片的ID,并通过串口显示给用户,然后输出操作提示,按下KEY0按键会擦除块0内容,擦除后按下KEY1按键读取内容会发现全是FF,然后按下KEY2按键将数据写入,此时再按下KEY1按键读取内容会发现和我们写入的内容一致,如下图所示为整个过程串口详细输出信息
注释1:图片来源多路SPI从设备连接方法--技术天地
注释2:图片来源STM32Cube高效开发教程(基础篇)
注释3:W25Q128FV Datasheet
注释4:驱动代码来源STM32Cube高效开发教程(基础篇)
STM32Cube高效开发教程(基础篇)
更多内容请浏览 STM32CubeMX+STM32F4系列教程文章汇总贴