STM32F103之SPI软件读写W25Q64
一、W25Q64简介
1.1 简介
W25Q64(Nor flash)、 24位地址,64Mbit/8MByte、是一种低成本、小型化、使用简单的非易失性存储器,常用于数据存储、字库存储、固件程序存储等场景
时钟频率:最大80MHz(STM32F103系统时钟为72MHz)、160MHz(MOSI,MISO同时发送,同时接受)、320MHz(将引脚DO、DI、WP、HOLD同时接收或发送)


VCC: 2.7-3.6V
WP: 写保护,低电平有效,当为低电平时,不可写
HOLD:数据保持、低电平有效;如果在正常读写flash时,突然产生中断,想用SPI去操控其他器件,要是把CS置1,则flash时序就会终止;若将HOLD置0,则时序可以保持原有状态
1.2 框图-地址划分
24位地址,可寻址范围:0x000000-0xFFFFFF;由于W25Q64的容量为8MB,所以其地址范围为0x000000-0x7FFFFF


将8MB的空间划分为块(block),每一个块的容量为64KB,共划分128块
将每一个64KB块划分为扇区,一个块可划分为16个扇区,一共有2048个扇区
将每一4KB的扇区划分为页,一个扇区可划分16个页;也可以看作一个flash可划分为32768个页
一页是256字节 ,一块是256页
如果想要往第3个块第210页的第118个字节写入,物理地址是多少呢?
第三个块对应高八位字节:0x020000
第210个页对应中间八位(页地址锁存器):0x00D100
第118个字节对应第八位((字节地址锁存器):0x000075
所以物理地址为:0x02D175(24位地址)
例如写入地址为0x123456,对应的是多少呢
0x12->18,也就是块17(或第18个块)
0x34->52,也就是块17页51(或第52个页)
0x56->86,也就是块17页51的85字节(或第86个字节)也就是说18*16+52-1=4659,对应4659页的85字节开始写入
当往flash中写入数据时,写入的数据先缓存到256字节页缓冲区,再从缓冲区转到flash里,这需要一定的时间,在这期间,芯片的flash的busy标志位将被置1;
注意:由于缓冲区只有256个字节,所以写入的一个时序连续写入的数据量不能超过256个字节
1.3 读写flash注意事项
写入时
<1>写入操作前必须进行写使能
<2>每个数据位只能由1改写为0,不能由0改写为1
例如 原来数据为0x78(01111000),想要在本地址直接写入0x11(00010001),就会变为 00010000
<3>写入前必须想先要擦除,擦除后,所有数据位变为1(0xFF表示空白)
<5>擦除必须按最小单元进行(最小单元为扇区(4KB=4096字节))
想要单独改写某一个字节,可以先将扇区的全部数据读出后,该写完读出来的数据后再 写入或者一个字节占一个扇区
<6>连续写入多字节时,最多写入一页的数据;超过尾页的数据会回到页尾覆盖数据(不能跨页)
<7>写入操作结束后,芯片进入忙状态,不响应新的读写操作(擦除时也会进入忙状态)(可读状态寄存器的busy位)读取
<1>直接调用读取时序、无需使能、无需额外操作,没有页的限制,读取操作结束后不会进入忙状态,但不能在忙状态时读取
注意:当给定一个地址时,数据会按地址自增写入,且不可跨页写入,如果大于页的容量会从本页头开始覆盖;写使能只对之后跟随的一条指令有效
二、软件模拟SPI读取flash
SPI是一种协议,软件可以进行模拟,如果板子上没有SPI端口或者SPI的端口被占用时,软件可以进行模拟,通过软件控制四个引脚的高低电平,从而达到硬件的效果,本节进行软件模拟SPI(模式0),与硬件不同的是,对于软件的四个引脚,输入引脚(CS,CLK,MOSI)配置为上拉或浮空输入,输出引脚(MISO)配置为推挽输出
对于SPI不了解的可以看一下https://mpbeta.csdn.net/mp_blog/creation/editor/148934665
引脚配置
/****************************
*@brief SPI引脚初始化
*@param void
*@return void
*@note PB13->CLK PB14->MISO->DOPB12->CS PB15->MOSI->DI/
对于软件来说输出引脚配置为推挽输出
输入引脚配置为上拉输入或浮空输入
*@time 2025-6-27
******************************/
void Soft_SPI_Init(void)
{/*1.打开IO引脚时钟*/RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB,ENABLE);/*2.引脚模式配置*/GPIO_InitTypeDef GPIO_InitStruct;GPIO_InitStruct.GPIO_Mode=GPIO_Mode_Out_PP ;GPIO_InitStruct.GPIO_Pin=GPIO_Pin_13|GPIO_Pin_15|GPIO_Pin_12 ;//输出配置为推挽输出GPIO_InitStruct.GPIO_Speed=GPIO_Speed_50MHz ;GPIO_Init(GPIOB,&GPIO_InitStruct) ;GPIO_InitStruct.GPIO_Mode=GPIO_Mode_IPU ;//输入配置为浮空输入GPIO_InitStruct.GPIO_Pin=GPIO_Pin_14 ;GPIO_Init(GPIOB,&GPIO_InitStruct) ;/*3.CS片选默认高电平、CLK默认低电平*/SOFT_SPI_CS(1) ;//宏定义SOFT_SPI_CLK(0) ;//宏定义
}

