STM32学习27--SPI通信之W25Q64
2026/9/8 15:55:30 网站建设 项目流程

一、W25Q64详解

1、简介

W25Qxx是低成本的非易失性存储器,常用于数据存储、字库存储、固件程序存储等;

存储介质:Nor Flash(闪存);W25Q64是8M存储,3.3V供电,标准四线SPI,24位地址;

时钟频率:80MHz/160MHz(双重SPI)/324MHz(四重SPI);

2、硬件电路

注:CS:斜杆/或上横线,表示低电平有效;

PA5→SCK;PA6→MISO;

PA7→MOSI;PA4→CS(普通GPIO输出,软件控制);

WP、HOLD直接接3.3V,VCC接3.3V,电源就近加0.1uF滤波电容;

3、存储结构(Nor Flash特性)

总容量:8MB=8388608字节;24位地址(0x000000~0x7FFFFF);

1)页Page256字节,页编程0x02,一次最多写256个字节,写超过页边界,会覆盖本页开头,不能按字节写,写之前必须擦除;

2)扇区Sector:4KB(4096字节):最小擦除单元,扇区擦除指令:0x20,擦除后,全部变成0xFF,不能只擦除几个字节。

改1个字节要把整个4KB扇区读出→修改→擦除→回写;

3)块Block:64KB,块擦除:0xD8,一次擦除64KB;

4)全片擦除0xC7:擦完整8MB,耗时约十几秒,谨慎使用;

5)NOR Flash铁律:

擦除:1→0:写;只能把1写成0,不能把0写成1;

想写新数据,必须先擦除对应扇区,擦完才全是0xFF

4、SPI时序模式

W25Q64支持SPI Mode0(CPOL=0,CPHA=0)模式

主机模式、双线全双工、8bit、MSB First(高位先行);

SPI1挂载APB2(72MHz),预分频建议4→18MHz,最高不超过20MHz。

5、最常用指令

指令码指令名称作用
0x9FJEDEC_ID读芯片ID,W25Q64返回0xEF 0x40 0x17,用来判断芯片是否正常
0x06Write Enable写使能写/擦除之前必须先发这条指令
0x05Read Status Register读状态寄存器bit0 =BUSY;1=正在擦写;0=空闲,必须轮询等待;
0x03Read Data读数据读flash,命令+3字节地址,之后连续读字节,地址自动递增;
0x02Page Program页编程写数据:命令+3字节地址+最多256字节数据
0x20Sector Erase扇区擦除擦除4KB,最小擦除单位;
0xD8Block Erase块擦除擦除64KB
0xC7Chip Erase全片擦除擦全部8MB

注:

1)状态寄存器(STATUS REGISTER):BUSY是状态寄存器的只读第0位(S0),当芯片正在执行:页编程Page Program(写数据)、扇区擦除、块擦除、全片擦除、写状态寄存器的时候(不是瞬间完成)→BUSY=1(忙)。芯片忙的时候,除读状态寄存器和擦除暂停指令外,其他所有指令全部忽略。

等写、擦除动作全部完成之后,硬件自动把BUSY置0→芯片空闲,可接受新指令。

2)Write Enable写使能

页编程、扇区擦除、块擦除、全片擦除、写状态寄存器之前,必须先写使能;

写使能时序:CS拉低→SPI发送1字节命令0x06→CS拉高;只需要发送这一个字节,不需要地址,不需要数据;

void W25Q64_WriteEnable(void) { MySPI_Start(); MySPI_SwapByte(W25Q64_WRITE_ENABLE); MySPI_Stop(); }

每次做擦除/页写,都有重新调用一次Write Enable。

不管是页编程结束、擦除结束,芯片硬件会自动把WEL位清零,不是开一次写使能就永久生效,干完一次写/擦除,WEL就清零,下一次操作必须重新发0x06。

即:Write Enable→扇区擦除→WaitBusy→Write Enable→页写→WaitBusy;

读指令不需要写使能;

发送完0x06后,CS必须拉高,WEL才会置1;如果一直CS拉低,WEL不会生效;

扇区擦除代码:

void W25Q64_SectorErase(uint32_t Address) { W25Q64_WriteEnable();//擦除前,必须写使能 MySPI_Start(); MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB);//扇区擦除命令 MySPI_SwapByte((Address>>16)&0xFF); MySPI_SwapByte((Address>>8)&0xFF); MySPI_SwapByte((Address)&0xFF); MySPI_Stop(); W25Q64_WaitBusy(); //等待擦除完成,WEL自动清零 }

一句话总结:只要是改动Flash(写、擦、改寄存器),必须先发0x06,只读不用。每一次写/擦操作,都要重新0x06。

3)页编程 page program

