1. SPI總線簡介
SPI全稱串行外設接口,是一種高速,全雙工,同步的外設總線;它工作在主從方式,常規需要至少4根線才能夠正常工作。SPI作為基本的外設接口,在FLASH,EPPROM和一些數字通訊中,具有廣泛的應用。SPI總線由四個接口構成:
CS :片選端,由主設備控制
MISO:主設備輸入,從設備輸出
MOSI:主設備輸出,從設備輸入
SCK :時鍾信號
其中SCK僅能由主設備提供,且接收和發送和同時產生的,因此在主設備接收數據時也要先發送數據從而為從設備提供時鍾;根據SPI時鍾信號配置相關說明,SPI的時鍾相位和極性由CPOL和CPHA兩位控制共有四種不同的工作時序。
其中CPOL:0 空閑狀態低電平 1 空閑時候高電平
CPHA: 0 第一個邊沿采樣 1 第二個邊沿采樣
2. 工作原理圖
了解了SPI總線,下面就開始進入正題,通過SPI總線操作外部flash(W25X16)。首先確定開發板原理圖對應的端口連接:
從上可以得出 CS:PB9 SCK:PA5
MISO:PA6 MOSI:PA7
不過因為開發板的資源有限,SD卡和外部flash共用SPI總線,因此在讀取SPI FLASH之前要關閉SD卡的片選端,避免出現總線沖突。
了解了這些,就可以開始SPI_FLASH驅動硬件部分的編寫了。
3. SPI硬件驅動
SPI端口配置比較簡單,主要包含端口時鍾啟動,端口功能配置,初始化即可
GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_GPIOB |RCC_APB2Periph_GPIOD | RCC_APB2Periph_AFIO, ENABLE); /*SD_CS Disable PD11*/ GPIO_InitStructure.GPIO_Pin = SD_CS_Pin; GPIO_InitStructure.GPIO_Speed= GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(SD_CS_Port, &GPIO_InitStructure); GPIO_SetBits(SD_CS_Port, SD_CS_Pin); /*SPI1_CS 端口配置*/ GPIO_InitStructure.GPIO_Pin = SPI1_CS_Pin; GPIO_Init(SPI1_CS_Port, &GPIO_InitStructure); SPI1_CS_Disable(); /*SPI1_SCK 端口配置*/ GPIO_InitStructure.GPIO_Pin = SPI1_SCK_Pin; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_Init(SPI1_SCK_Port, &GPIO_InitStructure); /*SPI1_MISO 端口配置*/ GPIO_InitStructure.GPIO_Pin = SPI1_MISO_Pin; GPIO_Init(SPI1_MISO_Port, &GPIO_InitStructure); /*SPI1_MOSI 端口配置*/ GPIO_InitStructure.GPIO_Pin = SPI1_MOSI_Pin; GPIO_Init(SPI1_MOSI_Port, &GPIO_InitStructure);
SPI功能配置主要包含上面我提到的主從設備,時鍾相位和極性,發送數據長度和順序(STM32本身集成功能,與SPI本身關系不大)等,具體配置如下:
SPI_InitTypeDef SPI_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1 | RCC_APB2Periph_AFIO, ENABLE); SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //SPI工作在雙向雙線模式
SPI_InitStructure.SPI_Mode = SPI_Mode_Master; //CPU的SPI工作在主機模式
SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; //SPI傳輸數據幀長度為8字節
SPI_InitStructure.SPI_CPOL = SPI_CPOL_High; //空閑時SCK高電平 MODE3模式
SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; //第二個下降沿接收/發送數據
SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; //啟用軟件從設備管理
SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_4; //波特率為Pclk2/4
SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; //高位先發送
SPI_InitStructure.SPI_CRCPolynomial = 7; //默認CRC校驗多項式 x^2+x+1 SPI_Init(SPI1, &SPI_InitStructure);
SPI_Cmd(SPI1, ENABLE);
STM32因為SPI總線已經集成在CPU內部,因此配置起來十分簡單,僅修改部分寄存器就可以實現對於SPI總線的配置,用於操作外部設備,不過涉及到外部設備的通訊並沒有這么簡單,這涉及讀取和操作芯片的時序和指令,下面我以開發板上的W25X16外部flash為例,講解SPI總線的實際運用。
4. SPI總線操作W25X16
外部flash的操作比較簡單,總結起來僅讀寄存器,寫寄存器,讀數據,寫數據,擦除數據,讀ID這6種工作模式,如W25X16指令表如下:
參照該表,程序中就可以有如下flash操作指令定義

/*外部flash相關指令*/
#define Flash_WriteEnable 0x06
#define Flash_WriteDisable 0x04
#define Flash_ReadStatusReg 0x05
#define Flash_WriteStatusReg 0x01
#define Flash_ReadData 0x03
#define Flash_FastReadData 0x0B
#define Flash_FastReadDual 0x3B
#define Flash_PageProgram 0x02
#define Flash_BlockErase 0xD8
#define Flash_SectorErase 0x20
#define Flash_ChipErase 0xC7
#define Flash_PowerDown 0xB9
#define Flash_ReleasePowerDown 0xAB
#define Flash_ManufactDeviceID 0x90
#define Flash_JedecDeviceID 0x9F
#define Flash_NoBusy 0xA5
可以看出外部flash主要包含擦除,讀寄存器,寫入寄存器,讀數據,寫數據,讀ID這幾種方式。
(1). flash單字節收發
根據上面SPI總線的說明,SPI的寫入和讀出是同時發生的,且時鍾只能由主設備提供,因此SPI總線的收發由同一個函數完成。如下:
/*等待SPI發送數據寄存器為空時,發送1字節數據*/
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET) { } SPI_I2S_SendData(SPI1, byte); /*在發送數據同時,SPI_MISO引腳會讀取管腳數據,等待讀取寄存器非空*/
while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET) { } return SPI_I2S_ReceiveData(SPI1);
當然,實際項目中在while循環內部需要添加超時時鍾,避免因為可能出現的SPI硬件出錯而導致整個系統停止的問題。
(2). flash擦除
外部flash的擦除主要包含sector(扇區)擦除,block(塊)擦除,chip(整片)擦除。其中扇區擦除4kb, 塊擦除64kb, flash的擦除按照芯片資料上要求,擦除需要3步:
1.寫入允許(0x06)
2.擦除指令(0x20)
3. 寫入擦除地址(24bit), 分三次發送
由時序可知,擦除片代碼如下:
void SPI_EraseSector(u32 SectorAddress) { SectorAddress = (SectorAddress>>12)<<12; //確定擦除塊的首地址
SPI_Write_Enable(); //允許寫入 SPI1_CS_Enable(); SPI_SendWrite_Byte(Flash_SectorErase); //寫入擦除指令
/*寫入帶擦除的扇區*/ SPI_SendWrite_Byte((SectorAddress&0xFF0000)>>16); SPI_SendWrite_Byte((SectorAddress&0xFF00)>>8); SPI_SendWrite_Byte(SectorAddress&0xFF); SPI1_CS_Disable(); SPI_WaitWriteEnd(); }
(3). 數據讀取
數據讀取包含3步,1.寫入讀取指令 2.寫入待讀取數據地址 3.讀取flash內部數據。如此便完成外部flash的讀取。
void SPI_Read(u8 *pBuffer,u32 ReadAddress,u16 ReadByteNum) { SPI1_CS_Enable(); SPI_SendWrite_Byte(Flash_ReadData); //寫入讀取指令
SPI_SendWrite_Byte(ReadAddress >> 16); //寫入待讀取數據地址
SPI_SendWrite_Byte(ReadAddress >> 8); SPI_SendWrite_Byte(ReadAddress); /*讀取數據*/
while(ReadByteNum--) { *pBuffer = SPI_SendWrite_Byte(Flash_NoBusy); pBuffer++; } SPI1_CS_Disable(); }
(4)數據寫入
數據寫入包含4步,1.寫入允許 2.寫入數據寫入指令 3.寫入數據存儲地址 4.寫入數據。如此便完成外部flash的寫入。
void SPI_PageWrite(u8 *pBuffer, u32 PageAddress,u16 WriteByteNum) { SPI_Write_Enable(); //寫入允許 SPI1_CS_Enable(); SPI_SendWrite_Byte(Flash_PageProgram); //寫入存儲指令
SPI_SendWrite_Byte(PageAddress >> 16); //寫入存儲地址
SPI_SendWrite_Byte(PageAddress >> 8); SPI_SendWrite_Byte(PageAddress); if(WriteByteNum > 256) { WriteByteNum = 256; printf("\n\r Err: SPI_PageWrite too large!"); } /*寫入數據*/
while(WriteByteNum--) { SPI_SendWrite_Byte(*pBuffer); pBuffer++; } SPI1_CS_Disable(); SPI_WaitWriteEnd(); }
(5)ID讀取
ID讀取比較簡單,主要用來測試硬件是否成功,具體代碼如下
u32 SPI_ReadID(void) { u32 ID_Temp,temp_h,temp_m,temp_l; SPI1_CS_Enable(); SPI_SendWrite_Byte(Flash_JedecDeviceID); temp_h = SPI_SendWrite_Byte(Flash_NoBusy); temp_m = SPI_SendWrite_Byte(Flash_NoBusy); temp_l = SPI_SendWrite_Byte(Flash_NoBusy); SPI1_CS_Disable(); ID_Temp = (temp_h << 16)|(temp_m << 8)|temp_l; return ID_Temp; }
上面便是SPI總線基本操作了,具體可參考代碼:
http://files.cnblogs.com/files/zc110747/6.SPI-Flash.7z
根據代碼中設計以及通過串口輸出如下圖,可以判斷成功實現了SPI-flash的讀,寫和擦除的工作。