SPI Flash的操作

智能硬件设备的MCU下面,常常会挂一个SPI Flash,用于存放字库等文件。容量不会太大,16MB左右。今天记录一下通过SPI接口对其进行操作。

    这个图是SPI的接口结构图。主机写数据寄存器,通过 MOSI 信号线 传送给从机,从机也将自己的移位寄存器中的内容通过 MISO 信号线返回给主机。这样,两个移位寄存器中的内容就被交换。 如果只进行写操作,主机只需忽略接收到的字节;反之,若主机要读取从机的一个字节,就必须发送一个空字节来引发从机的传输。最后这句要理解,如果要读从机,除了发读命令,还要写空数据到从机,把从机中的数据挤出来。

SPI的配置中,有两个比特要注意。CPOL用来配置空闲的时候,CLK电平的高低。

CPHA用来控制采样时刻。CPHA=1的时候,采样发生在CS变低后的第二个沿,无论是下降沿还是上升沿。CPHA=0的时候,采样发生在CS变低后的第一个沿。这个需要查看从机的时序来确定怎么配置。ST的MCU,NSS管脚可以选择用硬件控制,也可以用软件控制,软件控制就是写GPIO,输出高低。ST的SPI口的其余配置就很简单了。

接下来介绍一下这颗SPI Flash。W25Q128 将 16MB 的容量分为 256 个块( Block),每个块大小为 64K 字节,每个块又分为16 个扇区( Sector),每个扇区 4K 个字节。 W25Q128 的最小擦除单位为一个扇区,也就是每次必须擦除 4K 个字节。这样我们需要给 W25Q128 开辟一个至少 4K 的缓存区。每个扇区又分为16个页(page),每个page
256B, 可以对整个page进行写操作。

//数据读写函数,这个函数主要用来发送控制命令
u8 SPI1_ReadWriteByte(u8 TxData) {
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET){}//等待发送缓冲区为空,SR寄存器的TXE位 SPI_I2S_SendData(SPI1, TxData); //往DR寄存器写入要发送的值,即是发送数据 while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET){} //等待接收缓冲区为空 return SPI_I2S_ReceiveData(SPI1); //缓冲区空了,数据已经到DR寄存器了,就可以读了。 }

