文章目錄
- 一、SPI 協議簡介
- 二、硬件電路
- 2.1.SPI的連接
- 2.2.數據的移位
- 2.3.時序基本單元
- 2.3.1.起始條件和終止條件
- 2.3.2.模式 0
- 2.3.3.模式 1
- 2.3.4.模式 2
- 2.3.5.模式 3
- 2.4.時序
- 三、軟件實現
- 四、W25Q64
- 4.1.簡介
- 4.2.硬件電路
- 4.3.框圖
- 4.4.操作注意事項
- 五、實驗
一、SPI 協議簡介
SPI(Serial Peripheral Interface)是由Motorola公司開發的一種通用數據總線,四根通信線:
- SCK(Serial Clock)
- MOSI(Master Output Slave Input)
- MISO(Master Input Slave Output)
- SS(Slave Select)
具有時鐘線 SCK 是同步通信,發送和接收都分別有單獨的一根線 MOSI 和 MISO 是全雙工模式,支持總線掛載多設備(一主多從)。
二、硬件電路
2.1.SPI的連接
下圖是 SPI 協議下的一主多從模式,一個主機與多個從機相互通信,就需要多根 SS 與從機相連,不需要像 IIC 和 CAN 總線協議一樣,IIC 指定從機地址來和指定設備通信,CAN 的仲裁段讓總線上的設備來競選誰先發數據,SPI 就像一位大少爺,想和誰通信,就拉一根線與該設備相連接,這樣的方法優點是指定從機方式變簡單了,缺點是占用的 IO 口增多。
SS 線可以配置為推挽輸出,由主機決定是否要和指定從機通信;MISO 線配置為浮空或者上拉輸入,等待從機發送信息,主機接收;MOSI 線配置為復用推挽輸出,主機發信息給從機;SCK 線配置為推挽輸出,由主機提供時鐘。
2.2.數據的移位
下面是 SPI 數據傳輸的具體移位圖,高位先行,例如:當時鐘的上升沿到來時,主機和從機的移位寄存器就把一個數據移出寄存器,時鐘的下降沿到來時,就移入對方的移位寄存器中,有 8 個數據,就要循環 8 次的時鐘。雙方的時鐘源由主機的波特發生率來提供。
如果主機只想發送,主機發送了信息,就一定會收到從機交換來的信息,主機選擇不理睬就可以了;如果主機只想接收從機發來的信息,主機需要發送一條無關緊要的數據交換從機發來需要的信息。
2.3.時序基本單元
2.3.1.起始條件和終止條件
數據的傳輸都要在被選中設備的 SS 線由高電平變為低電平開始,SS 線為低電平期間內傳輸,SS 線由低電平變為高電平數據傳輸結束。
2.3.2.模式 0
SPI 數據傳輸有四個模式,這四個模式由兩個變量組成:CPOL (Clock Polarity) 時鐘極性;CPHA (Clock Phase) 時鐘相位。CPOL=0:空閑狀態時,SCK為低電平;CPHA=0:SCK第一個邊沿移入數據,第二個邊沿移出數據。時鐘空閑時為低電平,時鐘到來的第一個邊沿是上升沿,需要移入數據到數據寄存器,那雙方的數據在第一個時鐘上升沿到來之前就要將數據移出傳輸線上面,等待第二個邊沿移入,也就是下降沿。
2.3.3.模式 1
CPOL=0:空閑狀態時,SCK為低電平;CPHA=1:SCK第一個邊沿移出數據,第二個邊沿移入數據。時鐘空閑時為低電平,第一個邊沿為上升沿,需要移出數據到傳輸線上,第二個邊沿是下降沿,下降沿到來時就將數據移入對方的數據寄存器中。
2.3.4.模式 2
CPOL=1:空閑狀態時,SCK為高電平;CPHA=0:SCK第一個邊沿移入數據,第二個邊沿移出數據。時鐘空閑時為高電平,第一個邊沿到來之前需要將數據移出到傳輸線上,等待第一個邊沿到來移入數據,第一個邊沿時下降沿,下降沿移入數據,第二個邊沿是上升沿,這時候移入數據。
2.3.5.模式 3
CPOL=1:空閑狀態時,SCK為高電平;CPHA=1:SCK第一個邊沿移出數據,第二個邊沿移入數據。時鐘空閑時為高電平,第一個邊沿為下升沿,需要移出數據到傳輸線上,第二個邊沿是上降沿,上降沿到來時就將數據移入對方的數據寄存器中。
2.4.時序
- 指定地址寫:向SS指定的設備,發送寫指令(0x02),隨后在指定地址(Address[23:0])下,寫入指定數據(Data):
- 指定地址讀:向SS指定的設備,發送讀指令(0x03),隨后在指定地址(Address[23:0])下,讀取從機數據(Data):
三、軟件實現
初始化 SPI,PA4 為 SS,PA5 為時鐘輸出,PA7 為主機接收,PA6 為主機輸出:
#include "stm32f10x.h" // Device header/*** 函 數:SPI寫SS引腳電平,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初始化* 參 數:無* 返 回 值:無*/
void MySPI_Init(void)
{/*開啟時鐘*/RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //開啟GPIOA的時鐘RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE); //開啟SPI1的時鐘/*GPIO初始化*/GPIO_InitTypeDef GPIO_InitStructure;GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP;GPIO_InitStructure.GPIO_Pin = GPIO_Pin_4;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &GPIO_InitStructure); //將PA4引腳初始化為推挽輸出GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;GPIO_InitStructure.GPIO_Pin = GPIO_Pin_5 | GPIO_Pin_7;GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;GPIO_Init(GPIOA, &GPIO_InitStructure); //將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引腳初始化為上拉輸入/*SPI初始化*/SPI_InitTypeDef SPI_InitStructure; //定義結構體變量SPI_InitStructure.SPI_Mode = SPI_Mode_Master; //模式,選擇為SPI主模式SPI_InitStructure.SPI_Direction = SPI_Direction_2Lines_FullDuplex; //方向,選擇2線全雙工SPI_InitStructure.SPI_DataSize = SPI_DataSize_8b; //數據寬度,選擇為8位SPI_InitStructure.SPI_FirstBit = SPI_FirstBit_MSB; //先行位,選擇高位先行SPI_InitStructure.SPI_BaudRatePrescaler = SPI_BaudRatePrescaler_128; //波特率分頻,選擇128分頻SPI_InitStructure.SPI_CPOL = SPI_CPOL_Low; //SPI極性,選擇低極性SPI_InitStructure.SPI_CPHA = SPI_CPHA_1Edge; //SPI相位,選擇第一個時鐘邊沿采樣,極性和相位決定選擇SPI模式0SPI_InitStructure.SPI_NSS = SPI_NSS_Soft; //NSS,選擇由軟件控制SPI_InitStructure.SPI_CRCPolynomial = 7; //CRC多項式,暫時用不到,給默認值7SPI_Init(SPI1, &SPI_InitStructure); //將結構體變量交給SPI_Init,配置SPI1/*SPI使能*/SPI_Cmd(SPI1, ENABLE); //使能SPI1,開始運行/*設置默認電平*/MySPI_W_SS(1); //SS默認高電平
}/*** 函 數: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)
{while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_TXE) != SET); //等待發送數據寄存器空SPI_I2S_SendData(SPI1, ByteSend); //寫入數據到發送數據寄存器,開始產生時序while (SPI_I2S_GetFlagStatus(SPI1, SPI_I2S_FLAG_RXNE) != SET); //等待接收數據寄存器非空return SPI_I2S_ReceiveData(SPI1); //讀取接收到的數據并返回
}
四、W25Q64
4.1.簡介
W25Qxx 系列是一種低成本、小型化、使用簡單的非易失性存儲器,常應用于數據存儲、字庫存儲、固件程序存儲等場景。存儲介質:Nor Flash(閃存),時鐘頻率:80MHz / 160MHz (Dual SPI) / 320MHz (Quad SPI)。存儲容量:
4.2.硬件電路
該存儲芯片一共有 8 個引腳,除了 SPI 必要的四根線,還有自己的電源線、地線,還有寫保護和鎖定數據,方便被中斷打斷之后,還能繼續發送。
由下面硬件電路圖可知,CS 也是 SS,低電平有效,HOLD 數據鎖定和 WP 寫保護也是低電平有效。
4.3.框圖
W25Q64 的內存分布如下圖所示,該芯片一共有 64M Bite / 8M Byte 大小,存儲器以字節為單位,每一字節都有唯一地址。地址由左下角的 0 字節開始自增到 7FFFFF,W25Q64 的地址寬度是 24 位,3 個字節,因此 24 位地址最大的尋址范圍是 16MB。每 64KB 分為一個塊區,8MB 可以分為 128 個塊區,每個塊區又可以細分,每 4KB 分為一個扇區,可以分為 16 份。每 4KB 的扇區又可以細分為頁,每一頁有 256 個字節,4KB * 1024 = 4096 Byte, 4096 Byte / 256 = 16頁,每一個扇區又可以分為 16 頁:
4.4.操作注意事項
寫入操作時:
- 寫入操作前,必須先進行寫使能
- 每個數據位只能由 1 改寫為 0,不能由 0 改寫為 1
- 寫入數據前必須先擦除,擦除后,所有數據位變為 1
- 擦除必須按最小擦除單元進行(頁)
- 連續寫入多字節時,最多寫入一頁的數據,超過頁尾位置的數據,會回到頁首覆蓋寫入
- 寫入操作結束后,芯片進入忙狀態,不響應新的讀寫操作
讀取操作時:直接調用讀取時序,無需使能,無需額外操作,沒有頁的限制,讀取操作結束后不會進入忙狀態,但不能在忙狀態時讀取。
五、實驗
使用 SPI 協議將 STM32 最小系統板和 W25Q64 進行信息交流,將寫入的數據讀出來,下面是對 W25Q64 的代碼封裝和定義:
#ifndef __W25Q64_INS_H
#define __W25Q64_INS_H#define W25Q64_WRITE_ENABLE 0x06
#define W25Q64_WRITE_DISABLE 0x04
#define W25Q64_READ_STATUS_REGISTER_1 0x05
#define W25Q64_READ_STATUS_REGISTER_2 0x35
#define W25Q64_WRITE_STATUS_REGISTER 0x01
#define W25Q64_PAGE_PROGRAM 0x02
#define W25Q64_QUAD_PAGE_PROGRAM 0x32
#define W25Q64_BLOCK_ERASE_64KB 0xD8
#define W25Q64_BLOCK_ERASE_32KB 0x52
#define W25Q64_SECTOR_ERASE_4KB 0x20
#define W25Q64_CHIP_ERASE 0xC7
#define W25Q64_ERASE_SUSPEND 0x75
#define W25Q64_ERASE_RESUME 0x7A
#define W25Q64_POWER_DOWN 0xB9
#define W25Q64_HIGH_PERFORMANCE_MODE 0xA3
#define W25Q64_CONTINUOUS_READ_MODE_RESET 0xFF
#define W25Q64_RELEASE_POWER_DOWN_HPM_DEVICE_ID 0xAB
#define W25Q64_MANUFACTURER_DEVICE_ID 0x90
#define W25Q64_READ_UNIQUE_ID 0x4B
#define W25Q64_JEDEC_ID 0x9F
#define W25Q64_READ_DATA 0x03
#define W25Q64_FAST_READ 0x0B
#define W25Q64_FAST_READ_DUAL_OUTPUT 0x3B
#define W25Q64_FAST_READ_DUAL_IO 0xBB
#define W25Q64_FAST_READ_QUAD_OUTPUT 0x6B
#define W25Q64_FAST_READ_QUAD_IO 0xEB
#define W25Q64_OCTAL_WORD_READ_QUAD_IO 0xE3#define W25Q64_DUMMY_BYTE 0xFF#endif
#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終止
}
下面是主程序 main.c 代碼實現:
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "W25Q64.h"uint8_t MID; //定義用于存放MID號的變量
uint16_t DID; //定義用于存放DID號的變量uint8_t ArrayWrite[] = {0x01, 0x02, 0x03, 0x04}; //定義要寫入數據的測試數組
uint8_t ArrayRead[4]; //定義要讀取數據的測試數組int main(void)
{/*模塊初始化*/OLED_Init(); //OLED初始化W25Q64_Init(); //W25Q64初始化/*顯示靜態字符串*/OLED_ShowString(1, 1, "MID: DID:");OLED_ShowString(2, 1, "W:");OLED_ShowString(3, 1, "R:");/*顯示ID號*/W25Q64_ReadID(&MID, &DID); //獲取W25Q64的ID號OLED_ShowHexNum(1, 5, MID, 2); //顯示MIDOLED_ShowHexNum(1, 12, DID, 4); //顯示DID/*W25Q64功能函數測試*/W25Q64_SectorErase(0x000000); //扇區擦除W25Q64_PageProgram(0x000000, ArrayWrite, 4); //將寫入數據的測試數組寫入到W25Q64中W25Q64_ReadData(0x000000, ArrayRead, 4); //讀取剛寫入的測試數據到讀取數據的測試數組中/*顯示數據*/OLED_ShowHexNum(2, 3, ArrayWrite[0], 2); //顯示寫入數據的測試數組OLED_ShowHexNum(2, 6, ArrayWrite[1], 2);OLED_ShowHexNum(2, 9, ArrayWrite[2], 2);OLED_ShowHexNum(2, 12, ArrayWrite[3], 2);OLED_ShowHexNum(3, 3, ArrayRead[0], 2); //顯示讀取數據的測試數組OLED_ShowHexNum(3, 6, ArrayRead[1], 2);OLED_ShowHexNum(3, 9, ArrayRead[2], 2);OLED_ShowHexNum(3, 12, ArrayRead[3], 2);while (1){}
}