本文介紹如何使用STM32標准外設庫驅動FLASH,本例程驅動的FLASH為W25Q64。
本文適合對單片機及C語言有一定基礎的開發人員閱讀,MCU使用STM32F103VE系列。
1. FLASH簡介
FLASH存儲器又稱為閃存,為可重復擦寫的存儲器,容量比EEPROM大的多。
FLASH在寫入數據時只能把1改成0,而0無法直接改成1,因此要寫入數據時,必須先執行擦除操作,而一次擦除操作無法僅擦除一個字節,必須將一整塊區域的數據全部改成1。因此FLASH操作的特性是擦除時必須一次擦除一整塊區域;寫入時可以按字節或按塊寫入;讀取則不受限制,可以讀取一個字節和任意多個字節。
FLASH可分為NOR FLASH和NAND FLASH,兩者特性有所區別,NOR FLASH讀取速度快、可以按字節讀寫,但容量相同的情況下價格較高,而NAND FLASH讀取速度較慢,只能按塊為單位讀寫,但容量相同的情況下價格較低。一般NOR FLASH適用於存儲程序代碼,NAND FLASH適用於存儲大數據量存儲。
2. 常用FLASH
一般常用的NOR FLASH為Winbond公司的W25Qxx系列,常用容量從16M到256Mbit不等,換算成字節為2M到32MBytes,可以根據項目需求和價格綜合考慮選型。
3.FLASH操作說明
以W25Q64舉例,W25Q64容量為64Mbit,即8MByte,地址范圍0~0x800000,3個字節即可表示,因此地址長度為3字節。
W25Q64共分為128個Block,每個Block為64Kbytes,每個Block又分為16個Sector,每個Sector為4Kbytes,每個Sector又可分為16個Pages,每個Page有256個字節,每次擦除時至少需要擦除一整個Sector,寫入時則可以單字節寫入,也可以寫入多個字節,但最多寫入一個Page,即256個字節。
3.1. 設備ID
W25Q64廠商號為0xEF,FLASH型號為0x4017,可以讀取這些信息判定FLASH是否正常。
3.2. 指令
該表中的第一列為指令名,第二列為指令編碼,第三至第 N 列的具體內容根據指令的不同而有不同的含義。其中帶括號的字節參數,方向為 FLASH 向主機傳輸,即命令響應,不帶括號的則為主機向 FLASH 傳輸。表中“A0~A23”指 FLASH 芯片內部存儲器組織的地址;“M0~M7”為廠商號(MANUFACTURER ID);“ID0-ID15”為 FLASH 芯片的ID;“dummy”指該處可為任意數據;“D0~D7”為 FLASH 內部存儲矩陣的內容。
3.3. 讀取
使用讀取命令(指令編碼為03h),發送了指令編碼及要讀的起始地址后,FLASH 芯片就會按地址遞增的方式返回內部存儲的數據,讀取的數據量沒有限制,只要沒有停止通訊,FLASH 芯片就會一直返回數據。
3.4. 寫使能、寫禁用和狀態讀取
在向 FLASH 寫入數據或者擦除前,首先要使能寫操作,通過發送“Write Enable”命令使FLASH可寫,寫入或者擦除完畢之后,FLASH自動進入寫禁用狀態,無需單獨發送寫禁用命令,當FLASH為寫禁用狀態時,任何寫入或擦除操作無效,這樣可避免誤寫入或誤擦除。
由於FLASH寫入數據需要消耗一定的時間,並不是在總線通訊結束的一瞬間完成的,所以在寫操作后需要確認 FLASH 芯片“空閑”時才能進行再次寫入。為了表示自己的工作狀態,FLASH 芯片定義了一個狀態寄存器,這個狀態寄存器的第 0 位為“BUSY”,當這個位為“1”時,表明 FLASH芯片處於忙碌狀態,它可能正在進行“擦除”或“數據寫入”的操作。利用指令表中的“Read Status Register”指令可以獲取 FLASH 狀態寄存器的內容,只要向 FLASH 芯片發送了讀狀態寄存器的指令,FLASH 芯片就會持續向主機返回最新的狀態寄存器內容,直到收到 SPI通訊的停止信號。因此可以通過查看該位,直到該位為0時,即可對FLASH進行擦除或者寫操作。如果剛寫完數據就執行讀操作,也需要等待。
3.5. 擦除
FLASH寫入數據之前需要先擦除,擦除可分為扇區擦除(Sector Erase)、塊擦除(Block Erase)和整片擦除(Chip Erase)。指令編碼分別為20h、D8h,而整片擦除支持2個命令,即2個命令均可使用,為C7h和60h。要實現擦除操作時先發送指令編碼,扇區擦除和塊擦除需要繼續發送要擦除區域的地址,而整片擦除無需發送地址。要執行擦除操作之前需要確保FLASH處於寫使能狀態,可通過發送寫使能命令實現。
3.6. 寫入
使用頁寫入命令(指令編碼為02h),先發送指令編碼,然后發送要寫的起始地址,然后繼續發送要寫入的內容,一次寫入操作最多寫入256字節數據。 進行寫入之前需要確保FLASH處於寫使能狀態,可通過發送寫使能命令實現。如果想要一次寫入超過256字節,那么就需要對頁寫入命令進行封裝。
完整代碼(僅自己編寫的部分)
1 #include "flash.h" 2 #include "delay.h" 3 #include <stdio.h> 4 5 #define FLASH_PAGE_SIZE 256 //W25Q64每頁256個字節 6 7 #define W25X_WriteEnable 0x06 8 #define W25X_WriteDisable 0x04 9 #define W25X_ReadStatusReg 0x05 10 #define W25X_WriteStatusReg 0x01 11 #define W25X_ReadData 0x03 12 #define W25X_FastReadData 0x0B 13 #define W25X_FastReadDual 0x3B 14 #define W25X_PageProgram 0x02 15 #define W25X_BlockErase 0xD8 16 #define W25X_SectorErase 0x20 17 #define W25X_ChipErase 0xC7 18 #define W25X_PowerDown 0xB9 19 #define W25X_ReleasePowerDown 0xAB 20 #define W25X_DeviceID 0xAB 21 #define W25X_ManufactDeviceID 0x90 22 #define W25X_JedecDeviceID 0x9F 23 24 /* WIP(busy)標志,FLASH內部正在寫入 */ 25 #define WIP_Flag 0x01 26 27 //初始化FLASH接口 28 void FLASH_Init(void) 29 { 30 SPI_IoInit(); 31 } 32 33 //查看W25Q64是否空閑 34 //返回值: 1,FLASH忙,無法讀寫 35 // 0,FLASH空閑,可以讀寫 36 //注意執行完此函數后,FLASH已取消選中,如果要寫入,必須重新選中 37 uint8_t FLASH_WaitReady(void) 38 { 39 uint32_t i = 0; 40 uint8_t ret = 1; 41 uint8_t status = 0; 42 43 SPI_CS_0; 44 45 SPI_WriteByte(W25X_ReadStatusReg); 46 47 for(i = 0; i < 1000; i++){ 48 status = SPI_ReadByte(); 49 if((status & WIP_Flag) == RESET){ 50 ret = 0; 51 break; 52 } 53 delay_ms(10); 54 } 55 56 SPI_CS_1; 57 58 return ret; 59 } 60 61 /* 62 FLASH擦除、寫入數據完畢后會自動禁用寫使能,因此無需再執行寫禁用操作 63 注意執行此函數前,必須先選中FLASH 64 */ 65 void FLASH_WriteEnable(void) 66 { 67 /* 發送寫使能命令*/ 68 SPI_WriteByte(W25X_WriteEnable); 69 } 70 71 uint32_t FLASH_ReadJedecID(void) 72 { 73 uint32_t temp, temp0, temp1, temp2; 74 75 SPI_CS_0; 76 77 /* 發送JEDEC指令,讀取ID */ 78 SPI_WriteByte(W25X_JedecDeviceID); 79 80 temp0 = SPI_ReadByte(); 81 temp1 = SPI_ReadByte(); 82 temp2 = SPI_ReadByte(); 83 84 /*把數據組合起來,作為函數的返回值*/ 85 temp = (temp0 << 16) | (temp1 << 8) | temp2; 86 87 SPI_CS_1; 88 89 return temp; 90 } 91 92 uint8_t FLASH_SectorErase(uint32_t addr) 93 { 94 /* 判斷FLASH是否可寫,如果不可寫,直接返回錯誤 */ 95 if(FLASH_WaitReady()){ 96 return 1; 97 } 98 99 SPI_CS_0; 100 101 /* 發送FLASH寫使能命令 */ 102 FLASH_WriteEnable(); 103 104 /* 發送扇區擦除指令*/ 105 SPI_WriteByte(W25X_SectorErase); 106 /*發送擦除扇區地址的高位*/ 107 SPI_WriteByte((addr & 0xFF0000) >> 16); 108 /* 發送擦除扇區地址的中位 */ 109 SPI_WriteByte((addr & 0xFF00) >> 8); 110 /* 發送擦除扇區地址的低位 */ 111 SPI_WriteByte(addr & 0xFF); 112 /* 發送FLASH寫禁用命令 */ 113 // FLASH_WriteDisable(); 114 115 SPI_CS_1; 116 117 if(FLASH_WaitReady()){ 118 return 2; 119 } 120 121 return 0; 122 } 123 124 uint8_t FLASH_ChipErase(void) 125 { 126 /* 判斷FLASH是否可寫,如果不可寫,直接返回錯誤 */ 127 if(FLASH_WaitReady()){ 128 return 1; 129 } 130 131 SPI_CS_0; 132 133 /* 發送FLASH寫使能命令 */ 134 FLASH_WriteEnable(); 135 136 /* 發送扇區擦除指令*/ 137 SPI_WriteByte(W25X_ChipErase); 138 139 SPI_CS_1; 140 141 if(FLASH_WaitReady()){ 142 return 2; 143 } 144 145 return 0; 146 } 147 148 //在W25Q64里面的指定地址開始讀出指定個數的數據 149 //addr: 開始讀數的地址 150 //pBuffer: 需要讀取數據的指針 151 //numToRead:要讀出數據的個數 152 //返回值: 1,讀取失敗 153 // 0,讀取成功 154 uint8_t FLASH_Read(uint32_t addr, uint8_t *pBuffer, uint32_t numToRead) 155 { 156 SPI_CS_0; 157 158 SPI_WriteByte(W25X_ReadData); 159 160 /* 發送 讀 地址高位 */ 161 SPI_WriteByte((addr & 0xFF0000) >> 16); 162 /* 發送 讀 地址中位 */ 163 SPI_WriteByte((addr & 0xFF00) >> 8); 164 /* 發送 讀 地址低位 */ 165 SPI_WriteByte(addr & 0xFF); 166 167 /* 讀取數據 */ 168 while (numToRead--) /* while there is data to be read */ 169 { 170 /* 讀取一個字節*/ 171 *pBuffer++ = SPI_ReadByte(); 172 } 173 174 SPI_CS_1; 175 176 return 0; 177 } 178 179 //在W25Q64指定地址讀出一個數據 180 //addr: 開始讀數的地址 181 //pReadData:需要讀取數據的指針 182 //返回值: 1,讀取失敗 183 // 0,讀取成功 184 uint8_t FLASH_ByteRead(uint32_t addr, uint8_t * pReadData) 185 { 186 SPI_CS_0; 187 188 SPI_WriteByte(W25X_ReadData); 189 190 /* 發送 讀 地址高位 */ 191 SPI_WriteByte((addr & 0xFF0000) >> 16); 192 /* 發送 讀 地址中位 */ 193 SPI_WriteByte((addr & 0xFF00) >> 8); 194 /* 發送 讀 地址低位 */ 195 SPI_WriteByte(addr & 0xFF); 196 197 /* 讀取一個字節*/ 198 *pReadData = SPI_ReadByte(); 199 200 SPI_CS_1; 201 202 return 0; 203 } 204 205 //在W25Q64指定地址寫入一個數據 206 //addr: 寫入數據的目的地址 207 //dataToWrite: 要寫入的數據 208 //返回值: 1,寫入失敗 209 // 0,寫入成功 210 uint8_t FLASH_ByteWrite(uint32_t addr, uint8_t dataToWrite) 211 { 212 /* 判斷FLASH是否可寫,如果不可寫,直接返回錯誤 */ 213 if(FLASH_WaitReady()){ 214 return 1; 215 } 216 217 SPI_CS_0; 218 219 /* 發送FLASH寫使能命令 */ 220 FLASH_WriteEnable(); 221 222 /* 寫頁寫指令*/ 223 SPI_WriteByte(W25X_PageProgram); 224 /*發送寫地址的高位*/ 225 SPI_WriteByte((addr & 0xFF0000) >> 16); 226 /*發送寫地址的中位*/ 227 SPI_WriteByte((addr & 0xFF00) >> 8); 228 /*發送寫地址的低位*/ 229 SPI_WriteByte(addr & 0xFF); 230 231 /* 發送當前要寫入的字節數據 */ 232 SPI_WriteByte(dataToWrite); 233 234 SPI_CS_1; 235 236 if(FLASH_WaitReady()){ 237 return 2; 238 } 239 240 return 0; 241 } 242 243 uint8_t FLASH_PageWrite(uint32_t addr, uint8_t *pBuffer, uint32_t numToWrite) 244 { 245 /* 判斷FLASH是否可寫,如果不可寫,直接返回錯誤 */ 246 if(FLASH_WaitReady()){ 247 return 1; 248 } 249 250 SPI_CS_0; 251 252 /* 發送FLASH寫使能命令 */ 253 FLASH_WriteEnable(); 254 255 /* 寫頁寫指令*/ 256 SPI_WriteByte(W25X_PageProgram); 257 /*發送寫地址的高位*/ 258 SPI_WriteByte((addr & 0xFF0000) >> 16); 259 /*發送寫地址的中位*/ 260 SPI_WriteByte((addr & 0xFF00) >> 8); 261 /*發送寫地址的低位*/ 262 SPI_WriteByte(addr & 0xFF); 263 264 /* 寫入數據*/ 265 while(numToWrite--) 266 { 267 /* 發送當前要寫入的字節數據 */ 268 SPI_WriteByte(*pBuffer++); 269 } 270 271 SPI_CS_1; 272 273 if(FLASH_WaitReady()){ 274 return 2; 275 } 276 277 return 0; 278 } 279 280 /* 281 根據要寫入的地址、長度、頁大小計算如何分頁 282 輸入參數:addr: 寫入起始地址 283 len: 寫入數據長度 284 pageSize:每頁存儲的數據,對於W25Q64來說,該值為256 285 要寫入參數:pFirstPageLen: 首頁要寫入的字節 286 pLastPageLen: 尾頁要寫入的字節 287 pPageNum: 總共要寫入的頁數 288 */ 289 void FLASH_GetWritePages(uint32_t addr, uint32_t len, uint32_t pageSize, 290 uint32_t * pFirstPageLen, uint32_t * pLastPageLen, uint32_t * pPageNum) 291 { 292 uint32_t firstPageOffset; //首頁偏移 293 uint32_t otherLen; //去除首頁之后剩余長度 294 uint32_t otherPageNum; //去除首頁之后剩余整數頁數量 295 296 firstPageOffset = addr % pageSize; 297 *pFirstPageLen = pageSize - firstPageOffset; 298 299 if(len < *pFirstPageLen){ 300 *pFirstPageLen = len; 301 } 302 303 otherLen = len - *pFirstPageLen; 304 otherPageNum = otherLen / pageSize; 305 *pLastPageLen = otherLen % pageSize; 306 307 *pPageNum = otherPageNum + 1; 308 309 if(*pLastPageLen){ 310 (*pPageNum)++; 311 } 312 } 313 314 315 //在W25Q64里面的指定地址開始寫入指定個數的數據 316 //addr: 開始讀數的地址 317 //pBuffer: 需要讀取數據的指針 318 //NumToWrite:要寫入數據的個數 319 //返回值: 1,讀取失敗 320 // 0,讀取成功 321 uint8_t FLASH_Write(uint32_t addr, uint8_t *pBuffer, uint32_t numToWrite) 322 { 323 uint32_t i; 324 uint32_t firstPageLen, lastPageLen, pageNum; 325 326 FLASH_GetWritePages(addr, numToWrite, FLASH_PAGE_SIZE, 327 &firstPageLen, &lastPageLen, &pageNum); 328 329 printf("addr:%#x, numToWrite:%d, firstPageLen:%d, lastPageLen:%d, pageNum:%d\n", 330 addr, numToWrite, firstPageLen, lastPageLen, pageNum); 331 332 for(i = 0; i < pageNum; i++) 333 { 334 if(i == 0){ //首頁寫入長度為firstPageLen 335 if(FLASH_PageWrite(addr, pBuffer, firstPageLen)){ 336 goto write_fail; 337 } 338 addr += firstPageLen; 339 pBuffer += firstPageLen; 340 }else if(i == pageNum - 1){ //尾頁寫入長度為lastPageLen 341 if(FLASH_PageWrite(addr, pBuffer, lastPageLen)){ 342 goto write_fail; 343 } 344 addr += lastPageLen; 345 pBuffer += lastPageLen; 346 }else{ //除首頁和尾頁外寫入長度為FLASH_PAGE_SIZE 347 if(FLASH_PageWrite(addr, pBuffer, FLASH_PAGE_SIZE)){ 348 goto write_fail; 349 } 350 addr += FLASH_PAGE_SIZE; 351 pBuffer += FLASH_PAGE_SIZE; 352 } 353 } 354 355 return 0; 356 357 write_fail: 358 return 1; 359 }