//读状态寄存器
u8 W25QXX_ReadSR(void) { u8 byte=0; W25QXX_CS=0; SPI1_ReadWriteByte(W25X_ReadStatusReg); // W25X_ReadStatusReg是读状态寄存器指令,0x05; byte=SPI1_ReadWriteByte(0Xff); // 写个无效数据,把要读取的数据移出来 W25QXX_CS=1; // return byte; }

 

   这个是读ID的指令,代码如下:

u16 W25QXX_ReadID(void)
{
    u16 Temp = 0;      
    W25QXX_CS=0;                    
    SPI1_ReadWriteByte(0x90);// 发指令
    SPI1_ReadWriteByte(0x00);  //dummy       
    SPI1_ReadWriteByte(0x00);  //dummy       
    SPI1_ReadWriteByte(0x00);                     
    Temp|=SPI1_ReadWriteByte(0xFF)<<8;  //读MF7-MF0
    Temp|=SPI1_ReadWriteByte(0xFF);     //读ID7-ID0
    W25QXX_CS=1;                    
    return Temp;
}               

以上是读数据的时序,下面是代码

void W25QXX_Read(u8* pBuffer,u32 ReadAddr,u16 NumByteToRead)   //要放入的数组;读地址;要读的数据个数
{ 
     u16 i;                                               
    W25QXX_CS=0;                            // 
    SPI1_ReadWriteByte(W25X_ReadData);         //    03h
    SPI1_ReadWriteByte((u8)((ReadAddr)>>16));  //    地址23~16
    SPI1_ReadWriteByte((u8)((ReadAddr)>>8));   //    地址15~8
    SPI1_ReadWriteByte((u8)ReadAddr);         //     地址7~0
    for(i=0;i<NumByteToRead;i++)
    { 
        pBuffer[i]=SPI1_ReadWriteByte(0XFF);   //  发送dummy,移出读取数据
    }
    W25QXX_CS=1;                                
}  

//这个函数是用来page写,page写需要满足下面的条件。page都已经被擦除了,而且写使能已经执行了
//The Page Program instruction allows from one byte to 256 bytes (a page) of data to be programmed at
//previously erased (FFh) memory locations. A Write Enable instruction must be executed before the device
//will accept the Page Program Instruction (Status Register bit WEL= 1).


//这个函数使用的前提是,这个page被擦干净了,所以这个函数是不会被单独调用的,会在另外一个函数中被引用

void
W25QXX_Write_Page(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite) //NumByteToWrite不能超过一个page的大小 { u16 i; W25QXX_Write_Enable(); //写使能 W25QXX_CS=0; // SPI1_ReadWriteByte(W25X_PageProgram); // page编程指令 SPI1_ReadWriteByte((u8)((WriteAddr)>>16)); // 地址23~16 SPI1_ReadWriteByte((u8)((WriteAddr)>>8)); //地址15~8 SPI1_ReadWriteByte((u8)WriteAddr); //地址7~0 for(i=0;i<NumByteToWrite;i++)
SPI1_ReadWriteByte(pBuffer[i]); // 循环操作 W25QXX_CS=1; // W25QXX_Wait_Busy(); // }

下面这个函数,写入的数据要大于一个page。然后控制写入地址的偏移,把数据分割成小块,然后再调用上面的Page写函数。

pageremain表示这个page中要写入的数据个数
void W25QXX_Write_NoCheck(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite)   
{                       
    u16 pageremain;       
    pageremain=256-WriteAddr%256; //要写入的地址所在的page,还剩余多少空间                
    if(NumByteToWrite<=pageremain)pageremain=NumByteToWrite;// 如果要写入的数据,连第一个page也填不满
    while(1)
    {       
        W25QXX_Write_Page(pBuffer,WriteAddr,pageremain);
        if(NumByteToWrite==pageremain)break;//一个page都没满,这就写完了
         else //还需要写到下一个page
        {
            pBuffer+=pageremain; //地址偏移
            WriteAddr+=pageremain;    

            NumByteToWrite-=pageremain;              //已经写掉的去除
            if(NumByteToWrite>256)pageremain=256; // 
            else pageremain=NumByteToWrite;       // 
        }
    };        
} 

 以下是真正的写,会涉及到擦除,会调用上面的函数

u8 W25QXX_BUFFER[4096];     //先开辟一个4K的空间    
void W25QXX_Write(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite)   
{ 
    u32 secpos;
    u16 secoff;
    u16 secremain;       
    u16 i;    
    u8 * W25QXX_BUF;      
    W25QXX_BUF=W25QXX_BUFFER;         
    secpos=WriteAddr/4096;//获得sector号
    secoff=WriteAddr%4096;// sector中的偏移
    secremain=4096-secoff;// sector中剩余空间//printf("ad:%X,nb:%X
",WriteAddr,NumByteToWrite);//测试用
     if(NumByteToWrite<=secremain)secremain=NumByteToWrite;// 思路和上面的函数类似
    while(1) 
    {    
        W25QXX_Read(W25QXX_BUF,secpos*4096,4096);//因为后面可能需要擦除,所以要把sector读出来
        for(i=0;i<secremain;i++)// 
        {
            if(W25QXX_BUF[secoff+i]!=0XFF)break;  //碰到非FF的,就需要擦除了 
        }
        if(i<secremain)//跳出了for循环,说明碰到非FF了
        {
            W25QXX_Erase_Sector(secpos);// 擦除这个sector
            for(i=0;i<secremain;i++)       // 
            {
                W25QXX_BUF[i+secoff]=pBuffer[i];  //左边这个数据已经把整个sector读出来了,右边这个是需要写入的数据,右边把左边覆盖掉,这个是指针操作,所以可以这样    
            }
            W25QXX_Write_NoCheck(W25QXX_BUF,secpos*4096,4096); //虽然真正写入的是sector后面一部分,但是由于整个都擦除了,所以需要都写

        }else W25QXX_Write_NoCheck(pBuffer,WriteAddr,secremain); //  发现剩余部分没有非FF,那就直接全部写入
        if(NumByteToWrite==secremain)break;//  要写入的都在一个sector里面,就一次写完了,可以跳出
        else// 
        {
            secpos++;// 转到下一个sector
            secoff=0;// 到了一个新的sector,就是从偏移地址0开始写

               pBuffer+=secremain;  // 
            WriteAddr+=secremain;// 
               NumByteToWrite-=secremain;                // 
            if(NumByteToWrite>4096)secremain=4096;    //  这个和page操作类似
            else secremain=NumByteToWrite;            // 
        }     
    };     
}
原文地址:https://www.cnblogs.com/nasduc/p/4920162.html