SPI(下)——讀寫串列FLASH實驗

前兩篇文章介紹了SPI協議的基礎概念和通訊過程的時序,這篇可以進入實踐,完成一個小小的讀寫實驗了。

從設備硬體特性

實驗用的從設備是串列FLASH存儲晶元W25Q64,這是一種使用SPI協議進行通訊的NOR FLASH存儲器。這是一個64M位(即8M位元組)的串列快閃記憶體,有0~127個塊,每個塊64KB,每個塊包含16個扇區,其最小擦除單位是扇區(Sector)。如圖18-1所示。

圖18-1

查閱開發板的原理圖可以知道,W25Q64的四個引腳和stm32f103引腳的連接關係為:

圖18-2

需要注意的一點是,這裡stm32的NSS引腳會被配置為軟體模式,把它當成普通的GPIO引腳即可。

SPI_InitTypeDef

圖18-2

  • SPI_Direction:SPI通訊方向,可配置雙線全雙工、雙線只接收、單線只接收、單線只發送模式;

  • SPI_Mode:SPI的工作模式,即工作在主機模式或從機模式。若工作在從機模式,則SCK信號由外部提供;

  • SPI_DataSize:通訊的數據幀大小,可選8位或16位;

  • SPI_CPOL:時鐘極性,配置空閑狀態時的SCK電平;

  • SPI_CPHA:時鐘相位,配置數據採樣時刻,可配置在每個時鐘周期的第1個或第2個邊沿進行採樣;

  • SPI_NSS:配置NSS引腳的使用模式,可配置為硬體模式或軟體模式。軟體模式即是普通的GPIO口,人工拉高或置低其電平;

  • SPI_BaudRatePrescaler:波特率分頻因子,分頻後的時鐘即為SPI的SCK信號線的時鐘頻率;

  • SPI_FirstBit:串列通訊中總會牽扯到MSB(高位)先行還是LSB(低位)先行的問題,可以用這個結構體成員進行配置;

  • SPI_CRCPolynomial:CRC校驗,若使用CRC,則可計算CRC的值。

SPI配置

先配置用到的GPIO,關於如何配置GPIO,之前專欄里的文章已經介紹很詳細了,這裡不再贅述。只是GPIO的引腳模式(GPIO_Mode)需要配置正確,關於這個可查閱stm32f103參考手冊8.1.11章節的「外設的GPIO配置」,這裡有關於各個外設的引腳模式配置分配。如圖18-3。

圖18-3

配置SPI前,需要先了解從設備如何支持SPI,這些信息通過查詢W25Q64的數據手冊得到。

查閱W25Q64的數據手冊可知,W25Q64最高可以支持80MHz的時鐘頻率,其支持SPI模式0(CPOL=0,CPHA=0)及模式3(CPOL=1,CPHA=1),支持雙線全雙工,MSB先行,8位或16位數據幀長度。

結合查到這些設備信息配置結構體即可。

/**n * @brief SPI 工作模式配置n * @param 無n * @retval 無n */nstatic void SPI_Mode_Config(void)n{n SPI_InitTypeDef SPI_InitStructure; nn SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; // 雙線全雙工n SPI_InitStructure.SPI_Mode = SPI_Mode_Master; // 主機模式n SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; // 8位數據幀n SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; // 軟體模式n SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_2 ; // 波特率分頻因子,2分頻n SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; // 高位先行n SPI_InitStructure.SPI_CRCPolynomial = 0; // 不使用CRC校驗,數值隨便寫n //SPI使用模式3,CPOL=1,CPHA=1n SPI_InitStructure.SPI_CPOL = SPI_CPOL_High; // CPOL=1n SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; // CPHA=1nn SPI_Init(SPI1, &SPI_InitStructure);nn SPI_Cmd(SPI1, ENABLE);n}n

單位元組數據發送與接收

剛才將SPI配置為雙線全雙工,意味著發送和接收是同時進行的。主機發送數據到從機,驅動SCK時鐘運行,同時從機返回需要的數據到主機。

/**n * @brief SPI 工作模式配置n * @param data:發送並接收單位元組數據n * @retval 返回從機發回的位元組數據n */nuint8_t SPI_Send_Byte(uint8_t data)n{n // 檢查並等待TX緩衝區為空n while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);n n // 緩衝區為空後向緩衝區寫入要發送的位元組數據n SPI_I2S_SendData(SPI1, data);tnn // 檢查並等待RX緩衝區為非空n while(SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET);nn // 數據發送完畢,從RX緩衝區接收flash返回的數據 tn return SPI_I2S_ReceiveData(SPI1); n}n

而位元組接收的函數,正如上所述,需要向從機發送數據,才能驅動SCK運作,才能得到flash要返回給主機的數據,主機發送的這個數據可以是任意的,只是用於觸發SCK。

指令控制

那麼應該如何實現對flash設備的讀寫呢?這涉及到操作指令。

這些指令是flash晶元自定義的,主機通過SPI發送指令到從設備,從設備收到指令會執行對應的操作。如下圖是W25Q64的指令。

圖18-4

上圖中,第一列是指令名稱,第二列是指令編碼,第三列及以後的指令功能與對應的指令有關。帶括弧的位元組內容為flash向主機返回的位元組數據,不帶括弧則是主機向flash發送位元組數據。其中,

  • A0~A23:flash內部存儲器地址;
  • M0~M7:製造商ID;
  • ID0~ID15:flash晶元ID;
  • D0~D7:flash內部存儲的數據;
  • dummy:指任意數據。

例如常常需要讀取從設備ID用於測試識別設備。主機通過向flash發送「JEDEC ID」指令的編碼「9Fh」,flash會通過MISO線向主機返回三個位元組的數據:製造商ID(M7-M0)、晶元ID(ID15~ID8、ID7~ID0)。

在編程中,會用到剛才的SPI_Send_Byte()函數,這個函數是單位元組的數據收發,所以flash返回的三個位元組數據需要調用三次的SPI_Send_Byte()函數來進行接收。

#define FLASH_SPI_CS_HIGH GPIO_SetBits(GPIOA, GPIO_Pin_4); // flash晶元的CS引腳電平拉高n#define FLASH_SPI_CS_LOW GPIO_ResetBits(GPIOA, GPIO_Pin_4); // flash晶元的CS引腳電平置低nn#define Dummy_Byte 0xFF // 任意值nn/**n * @brief 讀取設備IDn * @param 無n * @retval 返回從機發回的ID值n */nuint32_t SPI_FLASH_ReadID(void)n{n uint32_t temp = 0, temp0 = 0, temp1 = 0, temp2 = 0;nn FLASH_SPI_CS_LOW; // 低電平片選有效,SPI通訊開始nn SPI_FLASH_SendByte(0x9f); // 發送讀取ID指令nn temp0 = SPI_FLASH_SendByte(Dummy_Byte); // 讀取製造商ID(M7-M0)n temp1 = SPI_FLASH_SendByte(Dummy_Byte); // 讀取晶元ID(ID15~ID8)n temp2 = SPI_FLASH_SendByte(Dummy_Byte); // 讀取晶元ID(ID7~ID0)nn FLASH_SPI_CS_HIGH; // 停止SPI通訊nn temp = (temp0 << 16) | (temp1 << 8) | temp2; // 組合數據nn return temp;n}n

將得到的ID值與預定義的設備ID進行對比即可測試flash晶元是否連接正常等。

有一點需要注意,向flash晶元寫入數據是需要時間的,所以進行下一次寫數據前必須確認flash晶元處於空閑狀態。而關於這個,flash晶元也定義了一個狀態寄存器,這個寄存器的第0位「BUSY」反應當前flash是否處於忙碌狀態。所以我們用「Read Status Register」指令來獲取狀態值。

#define FLASH_SPI_CS_HIGH GPIO_SetBits(GPIOA, GPIO_Pin_4); // flash晶元的CS引腳電平拉高n#define FLASH_SPI_CS_LOW GPIO_ResetBits(GPIOA, GPIO_Pin_4); // flash晶元的CS引腳電平置低nn#define Dummy_Byte 0xFF // 任意值nn/**n * @brief 等待flash內部時序操作完成n * @param 無n * @retval 無n */nvoid SPI_WaitForWriteEnd(void)n{n uint8_t status_reg = 0;ntn FLASH_SPI_CS_LOW;ntn SPI_FLASH_Send_Byte(0x05); // 發送「Read Status Register」指令ntn don {tn status_reg = SPI_FLASH_Send_Byte(Dummy_Byte);n }while((status_reg & 0x01) == 1);ntn FLASH_SPI_CS_HIGH;tnn}n

扇區擦除

flash有個特性,數據存儲的每個位,在更新存儲數據時,能把1改成0,而0卻不能改為1。所以這就要求在寫入數據前,必須對目標區域進行擦除操作,即把目標區域中的數據位擦除為1。

W25Q64支持扇區擦除、塊擦除及整片擦除,最小的擦除單位是扇區,一個塊包含16個扇區,有128個塊。詳見圖18-1。

以扇區擦除為例,擦除大小為4KB,即4096位元組的數據。由於擦除實際上是往扇區寫入數據,把數據位都寫為1,所以在擦除前還需要進行「寫使能」,並且需要等待flash空閑。

/**n * @brief 寫使能n * @param 無n * @retval 無n */nvoid SPI_FLASH_WriteEnable(void)n{n FLASH_SPI_CS_LOW;n SPI_FLASH_SendByte(0x06); // 發送「Write Enable」指令,06hn FLASH_SPI_CS_HIGH;n}nn/**n * @brief 扇區擦除n * @param SectorAddr:擦除地址n * @retval 無n */nvoid SPI_FLASH_SectorErase(u32 SectorAddr)n{n SPI_FLASH_WriteEnable();n SPI_FLASH_WaitForWriteEnd();nn FLASH_SPI_CS_LOW;nn SPI_FLASH_SendByte(0x20);n SPI_FLASH_SendByte((SectorAddr & 0xFF0000) >> 16); // 高位擦除地址A23~A16n SPI_FLASH_SendByte((SectorAddr & 0xFF00) >> 8); // 中位擦除地址A15~A8n SPI_FLASH_SendByte(SectorAddr & 0xFF); // 低位擦除地址A7~A0nn FLASH_SPI_CS_HIGH;nn SPI_FLASH_WaitForWriteEnd();n}n

這個函數有兩個點需要注意,一是先發送高位擦除地址,二是擦除地址要對齊到4KB。

數據寫入與讀取

寫入和讀取函數也是發送指令到flash執行相應操作。

/**n * @brief 數據寫入n * @param n * @arg addr:寫入地址n * @arg writeBuff:包含數據的指針n * @arg numByteToWrite:寫入位元組數n * @retval 無n */nvoid SPI_Write_Data(uint32_t addr,uint8_t *writeBuff,uint32_t numByteToWrite)n{n SPI_FLASH_WriteEnable();n FLASH_SPI_CS_LOW;nn SPI_FLASH_Send_Byte(0x02); // 發送「Page Program」指令,02hntn SPI_FLASH_Send_Byte((addr>>16) & 0xff);n SPI_FLASH_Send_Byte((addr>>8) & 0xff); n SPI_FLASH_Send_Byte(addr & 0xff); ntn while(numByteToWrite--)n {tntSPI_FLASH_Send_Byte(*writeBuff);ntwriteBuff++;n }ntn FLASH_SPI_CS_HIGH;tnn SPI_WaitForWriteEnd();n}n

這個寫入函數發送的是「Page Program」指令,執行頁寫入操作。頁寫入一次最多發送256位元組的數據。

#define Dummy_Byte 0xFF // 任意值nn/**n * @brief 讀取flash數據n * @param n * @arg addr:讀取地址n * @arg readBuff:存放讀出的數據的指針n * @arg numByteToWrite:讀出的位元組數n * @retval 無n */nvoid SPI_Read_Data(uint32_t addr,uint8_t *readBuff,uint32_t numByteToRead)n{n FLASH_SPI_CS_LOW;n n SPI_FLASH_Send_Byte(0x03); // 發送「Read Data」指令,03hn SPI_FLASH_Send_Byte((addr>>16) & 0xff);n SPI_FLASH_Send_Byte((addr>>8) & 0xff); n SPI_FLASH_Send_Byte(addr & 0xff); ntn while(numByteToRead--)n {tn *readBuff = SPI_FLASH_Send_Byte(Dummy_Byte);ntreadBuff++;n }nntn FLASH_SPI_CS_HIGH;tnn}n

至此,基本上的函數差不多都涉及到了,最後再main函數里調用並測試數據即可。

-----------------------------------------------------------------------

文章首發於知乎專欄 - stm32,轉載請私信,並註明原文出處。

推薦閱讀:

Sound of silence: 數據傳輸的小眾黑科技
SPI(中)——通訊過程時序
DMA數據傳輸
I2C協議(下)——GPIO模擬I2C的簡單讀寫實驗
可不可以用動態圖像(動態二維碼)傳輸數據,具體怎麼實現?

TAG:通信 | 数据传输 | SPI |