stm32學習總結)—SPI-FLASH 實驗 _
SPI匯流排
SPI 簡介
SPI 的全稱是"Serial Peripheral Interface",意為序列外圍介面,是Motorola 首先在其 MC68HCXX 系列處理器上定義的。SPI 介面主要應用在 EEPROM、 FLASH、實時時鐘、AD 轉換器,還有數字訊號處理器和數字訊號解碼器之間。SPI是一種高速的,全雙工,同步的通訊匯流排,並且在晶片的管腳上只佔用四根線,節約了晶片的管腳,同時為 PCB 的佈局上節省空間,提供方便,正是出於這種簡單易用的特性,如今越來越多的晶片集成了這種通訊協議,比如 STM32 系列晶片。下面我們看下 SPI 內部結構簡易圖,如圖 39.1.1.1 所示:SPI 物理層
SPI 通訊裝置之間的常用連線方式見圖 25-1。 SPI 通訊使用 3 條匯流排及片選線,3 條匯流排分別為 SCK、MOSI、MISO,片選線為SS,它們的作用介紹如下: (1) SS( Slave Select):從裝置選擇訊號線,常稱為片選訊號線,也稱為 NSS、CS,以下用 NSS 表示。當有多個 SPI 從裝置與 SPI 主機相連時,裝置的其它訊號線 SCK、MOSI 及 MISO 同時並聯到相同的 SPI 總線上,即無論有多少個從裝置,都共同只使用這 3 條匯流排;而每個從裝置都有獨立的這一條 NSS 訊號線,本訊號線獨佔主機的一個引腳,即有多少個從裝置,就有多少條片選訊號線。I2C 協議中通過裝置地址來定址、選中總線上的某個裝置並與其進行通訊;而 SPI 協議中沒有裝置地址,它使用 NSS 訊號線來定址,當主機要選擇從裝置時,把該從裝置的 NSS 訊號線設定為低電平,該從裝置即被選中,即片選有效,接著主機開始與被選中的從裝置進行 SPI 通訊。所以SPI 通訊以 NSS 線置低電平為開始訊號,以 NSS 線被拉高作為結束訊號。 (2) SCK (Serial Clock)協議層
通訊過程
STM32 使用 SPI 外設通訊時,在通訊的不同階段它會對“狀態暫存器 SR”的不同資料位寫入引數,我們通過讀取這些暫存器標誌來了解通訊狀態。 圖 25-6 中的是“主模式”流程,即 STM32 作為 SPI 通訊的主機端時的資料收發過程。 主模式收發流程及事件說明如下: (1) 控制 NSS 訊號線,產生起始訊號(圖中沒有畫出); (2) 把要傳送的資料寫入到“資料暫存器 DR”中,該資料會被儲存到傳送緩衝區; (3) 通訊開始,SCK 時鐘開始執行。MOSI 把傳送緩衝區中的資料一位一位地傳輸出去;MISO 則把資料一位一位地儲存進接收緩衝區中; (4) 當傳送完一幀資料的時候,“狀態暫存器 SR”中的“TXE 標誌位”會被置 1,表示傳輸完一幀,傳送緩衝區已空;類似地,當接收完一幀資料的時候,“RXNE標誌位”會被置 1,表示傳輸完一幀,接收緩衝區非空; (5) 等待到“TXE 標誌位”為 1 時,若還要繼續傳送資料,則再次往“資料暫存器DR”寫入資料即可;等待到“RXNE 標誌位”為 1 時,通過讀取“資料暫存器DR”可以獲取接收緩衝區中的內容。 假如我們使能了 TXE 或 RXNE 中斷,TXE 或 RXNE 置 1 時會產生 SPI 中斷訊號,進入同一個中斷服務函式,到 SPI 中斷服務程式後,可通過檢查暫存器位來了解是哪一個事件,再分別進行處理。也可以使用 DMA 方式來收發“資料暫存器 DR”中的資料。SPI 初始化結構體詳解
跟其它外設一樣,STM32 標準庫提供了 SPI 初始化結構體及初始化函式來配置 SPI 外設。初始化結構體及函式定義在庫檔案“stm32f4xx_spi.h”及“stm32f4xx_spi.c”中,程式設計時我們可以結合這兩個檔案內的註釋使用或參考庫幫助文件。瞭解初始化結構體後我們就能對 SPI 外設運用自如了,程式碼如下。 這些結構體成員說明如下,其中括號內的文字是對應引數在 STM32 標準庫中定義的巨集: (1) SPI_Direction 本成員設定 SPI 的通訊方向,可設定為雙線全雙工(SPI_Direction_2Lines_FullDuplex),雙線只接收(SPI_Direction_2Lines_RxOnly),單線只接收(SPI_Direction_1Line_Rx)、單線只發送模式(SPI_Direction_1Line_Tx)。 (2) SPI_Mode 本成員設定 SPI 工作在主機模式(SPI_Mode_Master)或從機模式(SPI_Mode_Slave ),這兩個模式的最大區別為 SPI 的 SCK 訊號線的時序,SCK 的時序是由通訊中的主機產生的。若被配置為從機模式,STM32 的 SPI 外設將接受外來的 SCK 訊號。 (3) SPI_DataSize 本成員可以選擇 SPI 通訊的資料幀大小是為 8 位(SPI_DataSize_8b)還是 16 位(SPI_DataSize_16b)。 (4) SPI_CPOL 和 SPI_CPHA 這兩個成員配置 SPI 的時鐘極性 CPOL 和時鐘相位 CPHA,這兩個配置影響到 SPI 的通訊模式,關於 CPOL 和 CPHA 的說明參考前面“通訊模式”小節。 時鐘極性 CPOL 成員,可設定為高電平(SPI_CPOL_High)或低電平(SPI_CPOL_Low )。 時鐘相位 CPHA 則可以設定為 SPI_CPHA_1Edge(在 SCK 的奇數邊沿採集資料) 或SPI_CPHA_2Edge (在 SCK 的偶數邊沿採集資料) 。 (5) SPI_NSS 本成員配置 NSS 引腳的使用模式,可以選擇為硬體模式(SPI_NSS_Hard )與軟體模式(SPI_NSS_Soft ),在硬體模式中的 SPI 片選訊號由 SPI 硬體自動產生,而軟體模式則需要我們親自把相應的 GPIO 埠拉高或置低產生非片選和片選訊號。實際中軟體模式應用比較多。 (6) SPI_BaudRatePrescaler 本成員設定波特率分頻因子,分頻後的時鐘即為 SPI 的 SCK 訊號線的時鐘頻率。這個成員引數可設定為 fpclk 的 2、4、6、8、16、32、64、128、256 分頻。 (7) SPI_FirstBit 所有序列的通訊協議都會有 MSB 先行(高位資料在前)還是 LSB 先行(低位資料在前)的問題,而 STM32 的 SPI 模組可以通過這個結構體成員,對這個特性程式設計控制。 (8) SPI_CRCPolynomial 這是 SPI 的 CRC 校驗中的多項式,若我們使用 CRC 校驗時,就使用這個成員的引數(多項式),來計算 CRC 的值。 配置完這些結構體成員後,我們要呼叫 SPI_Init 函式把這些引數寫入到暫存器中,實現 SPI 的初始化,然後呼叫 SPI_Cmd 來使能 SPI 外設。SPI 配置步驟
(1)使能 SPI 及對應 GPIO 埠時鐘並配置引腳的複用功能 要使用 SPI 就必須使能它的時鐘,前面介紹框圖時,我們知道 SPI1 是掛接在 APB2 總線上,而 SPI2 和 SPI3 掛接在 APB1 總線上。而且 SPI 匯流排介面對應不同的 STM32 引腳,所以還需使能對應引腳的埠時鐘,同時配置為複用功能。 (2)初始化 SPI,包括資料幀長度、傳輸模式、MSB 和 LSB 順序等 (3)使能(開啟)SPI (4)SPI 資料傳輸 通過上面幾個步驟的配置,SPI 已經可以開始通訊了,在通訊的過程中肯定會有資料的傳送和接收,韌體庫也提供了 SPI 的傳送和接收函式。 SPI 傳送資料函式原型為: void SPI_I2S_SendData(SPI_TypeDef* SPIx, uint16_t Data);這個函式很好理解,往 SPIx 資料暫存器寫入資料 Data,從而實現傳送。 SPI 接收資料函式原型為: uint16_t SPI_I2S_ReceiveData(SPI_TypeDef* SPIx);此函式非常簡單,從 SPIx 資料暫存器中讀取接收到的資料。 (5)檢視 SPI 傳輸狀態 在 SPI 傳輸過程中,我們經常要判斷資料是否傳輸完成,傳送區是否為空等狀態,這是通過函式 SPI_I2S_GetFlagStatus 實現的,此函式原型為: FlagStatus SPI_I2S_GetFlagStatus(SPI_TypeDef* SPIx, uint16_t SPI_I2S_FLAG); 此函式非常簡單,第二個引數是用來選擇 SPI 傳輸過程中判斷的標誌,對應的標誌可在 stm32f10x_spi.h 檔案中查詢到,使用較多的是傳送完成標誌(SPI_I2S_FLAG_TXE)和接收完成標誌(SPI_I2S_FLAG_RXNE)。 判斷髮送是否完成的方法是: SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE); 將以上幾步配置好後,我們就可以使用 STM32F1 的 SPI 和外部 FLASH(EN25QXX)通訊了。spi配置程式碼(只配置了spi1)
1 #ifndef _spi_H 2 #define _spi_H 3 4 #include "system.h" 5 6 void SPI1_Init(void); //初始化SPI1口 7 void SPI1_SetSpeed(u8 SpeedSet); //設定SPI1速度 8 u8 SPI1_ReadWriteByte(u8 TxData);//SPI1匯流排讀寫一個位元組 9 10 //void SPI2_Init(void); //初始化SPI2口 11 //void SPI2_SetSpeed(u8 SpeedSet); //設定SPI2速度 12 //u8 SPI2_ReadWriteByte(u8 TxData);//SPI2匯流排讀寫一個位元組 13 14 #endif
1 #include "spi.h" 2 3 //以下是SPI模組的初始化程式碼,配置成主機模式 4 //SPI口初始化 5 //這裡針是對SPI1的初始化 6 void SPI1_Init(void) 7 { 8 GPIO_InitTypeDef GPIO_InitStructure; 9 SPI_InitTypeDef SPI_InitStructure; 10 11 /* SPI的IO口和SPI外設開啟時鐘 */ 12 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); 13 RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE); 14 15 /* SPI的IO口設定 */ 16 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7; 17 GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; //複用推輓輸出 18 GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; 19 GPIO_Init(GPIOA, &GPIO_InitStructure); 20 21 SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //設定SPI單向或者雙向的資料模式:SPI設定為雙線雙向全雙工 22 SPI_InitStructure.SPI_Mode = SPI_Mode_Master; //設定SPI工作模式:設定為主SPI 23 SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; //設定SPI的資料大小:SPI傳送接收8位幀結構 24 SPI_InitStructure.SPI_CPOL = SPI_CPOL_High; //串行同步時鐘的空閒狀態為高電平 25 SPI_InitStructure.SPI_CPHA = SPI_CPHA_2Edge; //串行同步時鐘的第二個跳變沿(上升或下降)資料被取樣 26 SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; //NSS訊號由硬體(NSS管腳)還是軟體(使用SSI位)管理:內部NSS訊號有SSI位控制 27 SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_256; //定義波特率預分頻的值:波特率預分頻值為256 28 SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; //指定資料傳輸從MSB位還是LSB位開始:資料傳輸從MSB位開始 29 SPI_InitStructure.SPI_CRCPolynomial = 7; //CRC值計算的多項式 30 SPI_Init(SPI1, &SPI_InitStructure); //根據SPI_InitStruct中指定的引數初始化外設SPIx暫存器 31 32 SPI_Cmd(SPI1, ENABLE); //使能SPI外設 33 34 SPI1_ReadWriteByte(0xff);//啟動傳輸 35 } 36 37 //SPI1速度設定函式 38 //SPI速度=fAPB2/分頻係數 39 //@ref SPI_BaudRate_Prescaler:SPI_BaudRatePrescaler_2~SPI_BaudRatePrescaler_256 40 //fAPB2時鐘一般為84Mhz: 41 void SPI1_SetSpeed(u8 SPI_BaudRatePrescaler) 42 { 43 SPI1->CR1&=0XFFC7;//位3-5清零,用來設定波特率 44 SPI1->CR1|=SPI_BaudRatePrescaler; //設定SPI1速度 45 SPI_Cmd(SPI1,ENABLE); //使能SPI1 46 } 47 48 //SPI1 讀寫一個位元組 49 //TxData:要寫入的位元組 50 //返回值:讀取到的位元組 51 u8 SPI1_ReadWriteByte(u8 TxData) 52 { 53 54 while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) == RESET);//等待發送區空 55 56 SPI_I2S_SendData(SPI1, TxData); //通過外設SPIx傳送一個byte 資料 57 58 while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) == RESET); //等待接收完一個byte 59 60 return SPI_I2S_ReceiveData(SPI1); //返回通過SPIx最近接收的資料 61 62 }
上述程式中的一個奇怪的地方
在複用SPI匯流排時,必須先設定匯流排埠。讀取其他ARM晶片(如NXP)一般很容易看出晶片的設定是否正確。不過對於STM32就容易讓人迷惑了。就像上述程式中,我們在使用SPI匯流排進行通訊時,可以這樣設定:
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; // 複用的推輓輸出
其他埠如時鐘埠以及MOSI埠都是stm32向外輸出,引腳設定成推輓輸出沒問題, 但是大家對MISO埠的設定就會產生疑惑了,MISO不是應該設定成為輸入埠(GPIO_Mode_IN_FLOATING)才行的嗎?
答題是肯定的,對於STM32的這一類管腳來說(如USART_RX)即可以設定成為輸入模式,也可以設定成為複用的推輓輸出。其工作都是正常的,不過建議大家還是設定成為輸入埠的好,容易理解。
具體產生這一問題的原因是:從功能上來說,MISO應該配置為輸入模式才對,但為什麼也可以配置為GPIO_Mode_AF_PP?請看下面的GPIO複用功能配置框圖。當一個GPIO埠配置為GPIO_Mode_AF_PP是,這個埠的內部結構框圖如下:
圖中可以看到,片上外設的複用功能輸出訊號會連線到輸出控制電路,然後在埠上產生輸出訊號。但是在晶片內部,MISO是SPI模組的輸入引腳,而不是輸出引腳,也就是說圖中的"複用功能輸出訊號"根本不存在(MISO不會產生輸出訊號),因此"輸出控制電路"不能對外產生輸出訊號。
而另一方面看,即使在GPIO_Mode_AF_PP模式下,複用功能輸入訊號卻與外部引腳之間相互連線,既MISO得到了外部訊號的電平,實現了輸入的功能(可以4-5-6-7路線輸入資料,複用的情況下就是4-5-複用功能路線)。
FLASH介紹
控制 FLASH 的指令
搞定 SPI 的基本收發單元后,還需要了解如何對 FLASH 晶片進行讀寫。FLASH 晶片自定義了很多指令,我們通過控制 STM32 利用 SPI 匯流排向 FLASH 晶片傳送指令,FLASH晶片收到後就會執行相應的操作。而這些指令,對主機端(STM32)來說,只是它遵守最基本的 SPI 通訊協議傳送出的資料,但在裝置端(FLASH 晶片)把這些資料解釋成不同的意義,所以才成為指令。檢視FLASH 晶片的資料手冊《W25Q64》,可瞭解各種它定義的各種指令的功能及指令格式,見表 25-4。 該表中的第一列為指令名,第二列為指令編碼,第三至第 N 列的具體內容根據指令的不同而有不同的含義。其中帶括號的位元組引數,方向為 FLASH 向主機傳輸,即命令響應,不帶括號的則為主機向 FLASH 傳輸。表中“A0~A23”指 FLASH 晶片內部儲存器組織的地址;“M0~M7”為廠商號(MANUFACTURER ID);“ID0-ID15”為 FLASH 晶片的ID;“dummy”指該處可為任意資料;“D0~D7”為 FLASH 內部儲存矩陣的內容。 在 FLSAH 晶片內部,儲存有固定的廠商編號(M7-M0)和不同型別 FLASH 晶片獨有的編號(ID15-ID0),見表 25-5。 通過指令表中的讀 ID 指令“JEDEC ID”可以獲取這兩個編號,該指令編碼為“9Fh”,其中“9F h”是指 16 進位制數“9F” (相當於 C 語言中的 0x9F)。緊跟指令編碼的三個位元組分別為 FLASH 晶片輸出的“(M7-M0)”、“(ID15-ID8)”及“(ID7-ID0)” 。此處我們以該指令為例,配合其指令時序圖進行講解,見圖 25-8。 主機首先通過 MOSI 線向 FLASH 晶片傳送第一個位元組資料為“9F h”,當 FLASH 晶片收到該資料後,它會解讀成主機向它傳送了“JEDEC 指令”,然後它就作出該命令的響應:通過 MISO 線把它的廠商 ID(M7-M0)及晶片型別(ID15-0)傳送給主機,主機接收到指令響應後可進行校驗。常見的應用是主機端通過讀取裝置 ID 來測試硬體是否連線正常,或用於識別裝置。對於 FLASH 晶片的其它指令,都是類似的,只是有的指令包含多個位元組,或者響應包含更多的資料。 實際上,編寫裝置驅動都是有一定的規律可循的。首先我們要確定裝置使用的是什麼通訊協議。如上一章的 EEPROM 使用的是 I2C,本章的 FLASH 使用的是 SPI。那麼我們就先根據它的通訊協議,選擇好 STM32 的硬體模組,並進行相應的 I2C 或 SPI 模組初始化。接著,我們要了解目標裝置的相關指令,因為不同的裝置,都會有相應的不同的指令。如EEPROM 中會把第一個資料解釋為內部儲存矩陣的地址(實質就是指令)。而 FLASH 則定義了更多的指令,有寫指令,讀指令,讀 ID 指令等等。最後,我們根據這些指令的格式要求,使用通訊協議向裝置傳送指令,達到控制裝置的目標定義 FLASH 指令編碼表
為了方便使用,我們把 FLASH 晶片的常用指令編碼使用巨集來封裝起來,後面需要傳送指令編碼的時候我們直接使用這些巨集即可。1 //指令表 2 #define EN25X_WriteEnable 0x06 3 #define EN25X_WriteDisable 0x04 4 #define EN25X_ReadStatusReg 0x05 5 #define EN25X_WriteStatusReg 0x01 6 #define EN25X_ReadData 0x03 7 #define EN25X_FastReadData 0x0B 8 #define EN25X_FastReadDual 0x3B 9 #define EN25X_PageProgram 0x02 10 #define EN25X_BlockErase 0xD8 11 #define EN25X_SectorErase 0x20 12 #define EN25X_ChipErase 0xC7 13 #define EN25X_PowerDown 0xB9 14 #define EN25X_ReleasePowerDown 0xAB 15 #define EN25X_DeviceID 0xAB 16 #define EN25X_ManufactDeviceID 0x90 17 #define EN25X_JedecDeviceID 0x9F
讀取 FLASH 晶片 ID
根據“JEDEC”指令的時序,我們把讀取 FLASH ID 的過程編寫成一個函式。1 //讀取晶片ID 2 //返回值如下: 3 //0XEF13,表示晶片型號為EN25Q80 4 //0XEF14,表示晶片型號為EN25Q16 5 //0XEF15,表示晶片型號為EN25Q32 6 //0XEF16,表示晶片型號為EN25Q64 7 //0XEF17,表示晶片型號為EN25Q128 8 u16 EN25QXX_ReadID(void) 9 { 10 u16 Temp = 0; 11 EN25QXX_CS=0; 12 SPI2_ReadWriteByte(0x9F);//傳送讀取ID命令 13 SPI2_ReadWriteByte(0x00); 14 SPI2_ReadWriteByte(0x00); 15 SPI2_ReadWriteByte(0x00); 16 Temp|=SPI2_ReadWriteByte(0xFF)<<8; 17 Temp|=SPI2_ReadWriteByte(0xFF); 18 //EN25QXX_CS=1; 19 return Temp; 20 }這段程式碼利用控制 CS 引腳電平的巨集“SPI_FLASH_CS_LOW/HIGH”以及前面編寫的單位元組收發函式 SPI_FLASH_SendByte,很清晰地實現了“JEDEC ID”指令的時序:傳送一個位元組的指令編碼“W25X_JedecDeviceID”,然後讀取 3 個位元組,獲取 FLASH 晶片對該指令的響應,最後把讀取到的這 3 個數據合併到一個變數 Temp 中,然後作為函式返回值,把該返回值與我們定義的巨集“sFLASH_ID”對比,即可知道 FLASH 晶片是否正常。
FLASH 寫使能以及讀取當前狀態
在向 FLASH 晶片儲存矩陣寫入資料前,首先要使能寫操作,通過“Write Enable”命令即可寫使能。1 //EN25QXX寫使能 2 //將WEL置位 3 void EN25QXX_Write_Enable(void) 4 { 5 EN25QXX_CS=0; //使能器件 6 SPI2_ReadWriteByte(EN25X_WriteEnable); //傳送寫使能 7 EN25QXX_CS=1; //取消片選 8 }與 EEPROM 一樣,由於 FLASH 晶片向內部儲存矩陣寫入資料需要消耗一定的時間,並不是在匯流排通訊結束的一瞬間完成的,所以在寫操作後需要確認 FLASH 晶片“空閒”時才能進行再次寫入。為了表示自己的工作狀態,FLASH 晶片定義了一個狀態暫存器,見圖 25-9 我們只關注這個狀態暫存器的第 0 位“BUSY”,當這個位為“1”時,表明 FLASH晶片處於忙碌狀態,它可能正在對內部的儲存矩陣進行“擦除”或“資料寫入”的操作。利用指令表中的“Read Status Register”指令可以獲取 FLASH 晶片狀態暫存器的內容,其時序見圖 25-10。 只要向 FLASH 晶片傳送了讀狀態暫存器的指令,FLASH 晶片就會持續向主機返回最新的狀態暫存器內容,直到收到 SPI 通訊的停止訊號。據此我們編寫了具有等待 FLASH 晶片寫入結束功能的函式,見下面程式碼。
1 //讀取EN25QXX的狀態暫存器 2 //BIT7 6 5 4 3 2 1 0 3 //SPR RV TB BP2 BP1 BP0 WEL BUSY 4 //SPR:預設0,狀態暫存器保護位,配合WP使用 5 //TB,BP2,BP1,BP0:FLASH區域防寫設定 6 //WEL:寫使能鎖定 7 //BUSY:忙標記位(1,忙;0,空閒) 8 //預設:0x00 9 u8 EN25QXX_ReadSR(void) 10 { 11 u8 byte=0; 12 EN25QXX_CS=0; //使能器件 13 SPI2_ReadWriteByte(EN25X_ReadStatusReg); //傳送讀取狀態暫存器命令 14 byte=SPI2_ReadWriteByte(0Xff); //讀取一個位元組 15 EN25QXX_CS=1; //取消片選 16 return byte; 17 }
1 //等待空閒 2 void EN25QXX_Wait_Busy(void) 3 { 4 while((EN25QXX_ReadSR()&0x01)==0x01); // 等待BUSY位清空 5 }
FLASH 扇區擦除
由於 FLASH 儲存器的特性決定了它只能把原來為“1”的資料位改寫成“0”,而原來為“0”的資料位不能直接改寫為“1”。所以這裡涉及到資料“擦除”的概念,在寫入前,必須要對目標儲存矩陣進行擦除操作,把矩陣中的資料位擦除為“1”,在資料寫入的時候,如果要儲存資料“1”,那就不修改儲存矩陣 ,在要儲存資料“0”時,才更改該位。通常,對儲存矩陣擦除的基本操作單位都是多個位元組進行,如本例子中的 FLASH 芯 片支援“扇區擦除”、“塊擦除”以及“整片擦除”,見表 25-6。 FLASH 晶片的最小擦除單位為扇區(Sector),而一個塊(Block)包含 16 個扇區,其內部儲存矩陣分佈見圖 25-11。 使用扇區擦除指令“Sector Erase”可控制 FLASH 晶片開始擦寫,其指令時序見圖25-14。 扇區擦除指令的第一個位元組為指令編碼,緊接著傳送的 3 個位元組用於表示要擦除的儲存矩陣地址。要注意的是在扇區擦除指令前,還需要先發送“寫使能”指令,傳送扇區擦除指令後,通過讀取暫存器狀態等待扇區擦除操作完畢,程式碼如下。1 //擦除一個扇區 2 //Dst_Addr:扇區地址 根據實際容量設定 3 //擦除一個山區的最少時間:150ms 4 void EN25QXX_Erase_Sector(u32 Dst_Addr) 5 { 6 //監視falsh擦除情況,測試用 7 printf("fe:%x\r\n",Dst_Addr); 8 Dst_Addr*=4096; 9 EN25QXX_Write_Enable(); //SET WEL 10 EN25QXX_Wait_Busy(); 11 EN25QXX_CS=0; //使能器件 12 SPI2_ReadWriteByte(EN25X_SectorErase); //傳送扇區擦除指令 13 SPI2_ReadWriteByte((u8)((Dst_Addr)>>16)); //傳送24bit地址 14 SPI2_ReadWriteByte((u8)((Dst_Addr)>>8)); 15 SPI2_ReadWriteByte((u8)Dst_Addr); 16 EN25QXX_CS=1; //取消片選 17 EN25QXX_Wait_Busy(); //等待擦除完成 18 }這段程式碼呼叫的函式在前面都已講解,只要注意傳送擦除地址時高位在前即可。呼叫扇區擦除指令時注意輸入的地址要對齊到 4KB。
FLASH 的頁寫入
目標扇區被擦除完畢後,就可以向它寫入資料了。與 EEPROM 類似,FLASH 晶片也有頁寫入命令,使用頁寫入命令最多可以一次向 FLASH 傳輸 256 個位元組的資料,我們把這個單位為頁大小。FLASH 頁寫入的時序見圖 25-13。 從時序圖可知,第 1 個位元組為“頁寫入指令”編碼,2-4 位元組為要寫入的“地址 A”,接著的是要寫入的內容,最多個可以傳送 256 位元組資料,這些資料將會從“地址 A”開始,按順序寫入到 FLASH 的儲存矩陣。若傳送的資料超出 256 個,則會覆蓋前面傳送的資料。與擦除指令不一樣,頁寫入指令的地址並不要求按 256 位元組對齊,只要確認目標儲存單元是擦除狀態即可(即被擦除後沒有被寫入過)。所以,若對“地址 x”執行頁寫入指令後,傳送了 200 個位元組資料後終止通訊,下一次再執行頁寫入指令,從“地址(x+200)”開始寫入 200 個位元組也是沒有問題的(小於 256 均可)。 只是在實際應用中由於基本擦除單元是4KB,一般都以扇區為單位進行讀寫,想深入瞭解,可學習我們的“FLASH 檔案系統”相關的例子。把頁寫入時序封裝成函式,其實現見下列程式碼。1 //寫SPI FLASH 2 //在指定地址開始寫入指定長度的資料 3 //該函式帶擦除操作! 4 //pBuffer:資料儲存區 5 //WriteAddr:開始寫入的地址(24bit) 6 //NumByteToWrite:要寫入的位元組數(最大65535) 7 u8 EN25QXX_BUFFER[4096]; 8 void EN25QXX_Write(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite) 9 { 10 u32 secpos; 11 u16 secoff; 12 u16 secremain; 13 u16 i; 14 u8 * EN25QXX_BUF; 15 EN25QXX_BUF=EN25QXX_BUFFER; 16 secpos=WriteAddr/4096;//扇區地址 17 secoff=WriteAddr%4096;//在扇區內的偏移 18 secremain=4096-secoff;//扇區剩餘空間大小 19 //printf("ad:%X,nb:%X\r\n",WriteAddr,NumByteToWrite);//測試用 20 if(NumByteToWrite<=secremain)secremain=NumByteToWrite;//不大於4096個位元組 21 while(1) 22 { 23 EN25QXX_Read(EN25QXX_BUF,secpos*4096,4096);//讀出整個扇區的內容 24 for(i=0;i<secremain;i++)//校驗資料 25 { 26 if(EN25QXX_BUF[secoff+i]!=0XFF)break;//需要擦除 27 } 28 if(i<secremain)//需要擦除 29 { 30 EN25QXX_Erase_Sector(secpos);//擦除這個扇區 31 for(i=0;i<secremain;i++) //複製 32 { 33 EN25QXX_BUF[i+secoff]=pBuffer[i]; 34 } 35 EN25QXX_Write_NoCheck(EN25QXX_BUF,secpos*4096,4096);//寫入整個扇區 36 37 }else EN25QXX_Write_NoCheck(pBuffer,WriteAddr,secremain);//寫已經擦除了的,直接寫入扇區剩餘區間. 38 if(NumByteToWrite==secremain)break;//寫入結束了 39 else//寫入未結束 40 { 41 secpos++;//扇區地址增1 42 secoff=0;//偏移位置為0 43 44 pBuffer+=secremain; //指標偏移 45 WriteAddr+=secremain;//寫地址偏移 46 NumByteToWrite-=secremain; //位元組數遞減 47 if(NumByteToWrite>4096)secremain=4096; //下一個扇區還是寫不完 48 else secremain=NumByteToWrite; //下一個扇區可以寫完了 49 } 50 } 51 }這段程式碼的內容為:先發送“寫使能”命令,接著才開始頁寫入時序,然後傳送指令編碼、地址,再把要寫入的資料一個接一個地傳送出去,傳送完後結束通訊,檢查 FLASH狀態暫存器,等待 FLASH 內部寫入結束。
從 FLASH 讀取資料
相對於寫入,FLASH 晶片的資料讀取要簡單得多,使用讀取指令“Read Data”即可,其指令時序見圖 25-14。 傳送了指令編碼及要讀的起始地址後,FLASH 晶片就會按地址遞增的方式返回儲存矩陣的內容,讀取的資料量沒有限制,只要沒有停止通訊,FLASH 晶片就會一直返回資料。程式碼如下。1 //讀取SPI FLASH 2 //在指定地址開始讀取指定長度的資料 3 //pBuffer:資料儲存區 4 //ReadAddr:開始讀取的地址(24bit) 5 //NumByteToRead:要讀取的位元組數(最大65535) 6 void EN25QXX_Read(u8* pBuffer,u32 ReadAddr,u16 NumByteToRead) 7 { 8 u16 i; 9 EN25QXX_CS=0; //使能器件 10 SPI2_ReadWriteByte(EN25X_ReadData); //傳送讀取命令 11 SPI2_ReadWriteByte((u8)((ReadAddr)>>16)); //傳送24bit地址 12 SPI2_ReadWriteByte((u8)((ReadAddr)>>8)); 13 SPI2_ReadWriteByte((u8)ReadAddr); 14 for(i=0;i<NumByteToRead;i++) 15 { 16 pBuffer[i]=SPI2_ReadWriteByte(0XFF); //迴圈讀數 17 } 18 EN25QXX_CS=1; 19 }由於讀取的資料量沒有限制,所以傳送讀命令後一直接收 NumByteToRead 個數據到結束即可。
3. main 函式
最後我們來編寫 main 函式,進行 FLASH 晶片讀寫校驗,程式碼如下。1 #include "system.h" 2 #include "SysTick.h" 3 #include "led.h" 4 #include "usart.h" 5 #include "tftlcd.h" 6 #include "key.h" 7 #include "spi.h" 8 #include "flash.h" 9 10 11 //要寫入到25Q64的字串陣列 12 const u8 text_buf[]="www.prechin.net"; 13 #define TEXT_LEN sizeof(text_buf) 14 //u16 key3; 15 16 int main() 17 { 18 u8 i=0; 19 u8 key; 20 u8 buf[30]; 21 22 SysTick_Init(72); 23 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //中斷優先順序分組 分2組 24 LED_Init(); 25 USART1_Init(9600); 26 TFTLCD_Init(); //LCD初始化 27 KEY_Init(); 28 EN25QXX_Init(); 29 30 FRONT_COLOR=BLACK; 31 LCD_ShowString(10,10,tftlcd_data.width,tftlcd_data.height,16,"PRECHIN STM32F1"); 32 LCD_ShowString(10,30,tftlcd_data.width,tftlcd_data.height,16,"www.prechin.net"); 33 LCD_ShowString(10,50,tftlcd_data.width,tftlcd_data.height,16,"FLASH-SPI Test"); 34 LCD_ShowString(10,70,tftlcd_data.width,tftlcd_data.height,16,"K_UP:Write K_DOWN:Read"); 35 FRONT_COLOR=RED; 36 37 while(EN25QXX_ReadID()!=EN25Q64) //檢測不到EN25Q64 38 //while(1) 39 { 40 //key3 = EN25QXX_ReadID(); 41 printf("EN25Q64 Check Failed! \r\n"); 42 LCD_ShowString(10,150,tftlcd_data.width,tftlcd_data.height,16,"EN25Q64 Check Failed! "); 43 } 44 printf("EN25Q64 Check Success!\r\n"); 45 LCD_ShowString(10,150,tftlcd_data.width,tftlcd_data.height,16,"EN25Q64 Check Success!"); 46 47 LCD_ShowString(10,170,tftlcd_data.width,tftlcd_data.height,16,"Write Data:"); 48 LCD_ShowString(10,190,tftlcd_data.width,tftlcd_data.height,16,"Read Data :"); 49 50 while(1) 51 { 52 key=KEY_Scan(0); 53 if(key==KEY_UP) 54 { 55 EN25QXX_Write((u8 *)text_buf,0,TEXT_LEN); 56 printf("傳送的資料:%s\r\n",text_buf); 57 LCD_ShowString(10+11*8,170,tftlcd_data.width,tftlcd_data.height,16,"www.prechin.net"); 58 } 59 if(key==KEY_DOWN) 60 { 61 EN25QXX_Read(buf,0,TEXT_LEN); 62 printf("接收的資料:%s\r\n",buf); 63 LCD_ShowString(10+11*8,190,tftlcd_data.width,tftlcd_data.height,16,buf); 64 } 65 66 i++; 67 if(i%20==0) 68 { 69 led1=!led1; 70 } 71 72 delay_ms(10); 73 74 } 75 }注意: 由於實驗板上的 FLASH 晶片預設已經儲存了特定用途的資料,如擦除了這些資料會影響到某些程式的執行。所以我們預留了 FLASH 晶片的“第 0 扇區(0-4096 地址)”專用於本實驗,如非必要,請勿擦除其它地址的內容。如已擦除,可在配套資料裡找到“刷外部 FLASH 內容”程式,根據其說明給 FLASH 重新寫入出廠內容。
1 //無檢驗寫SPI FLASH 2 //必須確保所寫的地址範圍內的資料全部為0XFF,否則在非0XFF處寫入的資料將失敗! 3 //具有自動換頁功能 4 //在指定地址開始寫入指定長度的資料,但是要確保地址不越界! 5 //pBuffer:資料儲存區 6 //WriteAddr:開始寫入的地址(24bit) 7 //NumByteToWrite:要寫入的位元組數(最大65535) 8 //CHECK OK 9 void EN25QXX_Write_NoCheck(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite) 10 { 11 u16 pageremain; 12 pageremain=256-WriteAddr%256; //單頁剩餘的位元組數 13 if(NumByteToWrite<=pageremain)pageremain=NumByteToWrite;//不大於256個位元組 14 while(1) 15 { 16 EN25QXX_Write_Page(pBuffer,WriteAddr,pageremain); 17 if(NumByteToWrite==pageremain)break;//寫入結束了 18 else //NumByteToWrite>pageremain 19 { 20 pBuffer+=pageremain; 21 WriteAddr+=pageremain; 22 23 NumByteToWrite-=pageremain; //減去已經寫入了的位元組數 24 if(NumByteToWrite>256)pageremain=256; //一次可以寫入256個位元組 25 else pageremain=NumByteToWrite; //不夠256個位元組了 26 } 27 } 28 }
1 #ifndef _flash_H 2 #define _flash_H 3 4 #include "system.h" 5 6 7 //EN25X系列/Q系列晶片列表 8 //EN25Q80 ID 0XEF13 9 //EN25Q16 ID 0XEF14 10 //EN25Q32 ID 0XEF15 11 //EN25Q64 ID 0XEF16 12 //EN25Q128 ID 0XEF17 13 #define EN25Q80 0XEF13 14 #define EN25Q16 0XEF14 15 #define EN25Q32 0XEF15 16 //#define EN25Q64 0XEF16 17 //#define EN25Q128 0XEF17 18 //#define EN25Q64 0XC816 19 //#define EN25Q64 0X1C16 //GD25QXX 20 //#define EN25Q64 0X2016 //XM25QHXX 21 #define EN25Q64 0Xb16 //MXIC C216 22 #define EN25Q128 0XC817 23 24 extern u16 EN25QXX_TYPE; //定義EN25QXX晶片型號 25 26 #define EN25QXX_CS PGout(13) //EN25QXX的片選訊號 27 28 29 //指令表 30 #define EN25X_WriteEnable 0x06 31 #define EN25X_WriteDisable 0x04 32 #define EN25X_ReadStatusReg 0x05 33 #define EN25X_WriteStatusReg 0x01 34 #define EN25X_ReadData 0x03 35 #define EN25X_FastReadData 0x0B 36 #define EN25X_FastReadDual 0x3B 37 #define EN25X_PageProgram 0x02 38 #define EN25X_BlockErase 0xD8 39 #define EN25X_SectorErase 0x20 40 #define EN25X_ChipErase 0xC7 41 #define EN25X_PowerDown 0xB9 42 #define EN25X_ReleasePowerDown 0xAB 43 #define EN25X_DeviceID 0xAB 44 #define EN25X_ManufactDeviceID 0x90 45 #define EN25X_JedecDeviceID 0x9F 46 47 void EN25QXX_Init(void); 48 u16 EN25QXX_ReadID(void); //讀取FLASH ID 49 u8 EN25QXX_ReadSR(void); //讀取狀態暫存器 50 void EN25QXX_Write_SR(u8 sr); //寫狀態暫存器 51 void EN25QXX_Write_Enable(void); //寫使能 52 void EN25QXX_Write_Disable(void); //防寫 53 void EN25QXX_Write_NoCheck(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite); 54 void EN25QXX_Read(u8* pBuffer,u32 ReadAddr,u16 NumByteToRead); //讀取flash 55 void EN25QXX_Write(u8* pBuffer,u32 WriteAddr,u16 NumByteToWrite);//寫入flash 56 void EN25QXX_Erase_Chip(void); //整片擦除 57 void EN25QXX_Erase_Sector(u32 Dst_Addr); //扇區擦除 58 void EN25QXX_Wait_Busy(void); //等待空閒 59 void EN25QXX_PowerDown(void); //進入掉電模式 60 void EN25QXX_WAKEUP(void); //喚醒 61 62 63 #endif