智能硬件設備的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; // } }; }