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\r\n",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;            // 
        }     
    };     
}

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM