一、SPI總線概述
1、SPI總線介紹
SPI是一種通信協議,它是摩托羅拉公司研發出來的一種通信協議,就有自己的特點(串行,并行,單工,半雙工,全雙工,同步異步)。它主要應用于音視頻的開發. SPI是串行外設接口(Serial Peripheral Interface)的縮寫,是一種高速的,全雙工,同步的通信總線,并且在芯片的管腳上只占用四根線,節約了芯片的管腳,同時為PCB的布局上節省空間,提供方便,正是出于這種簡單易用的特性,越來越多的芯片集成了這種通信協議.
一般的通信速度可以達到幾十Mhz
SPI(Serial Peripheral Interface)是一種串行外設接口標準,用于在微控制器或其他數字設備之間進行通信。它被廣泛應用于各種應用領域,如通信、嵌入式系統和傳感器網絡等。
?2、SPI總線接口與物理拓撲結構
SPI按照接線的不同分為3線SPI與4線SPI。
同步串行全雙工
同步串行半雙工
物理上的連接是怎么樣的?
使用SPI協議重點是知道里面的一個時序的情況,如果知道了這種情況,完全可以實現這種協議。
SPI協議是一種一主多從的形式:
可以通過片選線來選中不同的從機
時鐘線(信號)只能由主機控制,從機受到主機發出的時鐘信號來做出相應的處理
針對主機而言:
當時鐘線產生上升沿時接收數據,當時鐘線產生下降沿時發送數據
當時鐘線產生下降沿時接收數據,當時鐘線產生上升沿時發送數據
3、SPI總線通信原理
SPI可以配置為4種模式(SPI控制器)
MODE0-MODE3
4、SPI總線數據格式
8位的數據,高位先發
SPI的通信過程其實就是數據交換的過程
首先需要拉低片選線
再根據當前的邊沿決定是發送數據還是接收數據(上升沿發送,下降沿接收)
結束本次通信拉高片選線
用IO口來實現SPI協議?
寫偽代碼(不是真正可以實現的,不過可以實現相應的邏輯)
當前選擇模式0:CPHA = 0, CPOL = 0,下降沿發送數據,上升沿接收數據
SCK, MOSI, MISO,CS
U8 Data = 0;//0000 0000
CS = 0;//選中這個芯片
SCK = 1;
for(i =0; i <8 ;i++)
{SCK = 0;MOSI = 0/1;SCK = 1;Data <<= 1;//空出低位的操作,data = 0000 0100if(MISO){Data |= 1;//0000 0001}
}
二、SPI控制器
1、SPI的控制器特征
基于三條線的全雙工同步傳輸(通過一個標志位的切換達到所謂的雙工通信)
??? ● 基于雙線的單工同步傳輸,其中一條可作為雙向數據線
??? ● 8 位或 16 位傳輸幀格式選擇
??? ● 主模式或從模式操作
??? ● 多主模式功能
??? ● 8 個主模式波特率預分頻器(最大值為 fPCLK/2)fpclk就是看當前用的SPI是在哪根總線上
??? ● 從模式頻率(最大值為 fPCLK/2)
??? ● 對于主模式和從模式都可實現更快的通信
??? ● 對于主模式和從模式都可通過硬件或軟件進行 NSS(CS管腳) 管理:動態切換主 /從操作
??? ● 可編程的時鐘極性和相位
??? ● 可編程的數據順序,最先移位 MSB 或 LSB
?????? ● 可觸發中斷的專用發送和接收標志
??? ● SPI 總線忙狀態標志
??? ● SPI TI 模式
??? ● 用于確保可靠通信的硬件 CRC 功能:
— 在發送模式下可將 CRC 值作為最后一個字節發送
— 根據收到的最后一個字節自動進行 CRC 錯誤校驗
● 可觸發中斷的主模式故障、上溢和 CRC 錯誤標志
● 具有 DMA 功能的 1 字節發送和接收緩沖器:發送和接收請求
2、SPI控制器框架分析
SPI接口:STM32的SPI控制器有多個SPI接口,每個SPI接口可以連接多個SPI從設備。每個SPI接口都包含用于發送和接收數據的寄存器。SPI接口提供了多種配置選項,如時鐘極性、時鐘相位、數據位數等。
主設備和從設備:在SPI通信中,系統中的一個設備充當主設備,它發起和控制數據傳輸。其他設備則充當從設備,它們響應主設備的命令并提供數據。主設備通過選擇從設備來確定與之通信的目標。
緩沖器和寄存器:SPI控制器使用緩沖器和寄存器來存儲發送和接收的數據。主設備通過將數據寫入發送緩沖器來發送數據,從設備通過讀取接收緩沖器來接收數據。SPI控制器還包含用于控制和配置的寄存器。
時鐘設置:SPI控制器使用時鐘信號來同步數據傳輸。時鐘信號由主設備控制,可以通過配置時鐘極性和相位來設置時鐘信號的工作方式。
SPI控制器的工作流程如下:
- 主設備選擇從設備并開始傳輸。
- 主設備將數據寫入發送緩沖器。
- 主設備發送時鐘信號,從設備接收數據。
- 從設備將接收到的數據存儲到接收緩沖器。
- 主設備繼續發送數據并重復上述過程,直到傳輸完成。
3、相關寄存器
SPI 控制寄存器 1 (SPI_CR1) (不用于 I 2 S 模式)![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
![]()
SPI 數據寄存器 (SPI_DR)![]()
SPI 狀態寄存器 (SPI_SR)![]()
SR寄存器用于提示當前能不能發生或者接收下一個字節的數據
等到發送緩沖區為空就可以繼續發下一個字節的數據
等到接收緩沖區為空就可以繼續接收下一個字節的數據
三、W25Q64芯片
1、芯片介紹
W25Q64 (64M-bit) , W25Q16(16M-bit) 和 W25Q32(32M-bit) 是為系統提供一個最小的空間、引腳和功耗的存儲器解決方案的串行 Flash 存儲器。 25Q 系列比普通的串行 Flash 存儲器更靈活,性能更優越。基于雙倍/四倍的 SPI,它們能夠可以立即完成提供數據給 RAM, 包括存儲聲音、文本和數據。芯片支持的工作電壓 2.7V 到 3.6V,正常工作時電流小于 5mA,掉電時低于 1uA。所有芯片提供標準的封裝。W25Q64/16/32 由每頁 256 字節組成。 每頁的 256 字節用一次頁編程指令即可完成。每次可以擦除 16 頁(1個扇區)、 128 頁(32KB 塊)、 256 頁( 64KB 塊)和全片擦除。
256字節 = 1頁
16頁 = 1個扇區
16個扇區 =? 1個塊
W25Q64 的內存空間結構: 一頁 256 字節, 4K(4096 字節) 為一個扇區, 16 個扇區為 1 塊, 容量為 8M 字節, 共有 128 個塊, 2048 個扇區。
FLASH: 可讀可寫,只能寫入0不能寫入1,只有把1變0的能力
擦除的概念:寫數據之前要先擦除,擦除就是全部數據變為1
1111 1111
1010 1010
當前W25Q64這個芯片最少擦除4096字節(就是一個扇區),然后去使用
具有唯一的 64 位識別序列號。
● SPI 串行存儲器系列 ???????????????????????????????????????? ●靈活的 4KB 扇區結構
-W25Q64:64M 位/8M 字節 ????????????????????????????????????-統一的扇區擦除( 4K 字節)-W25Q16:16M 位/2M 字節 ????????????????????????????????????-塊擦除( 32K 和 64K 字節)
-W25Q32:32M 位/4M 字節 ????????????????????????????????????-一次編程 256 字節-每 256 字節可編程頁 ???????????????????????????????????????????-至少 100,000 寫/擦除周期
-數據保存 20 年
2、芯片管腳說明
CS -- 片選
DO -- 數據輸出(MISO)
DI -- 數據輸入(MOSI)
Clk -- 時鐘線
3、芯片工作原理
W25Q64/16/32兼容的SPI 總線包含四個信號:串行時鐘( CLK)、片選端( /CS)、串行數據輸入( DI)和串行數據輸出( DO)。標準的 SPI 用 DI 輸入引腳在 CLK 的上升沿連續的寫命令、地址或數據到芯片內。 DO 輸出在 CLK 的下降沿從芯片內讀出數據或狀態。
?? ?支持 SPI 總線的工作模式 0( 0, 0)和 3( 1, 1)。模式 0 和模式 3 的主要區別在于常態時的 CLK
信號,當 SPI 主機已準備好數據還沒傳輸到串行 Flash 中,對于模式 0 CLK 信號常態為低.串行時鐘輸入引腳為串行輸入和輸出操作提供時序。(見 SPI 操作)設備數據傳輸是從高位開始, 數據傳輸的格式為8bit, 數據采樣從第二個時間邊沿開始, 空閑狀態時, 時鐘線 clk 為高電平。
當前這個器件可用的SPI時序符合前面所講到的模式0和模式3
模式0/3:上升沿接收數據,下降沿發送數據
模式1/2:上升沿發送數據,下降沿接收數據
支持模式0的一般就會支持模式3,支持模式1的一般就會支持模式2
4、芯片操作時序
讀ID的時序
拉低片選線(CS)
DI線就是代表代碼里需要發送的數據
DO線就代表代碼里接收到的數據
讀取制造商/設備 ID 指令可以讀取,制造商 ID 和特定的設備 ID。 讀取之間, 拉低 CS 片選信號, 接著發送指令代碼“90h” , 緊隨其后的是一個 24 位地址(A23-A0)000000h。之后, 設備發出華邦子制造商 ID(EFh) 和設備ID(w25q64 為 16h)。如果 24 位地址設置為 000001 h 的設備 ID 會先發出,然后跟著制造商 ID。制造商和設備 ID 可以連續讀取。完成指令后, 片選信號/ CS 拉高。
寫使能
拉低片選
接著發0x06的寫使能指令
拉高片選
讀狀態寄存器
先拉低片選
接著發05這條指令代表存儲芯片當前需要去讀狀態寄存器1
接下來就可以讀回來狀態寄存器的8個位了
拉高片選
扇區擦除
扇區擦除指令可以擦除指定一個扇區(4 k 字節)內所有數據, 將內存空間恢復到 0xFF 狀態。 寫入扇區擦除指令之前必須執行設備寫使能(發送設備寫使能指令 0x06), 并判斷狀態寄存器(狀態寄存器位最低位必須等于 0 才能操作)。發送的扇區擦除指令前, 先拉低/ CS, 接著發送扇區擦除指令碼”20 h”, 和 24 位地址(A23-A0), 地址發送完畢后,拉高片選線 CS/,, 并判斷狀態位,等待擦除結束。 擦除一個扇區的最少需要 150ms 時間。
寫使能
判斷狀態寄存器的第0位
先拉低片選
發送0x20的指令
發送24位的地址(當前你要擦除的地址是哪里--首地址),這個地址只能是4096的倍數
拉高片選線
頁編程指令
頁編程指令允許從一個字節到 256 字節的數據編程(一頁)(編程之前必須保證內存空間是 0XFF--要先擦除)。允許寫入指令之前,必須先發送設備寫使能指令。 寫使能開啟后, 設備才能接收編程指令。 開啟頁編程先拉底/ CS, 然后發送指令代碼“02 h”, 接著發送一個 24 位地址(開始存放數據的地址)(A23-A0)(發送 3 次,每次 8 位) 和至少一個數據字節(數據字節不能超過 256字節)。 數據字節發送完畢, 需要拉高片選線 CS/,并判斷狀態位, 等待寫入結束。進行頁編程時,如果數據字節數超過了 256 字節, 地址將自動回到頁的起始地址, 覆蓋掉之前的數據。 在某些情況下,數據字節小于 256 字節(同一頁內), 也可以正常對其他字節存放, 不會有任何影響。如果存放超過 256 字節的數據, 需要分次編程存放。
寫使能
拉低片選
發送頁編程指令(0x02)
發送24位的地址
發送需要存下來的數據(不能超過當前頁的最大地址,如果超過則會回到當前頁的起始地址開始存放)
拉高片選
判斷當前寫入完成沒有(讀狀態寄存器1的第0位)
讀數據
讀取數據指令允許按順序讀取一個字節的內存數據。當片選 CS/拉低之后, 緊隨其后是一個 24 位的地址(A23-A0)(需要發送 3 次, 每次 8 個字節, 先發高位)。芯片收到地址后,將要讀的數據按字節大小轉移出去, 數據是先轉移高位,對于單片機,時鐘下降沿發送數據,上升沿接收數據。 讀數據時, 地址會自動增加, 允許連續的讀取數據。這意味著讀取整個內存的數據,只要用一個指令就可以讀完。 數據讀取完成之后, 片選信號/ CS 拉高。讀取數據的指令序列,如上圖所示。 如果一個讀數據指令而發出的時候, 設備正在擦除扇區,或者(忙= 1), 該讀指令將該讀指令將被忽略,也不會對當前周期有什么影響。
拉低片選
先發一個0x03的指令
發送要從哪個地址開始讀取數據(24位的地址)
再去讀取數據
拉高片選
四、模擬SPI
#include "spi.h"/************************************
函數功能:初始化SPI的管腳
函數形參:void
函數返回值:void
函數說明:CS -- PB14: 推挽輸出(可以選中也可以釋放--有低電平也有高電平)
MISO -- PA6: 浮空輸入
MOSI -- PA7: 推挽輸出
SCK -- PA5: 推挽輸出
作者:
日期:
************************************/
void Spi_Port_Init(void)
{//打開GPIOA和GPIOB的時鐘RCC->AHB1ENR |= 0x3 << 0;//配置輸出//配置的是PA5--SCKGPIOA->MODER &= ~(0X3 << 10);GPIOA->MODER |= (0X1 << 10);//配置的是PA7--MOSIGPIOA->MODER &= ~(0X3 << 14);GPIOA->MODER |= (0X1 << 14);//配置的是PB14--CSGPIOB->MODER &= ~(0X3 << 28);GPIOB->MODER |= (0X1 << 28);//配置輸入GPIOA->MODER &= ~(0X3 << 12);//把速度配置為50MhzGPIOA->OSPEEDR &= ~(0X3F << 10);GPIOA->OSPEEDR |= (0X2A << 10);
}/************************************
函數功能:模擬SPI來讀寫數據
函數形參:u8 data -- 需要發送的數據
函數返回值:u8 -- 接收回來的數據可以通過返回值給調用者
函數說明:data = 1011 0011
高位先發
下降沿發送數據,上升沿接收數據
作者:
日期:
************************************/
u8 Spi_Change_Data(u8 data)
{u8 rec_data = 0;u8 i = 0;for(i = 0; i < 8; i++){SCK_L;//代表產生下降沿可以開始發送數據//1011 0011 & //0100 0000//spi不需要加延時if(data & (0x80 >> i)){MOSI_H;}else{MOSI_L;}SCK_H;//代表產生上升沿可以開始接收數據rec_data <<= 1;//可以空出最低位if(MISO){rec_data |= 1;}}return rec_data;
}
#ifndef __SPI_H_
#define __SPI_H_#include "stm32f4xx.h"
#include "io_bit.h"#define SCK_H (GPIOA->ODR |= 0X1 << 5)
#define SCK_L (GPIOA->ODR &= ~(0X1 << 5))
#define MOSI_H (GPIOA->ODR |= 0X1 << 7)
#define MOSI_L (GPIOA->ODR &= ~(0X1 << 7))
#define CS_H (GPIOB->ODR |= 0X1 << 14)
#define CS_L (GPIOB->ODR &= ~(0X1 << 14))
#define MISO PAin(6)void Spi_Port_Init(void);
u8 Spi_Change_Data(u8 data);
#endif
五、SPI控制器
1、硬件設計
CS: 推挽輸出(可以選中也可以釋放--有低電平也有高電平)
MISO: 浮空輸入
MOSI: 推挽輸出
SCK : 推挽輸出
2、軟件設計
GPIO口配置
SPI配置
#include "spi.h"/************************************
函數功能:初始化SPI的管腳
函數形參:void
函數返回值:void
函數說明:CS -- PB14: 推挽輸出(可以選中也可以釋放--有低電平也有高電平)
MISO -- PA6: 浮空輸入
MOSI -- PA7: 推挽輸出
SCK -- PA5: 推挽輸出
作者:li
日期:2021.8.6
************************************/
void Spi_Port_Init(void)
{//打開GPIOA和GPIOB的時鐘RCC->AHB1ENR |= 0x3 << 0;//PA5-7配置為復用功能GPIOA->MODER &= ~(0X3F << 10);GPIOA->MODER |= (0X2A << 10);//復用到SPI1GPIOA->AFR[0] &= ~(0XFFF << 20);GPIOA->AFR[0] |= (0X555 << 20);//把速度配置為50MhzGPIOA->OSPEEDR &= ~(0X3F << 10);GPIOA->OSPEEDR |= (0X2A << 10);//配置的是PB14--CSGPIOB->MODER &= ~(0X3 << 28);GPIOB->MODER |= (0X1 << 28);//打開SPI1的時鐘RCC->APB2ENR |= 0X1 << 12;SPI1->CR1 = 0;/*選擇雙線單向通信數據模式為發送 /接收選擇 8 位數據幀格式全雙工(發送和接收)先發送 MSBfPCLK/2下降沿發送數據,上升沿接收數據*///使能軟件從器件管理SPI1->CR1 |= 0X3 << 8;//主模式SPI1->CR1 |= 0X1 << 2;//使能外設SPI1->CR1 |= 0X1 << 6;
}/************************************
函數功能:
函數形參:u8 data -- 需要發送的數據
函數返回值:u8 -- 接收回來的數據可以通過返回值給調用者
函數說明:data = 1011 0011
高位先發
下降沿發送數據,上升沿接收數據
作者:
日期:
************************************/
u8 Spi_Change_Data(u8 data)
{u8 rec_data = 0;while(!(SPI1->SR & 0X1 << 1))//當前發送緩沖區為空才能跳出循環{}SPI1->DR = data;//發送數據while(!(SPI1->SR & 0X1 << 0))//當前接收緩沖區為空才能跳出循環{}rec_data = SPI1->DR;//接收數據return rec_data;
}
#ifndef __SPI_H_
#define __SPI_H_#include "stm32f4xx.h"
#include "io_bit.h"#define SCK_H (GPIOA->ODR |= 0X1 << 5)
#define SCK_L (GPIOA->ODR &= ~(0X1 << 5))
#define MOSI_H (GPIOA->ODR |= 0X1 << 7)
#define MOSI_L (GPIOA->ODR &= ~(0X1 << 7))
#define CS_H (GPIOB->ODR |= 0X1 << 14)
#define CS_L (GPIOB->ODR &= ~(0X1 << 14))
#define MISO PAin(6)void Spi_Port_Init(void);
u8 Spi_Change_Data(u8 data);
#endif
#include "w25q64.h"/************************************
函數功能:讀廠商ID
函數形參:void
函數返回值:void
函數說明:發送的指令是0x90
正常接收到的是0xef16
作者:
日期:
************************************/
void Read_ID(void)
{u16 w25q64_id = 0;CS_L;//拉低CS的片選信號也就是選中這一個芯片Spi_Change_Data(0x90);//發送讀ID的指令//發送24位的0Spi_Change_Data(0);Spi_Change_Data(0);Spi_Change_Data(0);w25q64_id = Spi_Change_Data(0);//得到廠商ID為EFh,此時發什么不重要w25q64_id <<= 8;//把接收到的數據存放到16位數據中的高8位w25q64_id |= Spi_Change_Data(0xff);CS_H;//拉高片選,表示這一次通信結束printf("%#x\r\n",w25q64_id);
}/************************************
函數功能:寫使能
函數形參:void
函數返回值:void
函數說明:發送的指令是0x06
作者:
日期:
************************************/
void Write_Enable(void)
{CS_L;Spi_Change_Data(0X06);CS_H;
}/************************************
函數功能:讀狀態寄存器1
函數形參:void
函數返回值:u8
函數說明:發送的指令是0x05
作者:
日期:
************************************/
u8 Read_Register1(void)
{u8 reg = 0;CS_L;Spi_Change_Data(0X05);reg = Spi_Change_Data(0);CS_H;return reg;
}/************************************
函數功能:扇區擦除
函數形參:u32 addr
函數返回值:void
函數說明:發送的指令是0x20
地址只能是4096的倍數
00000000 00000000 00010000 00000000
作者:
日期:
************************************/
void Sector_Erase(u32 addr)
{u8 *p = (u8*)&addr;//現在是得到32位里的最低8位u8 ret;Write_Enable();//直到不忙才可以往下執行do{ret = Read_Register1();}while(ret & 0x1 << 0);CS_L;Spi_Change_Data(0x20);//發送這24位的地址數據Spi_Change_Data(p[2]);Spi_Change_Data(p[1]);Spi_Change_Data(p[0]);CS_H;//直到不忙才可以往下執行do{ret = Read_Register1();}while(ret & 0x1 << 0);
}/************************************
函數功能:頁編程
函數形參:u32 addr--從這一頁的哪個位置開始寫入數據
u8 *str, -- 需要寫入的數據
u8 len --數據的長度
函數返回值:void
函數說明:發送的指令是0x02
當前擦除了哪一個扇區就可以在當前扇區的某一頁寫入數據
最大寫入256字節
作者:
日期:
************************************/
void Page_Program(u32 addr,u8 *str,u8 len)
{u8 *p = (u8*)&addr;//現在是得到32位里的最低8位u8 ret;Write_Enable();//直到不忙才可以往下執行do{ret = Read_Register1();}while(ret & 0x1 << 0);CS_L;Spi_Change_Data(0x02);//發送這24位的地址數據Spi_Change_Data(p[2]);Spi_Change_Data(p[1]);Spi_Change_Data(p[0]);while(len--){Spi_Change_Data(*str);str++;}CS_H;//直到不忙才可以往下執行do{ret = Read_Register1();}while(ret & 0x1 << 0);}/************************************
函數功能:讀數據
函數形參:u32 addr--從這一頁的哪個位置開始讀取數據
u8 *str, -- 讀取到的數據存放的地方
u8 len -- 數據的長度
函數返回值:void
函數說明:發送的指令是0x03
可以一直讀到芯片最后
作者:
日期:
************************************/
void Read_Data(u32 addr,u8 *str,u8 len)
{u8 *p = (u8*)&addr;//現在是得到32位里的最低8位u8 ret;CS_L;Spi_Change_Data(0x03);//發送這24位的地址數據Spi_Change_Data(p[2]);Spi_Change_Data(p[1]);Spi_Change_Data(p[0]);while(len--){*str = Spi_Change_Data(0);str++;}CS_H;
}/************************************
函數功能:跨頁寫
函數形參:u32 addr--從這一頁的哪個位置開始讀取數據
u8 *str, -- 讀取到的數據存放的地方
u8 len -- 數據的長度
函數返回值:void
函數說明:一次性可以發送多于256字節的數據到FLASH里
問題:當前的頁編程不能超過本頁的最大地址
解決:
需要判斷什么時候到達本頁的最大地址了
如果已經到了則地址自動跳轉到下一頁判斷這一頁還剩多少空間,如果不夠再去換頁寫入作者:
日期:
************************************/
void Page_AutoWrite(u32 addr,u8 *str,u8 len)//addr = 0,len = 289
{u16 less_len = 0;less_len = 256 - addr % 256;//判斷任意一頁還剩多少空間可以寫//如果剩余的空間足夠存下要寫入的數據則寫入數據長度為當前數據長度if(less_len>=len){less_len = len;}while(1){//寫入數據Page_Program(addr,str,less_len);//判斷當前頁剩余空間是不是已經等于要寫入數據的長度,如果相等則不需要換頁if(less_len == len){break;}//"4564654"addr += less_len;//代表當前地址的偏移str += less_len;//加上已經寫入的長度可以去到未寫入的數據的位置len = len - less_len;//代表未寫入的數據長度less_len = 256;//當前如果能跑到這一步,肯定是到下一頁的起始地址了if(less_len>=len)//1024{less_len = len;}}
}
#ifndef __W25Q64_H_
#define __W25Q64_H_#include "stm32f4xx.h"
#include "io_bit.h"
#include "spi.h"
#include "stdio.h"void Read_ID(void);
void Sector_Erase(u32 addr);
void Page_Program(u32 addr,u8 *str,u8 len);
void Read_Data(u32 addr,u8 *str,u8 len);
void Page_AutoWrite(u32 addr,u8 *str,u8 len);#endif