命令:0x02 (写数据)

向已经擦除过(值为0xFF)的Flash地址,写入1~256字节(1页)数据;

时序:CS拉低→发命令0x02→24bit地址(A23--A0)→发送1~256字节数据→CS拉低,芯片开始内部烧写。

24bit地址理解:

24bit地址=高8位地址字节+中8位地址字节+低8位地址字节,举例:

要写地址0x123456,拆分24位:

addr =0x123456;

addr_h =(addr>>16)&0xFF;//0x12最高字节A23~A16。

addr_m =(addr>>8)&0xFF;//0x34最高字节A15~A8。

addr_l =addr&0xFF;//0x56最高字节A7~A0。

SPI发送顺序:0x02→addr_h→addr_m→addr_l→后续数据字节;

但写代码时,uint32_t addr =0x00ABCDEF,32位无符号整数。

32为内存布局:

bit31~bit24bit23~bit16bit15~bit8bit7~bit0
0x000xAB0xCD0xEF

W25Q64需要24bit地址:bit23~bit0,对应:

24bit高字节:bit23~bit16→值0xAB;

24bit中字节:bit15~bit8→值0xCD;

24bit低字节:bit7~bit0→值0xEF;

那为啥32位变量,右移16位能得到24bit的高字节呢??

我们操作的是32位变量里面包含的那24个有效地址位

addr>>16:把整个32位数整体向右移16位:0x000000AB,再&0xFF只保留最低8位,取出0xAB,即:

addr_h =(addr>>16)&0xFF→0xAB;

addr_m =(addr>>8)&0xFF→0xCD;

addr_l =addr&0xFF→0xEF;

向右移16位,结果仍然是32位uint32_t类型,强制转换为uint8_t或者&0xFF才能截取8bit字节,才能送给SPI发送函数(函数参数是uint8_t)。

void W25Q64_PageProgram(uint32_t Address,uint8_t* DataArray,uint16_t Counter) { uint16_t i; if(DataArray == NULL ||Counter ==0){return ;} //空指针检查 W25Q64_WriteEnable(); MySPI_Start(); MySPI_SwapByte(W25Q64_PAGE_PROGRAM); MySPI_SwapByte((Address>>16)&0xFF); MySPI_SwapByte((Address>>8)&0xFF); MySPI_SwapByte((Address)&0xFF); for(i =0;i<Counter;i++) { MySPI_SwapByte(DataArray[i]); } MySPI_Stop(); W25Q64_WaitBusy(); }

注:

1)页编程最大单次256字节,如果大于256,连续写入多字节时,最多写入一页的数据,超过页尾位置的数据,会回到页首覆盖写入。地址0x0000FF,Counter =10直接调用本函数,就发送覆盖问题。

2)指针判断是否为空

如果传入DataArray =NULL空指针,for循环访问DataArray[i]直接死机。

3)只有CS拉高之后(就是MySPI_Stop()执行后),芯片才真正启动那个页编程。

4)读写记忆口诀:

写外设:把要发的数据填进MySPI_SwapByte()函数里;

读外设:MySPI_SwapByte()括号内填哑字节,返回值存进变量(比如数组里);

4)读数据Read Data

命令:0x03普通读指令

特点:可以连续读,时钟不停地址自动+1,软件模拟优先用0x03;

时序:CS拉低→DI引脚(MOSI)主机发送1字节命令0x03→继续发24bit地址(A23~A0,高字节→中间字节→低字节,和页编程地址格式一样)。

24位地址发完之后,DI(MOSI)可以随便发哑字节0xFF,只用来产生SPI时钟脉冲;

DO(MISO)引脚,W25Q向外输出存储的数据,高位MSB先输出。

每读完1字节,芯片内部地址硬件自动+1;

只要CLK持续给时钟,就可以源源不断连续读下去,不用重发命令和地址。

读完想要的字节,把/CS拉高(不要中途拉高,不然重发命令0x03),结束读指令;

如果Flash正在忙(BUSY=1,在擦除或写),此时指令被忽略,读出的数据无效。等到BUSY=0时,再调用读函数

void W25Q64_ReadData(uint32_t Address,uint8_t* DataArray,uint16_t Counter) { uint16_t i; MySPI_Start(); MySPI_SwapByte(W25Q64_READ_DATA); MySPI_SwapByte((Address>>16)&0xFF); MySPI_SwapByte((Address>>8)&0xFF); MySPI_SwapByte((Address)&0xFF); for(i=0;i<Counter;i++) { DataArray[i] =MySPI_SwapByte(W25Q64_DUMMY_BYTE); } MySPI_Stop(); }

二、读取W25Q64数据

接文章STM32学习26-SPI通信1,软件模拟代码:

W25Q64.c文件

#include "stm32f10x.h" #include "MySPI.h" #include "W25Q64_Ins.h" #include "stddef.h" void W25Q64_Init() { MySPI_Init(); } void W25Q64_ReadID(uint8_t*MID,uint16_t*DID) { MySPI_Start(); MySPI_SwapByte(W25Q64_JEDEC_ID); *MID =MySPI_SwapByte(W25Q64_DUMMY_BYTE); *DID =MySPI_SwapByte(W25Q64_DUMMY_BYTE); *DID <<=8; *DID|=MySPI_SwapByte(W25Q64_DUMMY_BYTE); MySPI_Stop(); } void W25Q64_WriteEnable(void) { MySPI_Start(); MySPI_SwapByte(W25Q64_WRITE_ENABLE); MySPI_Stop(); } void W25Q64_WaitBusy(void) { uint32_t Timeout =100000; MySPI_Start(); MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1); while((MySPI_SwapByte(W25Q64_DUMMY_BYTE) &(0x01)) ==0x01) { Timeout--; if(Timeout ==0) { break; } } MySPI_Stop(); } void W25Q64_PageProgram(uint32_t Address,uint8_t* DataArray,uint16_t Counter) { uint16_t i; if(DataArray == NULL ||Counter ==0){return ;} //空指针检查 W25Q64_WriteEnable(); MySPI_Start(); MySPI_SwapByte(W25Q64_PAGE_PROGRAM); MySPI_SwapByte((Address>>16)&0xFF); MySPI_SwapByte((Address>>8)&0xFF); MySPI_SwapByte((Address)&0xFF); for(i =0;i<Counter;i++) { MySPI_SwapByte(DataArray[i]); } MySPI_Stop(); W25Q64_WaitBusy(); } void W25Q64_SectorErase(uint32_t Address) { W25Q64_WriteEnable(); MySPI_Start(); MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB); MySPI_SwapByte((Address>>16)&0xFF); MySPI_SwapByte((Address>>8)&0xFF); MySPI_SwapByte((Address)&0xFF); MySPI_Stop(); W25Q64_WaitBusy(); } void W25Q64_ReadData(uint32_t Address,uint8_t* DataArray,uint16_t Counter) { uint16_t i; MySPI_Start(); MySPI_SwapByte(W25Q64_READ_DATA); MySPI_SwapByte((Address>>16)&0xFF); MySPI_SwapByte((Address>>8)&0xFF); MySPI_SwapByte((Address)&0xFF); for(i=0;i<Counter;i++) { DataArray[i] =MySPI_SwapByte(W25Q64_DUMMY_BYTE); } MySPI_Stop(); }

2、W25Q64.h文件

#ifndef __W25Q64_H #define __W25Q64_H void W25Q64_Init(); void W25Q64_ReadID(uint8_t*MID,uint16_t*DID); void W25Q64_PageProgram(uint32_t Address,uint8_t* DataArray,uint16_t Counter); void W25Q64_SectorErase(uint32_t Address); void W25Q64_ReadData(uint32_t Address,uint8_t* DataArray,uint16_t Counter); #endif

3、main函数

#include "stm32f10x.h" #include "Delay.h" #include "OLED.h" #include "W25Q64.h" uint8_t MID; uint16_t DID; uint8_t ArrayWrite[4] ={0x01,0x02,0x0E,0x04}; uint8_t ArrayRead[4]; int main() { OLED_Init(); W25Q64_Init(); OLED_ShowString(1,1,"ID:"); W25Q64_ReadID(&MID,&DID); OLED_ShowHexNum(1,4,MID,2); OLED_ShowHexNum(1,7,DID,4); W25Q64_SectorErase(0x000000);//先擦除,再写 W25Q64_PageProgram(0x000000,ArrayWrite,4); OLED_ShowHexNum(2,1,ArrayWrite[0],2); OLED_ShowHexNum(2,4,ArrayWrite[1],2); OLED_ShowHexNum(2,7,ArrayWrite[2],2); OLED_ShowHexNum(2,10,ArrayWrite[3],2); W25Q64_ReadData(0x000000,ArrayRead,4); OLED_ShowHexNum(3,1,ArrayRead[0],2); OLED_ShowHexNum(3,4,ArrayRead[1],2); OLED_ShowHexNum(3,7,ArrayRead[2],2); OLED_ShowHexNum(3,10,ArrayRead[3],2); while(1) { } }

注:

1)NULL需要包含头文件#include "stddef.h";

2)W25Q64_PageProgram(0x000000,ArrayWrite,4);地址改成0x0000FD,测试超过页尾的数据回到页首覆盖问题。

本文完!

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询