从零实现 SPI_flash(W25Q256)

前言:

SPI是全双工,即同一时刻可以双向通讯。

SPI是英语serial peripheral interface 的缩写,顾名思义就是穿行外围设备接口。是motorola首先在其MC68HCXX系列处理器上定义的。SPI接口主要应用在EEPROM,FLASH,实时时钟,AD转换器,还有数字信号处理器和数字信号解码器之间。SPI是一种高速的,全双工,同步的通信总线。并且在芯片的管脚上只占用4根线。

SPI的主要特点:可以同时发送和接收串行数据;可以当作主机或者从机工作;提供频率可编程时钟;发送结束中断标志;写冲突保护;总线竞争保护等。

W25Q256(256Mb)将32M(字节byte)的容量分为512个块(block),每个块大小为64K字节,每个块又分为16个扇区(sector),每个扇区4K字节。最小擦除单位为一个扇区,也就是4K字节。

W25Q256的擦写周期多达10W次,具有20年的数据保存期限。这就是我用的norflash,我当前用2M字节,因为用norflash的目的是IAP远程升级代码用,而flash只有1MB,2MB的norflash也绰绰有余了。

后面做到STM32H750时还会发现,SPI_FLASH是可以用作分散加载的。还有QSPI通讯方式


在原子哥的讲解中关于CPHA=0和CPHA=1时的时序图弄反了,查了STM32F29的中文手册SPI部分,是这样的。对比一下即可知。

从零实现 SPI_flash(W25Q256)_第1张图片

从零实现 SPI_flash(W25Q256)_第2张图片


配置cubemx

cubemx:模式选择全速双向主机(双工主机嘛)。NSS是一个关于CS的管脚,若为从机则NSS就用作CS,若为主机则可以当作控制线控制从机的CS,他会在通信前拉低通信后拉高,但是我们这里采用软件控制,所以并不需要他的NSS功能。

原子的代码解释的非常清晰,参照下图可以配置SPI5

从零实现 SPI_flash(W25Q256)_第3张图片

 

CS用PF6,设置为推挽输出,默认上拉

从零实现 SPI_flash(W25Q256)_第4张图片


硬件配置完毕,生成代码后,还需要对W25Q256特殊初始化一下。需要注意和原子不同,我将SPI5的初始化蔽掉了,因为cubemx上已经设置好了,代码生成已包含。此外这个时候对SPI5的速度进行设置我不太理解,前面SPI配置为256分频,后面又改为2。那直接配置为2不就好了么。   其实若刚上来就用高时钟来配置SPI_FLASH是容易出错的,不如先用低时钟先配置好,使用前在配置为高时钟。

//4Kbytes为一个Sector
//16个扇区为1个Block
//W25Q256
//容量为32M字节,共有512个Block,8192个Sector 												 
//初始化SPI FLASH的IO口
void W25QXX_Init(void)
{ 
    uint8_t temp;
	
	W25QXX_CS(1);			                //SPI FLASH不选中
//	SPI5_Init();		   			        //初始化SPI
//	SPI5_SetSpeed(SPI_BAUDRATEPRESCALER_2); //设置为45M时钟,高速模式
	W25QXX_TYPE=W25QXX_ReadID();	        //读取FLASH ID.
    if(W25QXX_TYPE==W25Q256)                //SPI FLASH为W25Q256
    {
        temp=W25QXX_ReadSR(3);              //读取状态寄存器3,判断地址模式
        if((temp&0X01)==0)			        //如果不是4字节地址模式,则进入4字节地址模式
		{
			W25QXX_CS(0); 			        //选中
			SPI5_ReadWriteByte(W25X_Enable4ByteAddr);//发送进入4字节地址模式指令   
			W25QXX_CS(1);       		        //取消片选   
		}
    }
}  

接下来编写测试代码,先在while(1)前读一下W25Q256的ID号,若能读出代表通信没问题。若读回不正确则闪烁LED1。

接下来再通过KEY0,KEY1分别写入一段字符串,读出同样地址的字符串。在LCD上显示看是否一致,以此检查是否正常。

//main.c 
main()
{
    MX_SPI5_Init();
    W25QXX_Init();
    //检查W25QXX是否读回ID号是否正确
    if(W25QXX_ReadID()!=W25Q256)
        LED_Twinkle(1);
    while(1)
    {
	if(_u8KeyStatus[KEY0])
	{
		W25QXX_Write((uint8_t*)TEXT_Buffer,FLASH_SIZE-100,sizeof(TEXT_Buffer));		//从倒数第100个地址处开始,写入SIZE长度的数据
		memcpy(InLtdcBuff1, TEXT_Buffer, sizeof(TEXT_Buffer));						//提示传送完成
	}
	if(_u8KeyStatus[KEY1])
	{
		W25QXX_Read(datatemp,FLASH_SIZE-100,sizeof(TEXT_Buffer));					//从倒数第100个地址处开始,读出SIZE个字节
		memcpy(InLtdcBuff2, datatemp, sizeof(TEXT_Buffer));
	}
    }
}

实验效果:

第一行“hello world”是我写入的,第二行“hello world”是我读出


最后:

想聊一聊原子哥写的

uint8_t W25QXX_BUFFER[4096];         
void W25QXX_Write(uint8_t* pBuffer,uint32_t WriteAddr,uint16_t NumByteToWrite)   

在这个函数中实现的功能是从pBuffer地址向地址WriteAddr写入NumByteToWrite长度的字节。

因为最小擦除单位是一个扇区,4K字节嘛,所以在写入的时候需要检查下要写入的部分是否有不为0XFF,否则就得先擦再写。所有可能要写入的字节数和目标地址跨扇区,那么假设要从扇区1的一半写到扇区3的一半,怎么办呢。

1.先把扇区1的前一半读出来,擦除扇区1,然后将要写入的剩下一半扇区的长度的数据连同扇区之前保存的前一半一并写入。扇区1的写入完成。

2.判断扇区2也有数据,因为我要写满一个扇区,所以我先把扇区2清空,再接着写入一个扇区长度的数据。扇区2的写入完成。

3.扇区3我要写前一半,那么就也判断下,若需要擦除,那我得把扇区3的后半部分读出来,擦除扇区3,再一并写入到扇区3。扇区3完成。

整个过程有一种似曾相识感觉,这跟我之前IAP的过程极为相似,单片机的flash最小擦除单位也为一个扇区。nandflash也很相似,他也是最小擦除单位为扇区,你说巧不巧。

所以技术是有相通的部分,今后在学习时,不仅要掌握用法,也要从思想上去理解,这样做的好处在哪。这种思维还能用在哪些地方会有更好的效果。

你可能感兴趣的:(基础嵌入式编程,存储类)