对于模式0,时钟默认低电平且在时钟的第一个边沿进行采样 ,有时序图可知,MCU在时钟下降沿通过MOSI输出高位,在时钟上升沿时,采样 从机通过MOSI(对应主机MISO的)输出的电平,在写数据收发时序时只需要盯着主机,从机我们无法干涉
数据交换
/****************************
*@brief 交换数据
*@param void
*@return void
*@note 高位先发,SPI模式0
*@time 2025-6-27
******************************/
int8_t Soft_SPI_Dataswap(int8_t Send_Data)
{int8_t Receive_Data=0x00;for(int8_t i=0;i<8;i++){SOFT_SPI_MOSI(Send_Data>>(7-i)&0x01) ;//高位数据通过MOSI送出SOFT_SPI_CLK(1) ;//时钟上升沿//if(Soft_SPI_MISO()==1){Receive_Data |=(0x80>>i);}Receive_Data|=SOFT_SPI_MISO()<<(7-i) ;//主机从从机的MOSI口接收的高位数据SOFT_SPI_CLK(0) ;//时钟下降沿}return Receive_Data;
}
当时钟为低电平时,主机通过 SOFT_SPI_MOSI(Send_Data>>(7-i)&0x01) 将1位数据通过MOSI口送出;当时钟位上升沿时,主机进行采样,通过 Receive_Data|=SOFT_SPI_MISO()<<(7-i) 将从机的MOSI上的数据采入
注意:由于C语言时逐语句进行的,所以不可能发送的同时进行接收,可以利用RTOS或者FPGA
flash函数封装
/****************************
*@brief 读取ID
*@param uint8_t *MID 厂商IDuint16_t *DID 设备ID
*@return void
*@note void
*@time 2025-6-28
******************************/
void Soft_SPI_ReadID_W25Q64(uint8_t *MID,uint16_t *DID)
{SOFT_SPI_START;Soft_SPI_Dataswap(SOFT_SPI_R_ID) ;*MID = Soft_SPI_Dataswap(0XFF) ;*DID = Soft_SPI_Dataswap(0XFF) ;*DID <<= 8;*DID |= Soft_SPI_Dataswap(0XFF);SOFT_SPI_STOP;
}
/****************************
*@brief 写使能
*@param void
*@return void
*@note void
*@time 2025-6-28
******************************/
void Soft_SPI_W_ENABLE_W25Q64(void)
{SOFT_SPI_START;Soft_SPI_Dataswap(SOFT_SPI_W_ENABLE) ;//指令SOFT_SPI_STOP;
}
/****************************
*@brief 等待寄存器忙状态
*@param void
*@return void
*@note busy位维1,表示忙
*@time 2025-6-28
******************************/
void Soft_SPI_R_Status_W25Q64_busy(void)
{SOFT_SPI_START;Soft_SPI_Dataswap(SOFT_SPI_GetStatus);//读取指令while(Soft_SPI_Dataswap(0xFF)&0x01) ;//busy=1时等待SOFT_SPI_STOP;
}
/****************************
*@brief 页编程
*@param uint8_t ArrayData[]数据uint32_t Address写入的数据地址uint16_t len 字节个数
*@return void
*@note void
*@time 2025-6-28
******************************/
void Soft_SPI_W_Data_W25Q64(uint8_t *ArrayData,uint32_t Address,uint16_t cnt)
{Soft_SPI_W_ENABLE_W25Q64();//写使能SOFT_SPI_START;Soft_SPI_Dataswap(SOFT_SPI_W_DATA);//写指令/*函数Soft_SPI_Dataswap()一次只能交换8位*/Soft_SPI_Dataswap(Address>>16); //高地址,决定哪块Soft_SPI_Dataswap(Address>>8); //中间地址,决定哪个扇区Soft_SPI_Dataswap(Address>>0); //低地址,决定哪个页/*写数据*/for(uint16_t i=0;i<cnt;i++){Soft_SPI_Dataswap(ArrayData[i]);}SOFT_SPI_STOP;Soft_SPI_R_Status_W25Q64_busy();//busy等待
}
/****************************
*@brief 扇区擦除
*@param uint32_t Address擦除地址
*@return void
*@note void
*@time 2025-6-28
******************************/
void Soft_SPI_Sector_Clean_W25Q64(uint32_t Address)
{Soft_SPI_W_ENABLE_W25Q64();//写使能SOFT_SPI_START;Soft_SPI_Dataswap(SOFT_SPI_SECTOR_CLEAN );//指令/*函数Soft_SPI_Dataswap()一次只能交换8位*/Soft_SPI_Dataswap(Address>>16); //高地址,决定哪块Soft_SPI_Dataswap(Address>>8); //中间地址,决定哪个扇区Soft_SPI_Dataswap(Address>>0); //低地址,决定哪个页SOFT_SPI_STOP;Soft_SPI_R_Status_W25Q64_busy();//busy等待
}
/****************************
*@brief 读数据
*@param uint8_t* ArrayData 输出数据uint32_t Address 读出的数据起始地址uint16_t len 字节个数
*@return void
*@note void
*@time 2025-6-28
******************************/
void Soft_SPI_R_Data_W25Q64(uint8_t *ArrayData,uint32_t Address,uint32_t cnt)
{SOFT_SPI_START;Soft_SPI_Dataswap(SOFT_SPI_R_DATA);//写指令/*函数Soft_SPI_Dataswap()一次只能交换8位*/Soft_SPI_Dataswap(Address>>16); //高地址,决定哪块Soft_SPI_Dataswap(Address>>8); //中间地址,决定哪个扇区Soft_SPI_Dataswap(Address>>0); //低地址,决定哪个页/*读数据*/for(uint32_t i=0;i<cnt;i++){ArrayData[i]= Soft_SPI_Dataswap(0xFF);}SOFT_SPI_STOP;
}
上述代码对读取ID,写使能,读数据,写数据,擦除进行了封装,相同的流程是
①片选语句拉低(SOFT_SPI_START)
②flash操作指令(Soft_SPI_Dataswap(指令))
③读/写数据
④片选语句拉低(SOFT_SPI_STOP)
注意:
可以跨页读但不能跨页写;
写之前须写使能;
一次最多写入256字节数据;
再次写入相同地址需要先擦除,再写入;
擦除、写入操作会使busy置1,busy置1不再响应读,写,擦除操作
三、整个代码
运行代码: