一、SPI簡介
- SPI(Serial Peripheral Interface)是由Motorola公司開發的一種通用數據總線
- 四根通信線:SCK(Serial Clock)、MOSI(Master Output Slave Input)、MISO(Master Input Slave Output)、SS(Slave Select)
- 同步,全雙工
- 支持總線掛載多設備(一主多從)
SCK:提供時鐘信號,數據位的輸出和輸入,都是在SCK的上升沿或下降沿進行的。?
MOSI:主機輸出從機輸入,主機配置為輸出。從機配置為輸入。??
MISO:主機輸入從機輸出
SS:從機選擇線。有幾個從機就有幾條SS
二、硬件電路
- 所有SPI設備的SCK、MOSI、MISO分別連在一起
- 主機另外引出多條SS控制線,分別接到各從機的SS引腳
- 輸出引腳配置為推挽輸出,輸入引腳配置為浮空或上拉輸入
解釋:
- 因為有三個從機,所以有三個SS線,一共就是6跟線。因為都是單端型號,所以所有的設備還需要共地。從機沒有獨立供電的話,主機還需要額外引出電源正極VCC,給從機供電。?
- SS線是低電平有效,同一時間只能置一個SS為低電平,只能選中一個從機,同時未選中的從機的MISO引腳,為高阻態,否則三個輸出同時進入主機的輸入會產生沖突
- 對于輸出,選擇推挽輸出,使得上升沿、下降沿非常迅速
上圖是SPI內部移位示意圖
步驟:SPI高位先行,每來一個時鐘,移位寄存器都會向左進行移位。
假設現在主機要將10101010發給從機,從機要將01010101發給主機。
首先向左移位
?移出去的數據會在輸出數據寄存器
此時MOSI為高電平1,MISO為低電平0。這就是第一個時鐘上升沿的結果?。那么在第一個時鐘的下降沿,寄存器里的數據被分別采樣輸入到對應要去的最低位。
這就是第一個時鐘結束后的現象。
三、SPI時序基本單元
- 起始條件:SS從高電平切換到低電平
- 終止條件:SS從低電平切換到高電平
住:這里一共有四種模式,由CPOL和CPHA變換1、0來選擇。是為了適配更多的設備而設置的。傳輸流程看上面的硬件電路部分。
四、W25Q64簡介
注:上面有橫線的都是低電平有效
電路框圖:
- (1)和(2)描述的是存儲器規劃示意圖,(2)被劃分為若干個塊“Block”,其中每一塊再劃分為若干個扇區(1)“Sector”。對于每個扇區又可以分為很多頁? “Page” 。?
- 在(2)里面,以64KB為一個基本單元,分了128塊(因為一共8MB)。? ?
- 在(1)里面,是對于塊的更細的劃分,以4KB為一份,分了16份。
- 在寫入數據時,還會有個更細的劃分Page,256字節為一份。
- (7)為SPI控制邏輯,接收指令和數據等
- (8)狀態寄存器,表示芯片是否處于忙狀態、是否寫使能、是否寫保護
- (9)寫控制邏輯,配合WP引腳實現硬件寫保護
- (4)高電壓生成器,配合Flash編程,Flash要實現掉電不丟失,就需要高電壓來刺激。
- (5)頁地址鎖存/計數器、(6)字節地址鎖存/計數器,用來指定地址,讀寫操作
- (3)256字節的RAM存儲器,由于SPI寫入頻率非常高,所有寫入數據先到RAM緩存區里,再寫入Flash(2)里面。寫入時會給狀態寄存器(8)的BUSY位置1
五、Flash操作注意事項
寫入操作時:
- 寫入操作前,必須先進行寫使能
- 每個數據位只能由1改寫為0,不能由0改寫為1
- (由于上一條)寫入數據前必須先擦除,擦除后,所有數據位變為1
- 擦除必須按最小擦除單元進行(一個扇區)
- 連續寫入多字節時,最多寫入一頁的數據(256字節),超過頁尾位置的數據,會回到頁首覆蓋寫入
- 寫入操作結束后(寫到RAM),芯片進入忙狀態(當前在RAM轉Flash,或者在擦除當中),不響應新的讀寫操作
讀取操作時:
- 直接調用讀取時序,無需使能,無需額外操作,沒有頁的限制,讀取操作結束后不會進入忙狀態,但不能在忙狀態時讀取
六、代碼部分
MySPI.c
#include "stm32f10x.h" // Device header/*引腳配置層*//*** 函 數:SPI寫SS引腳電平* 參 數:BitValue 協議層傳入的當前需要寫入SS的電平,范圍0~1* 返 回 值:無* 注意事項:此函數需要用戶實現內容,當BitValue為0時,需要置SS為低電平,當BitValue為1時,需要置SS為高電平*/
void MySPI_W_SS(uint8_t BitValue)
{GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue); //根據BitValue,設置SS引腳的電平
}/*** 函 數:SPI寫SCK引腳電平* 參 數:BitValue 協議層傳入的當前需要寫入SCK的電平,范圍0~1* 返 回 值:無* 注意事項:此函數需要用戶實現內容,當BitValue為0時,需要置SCK為低電平,當BitValue為1時,需要置SCK為高電平*/
void MySPI_W_SCK(uint8_t BitValue)
{GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)BitValue); //根據BitValue,設置SCK引腳的電平
}/*** 函 數:SPI寫MOSI引腳電平* 參 數:BitValue 協議層傳入的當前需要寫入MOSI的電平,范圍0~1* 返 回 值:無* 注意事項:此函數需要用戶實現內容,當BitValue為0時,需要置MOSI為低電平,當BitValue為1時,需要置MOSI為高電平*/
void MySPI_W_MOSI(uint8_t BitValue)
{GPIO_WriteBit(GPIOA, GPIO_Pin_7, (BitAction)BitValue); //根據BitValue,設置MOSI引腳的電平,BitValue要實現非0即1的特性
}/*** 函 數:I2C讀MISO引腳電平* 參 數:無* 返 回 值:協議層需要得到的當前MISO的電平,范圍0~1* 注意事項:此函數需要用戶實現內容,當前MISO為低電平時,返回0,當前MISO為高電平時,返回1*/
uint8_t MySPI_R_MISO(void)
{return GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6); //讀取MISO電平并返回
}/*** 函 數:SPI初始化* 參 數:無* 返 回 值:無* 注意事項:此函數需要用戶實現內容,實現SS、SCK、MOSI和MISO引腳的初始化*/
void MySPI_Init(void)
{/*開啟時鐘*/RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //開啟GPIOA的時鐘/*GPIO初始化*/GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_7;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &GPIO_InitStructure); //將PA4、PA5和PA7引腳初始化為推挽輸出GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU;GPIO_InitStructure.GPIO_Pin = GPIO_Pin_6;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &GPIO_InitStructure); //將PA6引腳初始化為上拉輸入/*設置默認電平*/MySPI_W_SS(1); //SS默認高電平MySPI_W_SCK(0); //SCK默認低電平
}/*協議層*//*** 函 數:SPI起始* 參 數:無* 返 回 值:無*/
void MySPI_Start(void)
{MySPI_W_SS(0); //拉低SS,開始時序
}/*** 函 數:SPI終止* 參 數:無* 返 回 值:無*/
void MySPI_Stop(void)
{MySPI_W_SS(1); //拉高SS,終止時序
}/*** 函 數:SPI交換傳輸一個字節,使用SPI模式0* 參 數:ByteSend 要發送的一個字節* 返 回 值:接收的一個字節*/
uint8_t MySPI_SwapByte(uint8_t ByteSend)
{uint8_t i, ByteReceive = 0x00; //定義接收的數據,并賦初值0x00,此處必須賦初值0x00,后面會用到for (i = 0; i < 8; i ++) //循環8次,依次交換每一位數據{/*兩個!可以對數據進行兩次邏輯取反,作用是把非0值統一轉換為1,即:!!(0) = 0,!!(非0) = 1*/MySPI_W_MOSI(!!(ByteSend & (0x80 >> i))); //使用掩碼的方式取出ByteSend的指定一位數據并寫入到MOSI線MySPI_W_SCK(1); //拉高SCK,上升沿移出數據if (MySPI_R_MISO()){ByteReceive |= (0x80 >> i);} //讀取MISO數據,并存儲到Byte變量//當MISO為1時,置變量指定位為1,當MISO為0時,不做處理,指定位為默認的初值0MySPI_W_SCK(0); //拉低SCK,下降沿移入數據}return ByteReceive; //返回接收到的一個字節數據
}
W25Q64.c
#include "stm32f10x.h" // Device header
#include "MySPI.h"
#include "W25Q64_Ins.h"/*** 函 數:W25Q64初始化* 參 數:無* 返 回 值:無*/
void W25Q64_Init(void)
{MySPI_Init(); //先初始化底層的SPI
}/*** 函 數:W25Q64讀取ID號* 參 數:MID 工廠ID,使用輸出參數的形式返回* 參 數:DID 設備ID,使用輸出參數的形式返回* 返 回 值:無*/
void W25Q64_ReadID(uint8_t *MID, uint16_t *DID)
{MySPI_Start(); //SPI起始MySPI_SwapByte(W25Q64_JEDEC_ID); //交換發送讀取ID的指令*MID = MySPI_SwapByte(W25Q64_DUMMY_BYTE); //交換接收MID,通過輸出參數返回*DID = MySPI_SwapByte(W25Q64_DUMMY_BYTE); //交換接收DID高8位*DID <<= 8; //高8位移到高位*DID |= MySPI_SwapByte(W25Q64_DUMMY_BYTE); //或上交換接收DID的低8位,通過輸出參數返回MySPI_Stop(); //SPI終止
}/*** 函 數:W25Q64寫使能* 參 數:無* 返 回 值:無*/
void W25Q64_WriteEnable(void)
{MySPI_Start(); //SPI起始MySPI_SwapByte(W25Q64_WRITE_ENABLE); //交換發送寫使能的指令MySPI_Stop(); //SPI終止
}/*** 函 數:W25Q64等待忙* 參 數:無* 返 回 值:無*/
void W25Q64_WaitBusy(void)
{uint32_t Timeout;MySPI_Start(); //SPI起始MySPI_SwapByte(W25Q64_READ_STATUS_REGISTER_1); //交換發送讀狀態寄存器1的指令Timeout = 100000; //給定超時計數時間while ((MySPI_SwapByte(W25Q64_DUMMY_BYTE) & 0x01) == 0x01) //循環等待忙標志位{Timeout --; //等待時,計數值自減if (Timeout == 0) //自減到0后,等待超時{/*超時的錯誤處理代碼,可以添加到此處*/break; //跳出等待,不等了}}MySPI_Stop(); //SPI終止
}/*** 函 數:W25Q64頁編程* 參 數:Address 頁編程的起始地址,范圍:0x000000~0x7FFFFF* 參 數:DataArray 用于寫入數據的數組* 參 數:Count 要寫入數據的數量,范圍:0~256* 返 回 值:無* 注意事項:寫入的地址范圍不能跨頁*/
void W25Q64_PageProgram(uint32_t Address, uint8_t *DataArray, uint16_t Count)
{uint16_t i;W25Q64_WriteEnable(); //寫使能MySPI_Start(); //SPI起始MySPI_SwapByte(W25Q64_PAGE_PROGRAM); //交換發送頁編程的指令MySPI_SwapByte(Address >> 16); //交換發送地址23~16位MySPI_SwapByte(Address >> 8); //交換發送地址15~8位MySPI_SwapByte(Address); //交換發送地址7~0位for (i = 0; i < Count; i ++) //循環Count次{MySPI_SwapByte(DataArray[i]); //依次在起始地址后寫入數據}MySPI_Stop(); //SPI終止W25Q64_WaitBusy(); //等待忙
}/*** 函 數:W25Q64扇區擦除(4KB)* 參 數:Address 指定扇區的地址,范圍:0x000000~0x7FFFFF* 返 回 值:無*/
void W25Q64_SectorErase(uint32_t Address)
{W25Q64_WriteEnable(); //寫使能MySPI_Start(); //SPI起始MySPI_SwapByte(W25Q64_SECTOR_ERASE_4KB); //交換發送扇區擦除的指令MySPI_SwapByte(Address >> 16); //交換發送地址23~16位MySPI_SwapByte(Address >> 8); //交換發送地址15~8位MySPI_SwapByte(Address); //交換發送地址7~0位MySPI_Stop(); //SPI終止W25Q64_WaitBusy(); //等待忙
}/*** 函 數:W25Q64讀取數據* 參 數:Address 讀取數據的起始地址,范圍:0x000000~0x7FFFFF* 參 數:DataArray 用于接收讀取數據的數組,通過輸出參數返回* 參 數:Count 要讀取數據的數量,范圍:0~0x800000* 返 回 值:無*/
void W25Q64_ReadData(uint32_t Address, uint8_t *DataArray, uint32_t Count)
{uint32_t i;MySPI_Start(); //SPI起始MySPI_SwapByte(W25Q64_READ_DATA); //交換發送讀取數據的指令MySPI_SwapByte(Address >> 16); //交換發送地址23~16位MySPI_SwapByte(Address >> 8); //交換發送地址15~8位MySPI_SwapByte(Address); //交換發送地址7~0位for (i = 0; i < Count; i ++) //循環Count次{DataArray[i] = MySPI_SwapByte(W25Q64_DUMMY_BYTE); //依次在起始地址后讀取數據}MySPI_Stop(); //SPI終止
}