(1)SPI程序層次
一、核心邏輯:“SPI Flash 操作” 是怎么跑起來的?
要讀寫 SPI Flash,需同時理解?硬件連接(怎么接線)?和?軟件分層(誰負責發指令、誰負責控制邏輯),二者結合才能讓數據正確讀寫。
二、逐圖拆解(從 “硬件物理連接” 到 “軟件邏輯分層”)
1. 「硬件接線 - 實際底板」
- 內容:展示 DShanMCU-F103 學習底板上,SPI Flash 模塊的物理安裝位置(絲印 “FLASH 模塊” 的排母接口)。
- 作用:實操時知道 “模塊插哪里”,確保硬件連接正確。
- 關鍵細節:
- 模塊名稱:“Flash 模塊”(紅框標注)。
- 底板標識:DShanMCU-F103 Base Board,確認自己用的開發板匹配。
2. 「軟件層次 - 操作 Flash 的分工」
- 內容:把 “操作 SPI Flash” 的軟件拆成 3 層,明確每層職責:
- 應用程序(最上層):決定 “讀寫什么數據、在 Flash 的哪個位置操作”(比如你寫的?
main.c
?里,要存一個漢字字模到 Flash 地址?0x0800
)。 - Flash 驅動(中間層):把應用程序的 “讀寫需求” 轉成?SPI 協議命令(比如讀操作要發?
0x03
?指令 + 地址,寫操作要先發擦除命令再寫)。 - SPI 控制器驅動(HAL)(最下層):最基礎的 “SPI 硬件控制”,負責發送具體的 SPI 時序信號(時鐘、數據),跟硬件寄存器直接交互。
- 應用程序(最上層):決定 “讀寫什么數據、在 Flash 的哪個位置操作”(比如你寫的?
- 作用:理解 “軟件怎么分層協作”,開發時知道改哪一層(比如換 Flash 型號,只需改 “Flash 驅動層”;換主控芯片,可能改 “HAL 層”)。
3. 「硬件框圖 - 系統級連接邏輯」
- 內容:從?SoC(芯片系統)?角度,展示 CPU、內存管理單元、SPI 控制器、Flash 之間的連接關系:
- CPU:發讀寫指令(比如 “讀 Flash 地址?
0x1000
?的數據”)。 - 內存管理單元:處理地址映射、片選信號(決定操作 RAM 還是 Flash 還是其他外設)。
- SPI 控制器:把 CPU 的指令轉成 SPI 時序(時鐘 SCK、數據 MOSI/MISO、片選 CS 等),發給外部 Flash。
- Flash:接收 SPI 信號,執行讀寫擦除操作,返回數據。
- CPU:發讀寫指令(比如 “讀 Flash 地址?
- 作用:理解 “硬件模塊如何協同工作”,比如為什么操作 Flash 時要控制片選信號(片選 3 對應 Flash),地址怎么通過內存管理單元轉發。
4. 「硬件原理圖 - 引腳級連接」
- 內容:SPI Flash 模塊與 STM32 主控的具體引腳連接:
- PA7(DI/SPI1 MOSI):主控 → Flash 發數據(Master Out Slave In)。
- PA5(SCK/SPI1 SCK):主控給 Flash 提供時鐘信號。
- PA6(DO/SPI1 MISO):Flash → 主控返回數據(Master In Slave Out)。
- PB9(CS):片選信號,拉低時選中 Flash 模塊(同一總線上可能接多個 SPI 設備,靠 CS 區分)。
- VCC +3.3V:給 Flash 模塊供電;GND:接地。
- 關鍵細節:
- “M6/M12 要互斥操作”:同一套 SPI 引腳可能接多個模塊(比如 M6 和 M12),同一時間只能選一個(通過不同 CS 控制)。
- 引腳功能:MOSI 發命令 / 地址 / 數據,MISO 收數據,SCK 同步時鐘,CS 選設備。
(2)SPI協議和SPI控制結構
一、知識地圖:SPI 學習的 “原子級” 邏輯鏈
要徹底掌握 SPI,需理解?“硬件物理層 → 信號時序層 → 控制器內部邏輯層 → 軟件驅動層”?的完整閉環
二、逐圖拆解:從 “物理線” 到 “寄存器位” 的原子級解析
1. 「硬件接線 - SPI 外設連接」
- 核心作用:明確 SPI 控制器如何外接多個設備(SPI Flash、SPI OLED),理解?“共享總線 + 片選區分”?的設計。
- 關鍵細節(陰暗角落!):
- 總線共享:
SCK、DO(MOSI)、DI(MISO)
?是共享總線,多個設備并聯在這三根線上。 - 片選(CS)的靈魂:
- 每個設備有獨立?
CS
(CS0 連 Flash,CS1 連 OLED),低電平有效(拉低對應 CS 才選中設備)。 - 同一時間只能有一個 CS 被拉低,否則總線沖突(比如同時選 Flash 和 OLED,MOSI 發的數據會亂套)。
- 每個設備有獨立?
- GPIO 的隱藏作用:如果 SPI 控制器的?
NSS
(硬件片選)不用,可通過 GPIO 模擬片選(靈活,但占 GPIO 資源)。
- 總線共享:
- 關聯下一張圖:知道設備咋連后,得理解 “SPI 信號咋交互” → 看時序圖。
2. 「SPI 控制器內部框圖」
- 核心作用:揭秘?“SPI 控制器內部咋把 CPU 數據轉成時序信號”,理解移位寄存器、緩沖器的關鍵作用。
- 關鍵模塊 & 陰暗細節:
- 移位寄存器(Shift register):
- 是 SPI 收發的核心!發送時,CPU 把數據寫入?
Tx buffer
,移位寄存器逐位把數據推到?MOSI
;接收時,MISO
?來的數據逐位移入移位寄存器,裝滿后送到?Rx buffer
。 - 不管是 8 位還是 16 位幀,都靠它按位 “搬運” 數據(比如發 0x56,會拆成 8 個比特,在 SCK 時鐘下一位位發)。
- 是 SPI 收發的核心!發送時,CPU 把數據寫入?
- Tx/Rx buffer(發送 / 接收緩沖):
Tx buffer
:CPU 寫數據到這里,移位寄存器從這里取數據發出去(先入先出,發完一個字節才會取下一個)。Rx buffer
:移位寄存器收完數據,存在這里,CPU 從這里讀(必須及時讀,否則新數據會覆蓋舊數據,導致?OVR
?溢出錯誤)。
- 波特率發生器(Baud rate generator):
- 決定?
SCK
?的頻率(比如?f_SCK = f_PCLK / (BR[2:0] + 1)
),直接影響傳輸速度(太快可能導致從機跟不上,出現數據錯誤)。
- 決定?
- 控制寄存器(SPI_CR1、SPI_CR2):
CPOL、CPHA
:決定時鐘極性和相位(后面時序圖重點講)。MSTR
:設為 1 表示主控模式(開發板作為 SPI 主機控制外設)。LSB FIRST
:設為 1 則先傳低位(默認先傳高位,需看外設要求)。TXEIE、RXNEIE
:發送 / 接收中斷使能(數據發完 / 收到時觸發中斷,異步通信常用)。
- 移位寄存器(Shift register):
- 關聯下一張圖:理解控制器內部后,得掌握 “SCK、MOSI、MISO 咋配合發數據” → 看時序圖。
3. 「SPI 時序圖(CPOL=1 系列)」
- 核心作用:明確?“時鐘極性(CPOL)、相位(CPHA)”?如何決定數據采樣時機,是 SPI 通信的協議靈魂。
- 陰暗細節(逐周期解析!):
- CPOL(時鐘極性):
CPOL=1
:SCK
?空閑時是高電平;CPOL=0
:空閑時是低電平(決定時鐘的 “基礎電平”)。
- CPHA(時鐘相位):
CPHA=0
:第一個時鐘沿(上升沿 / 下降沿,看 CPOL)采樣數據;CPHA=1
:第二個時鐘沿采樣數據。
- 以?
CPOL=1, CPHA=0
(Format A)為例:SCK
?空閑高電平 → 第一個時鐘沿是下降沿(從高→低),此時采樣?MOSI/MISO
?數據。- 數據傳輸:
MOSI
?先發?MSB
(最高位),MISO
?同步返回數據,8 個時鐘周期傳完 1 字節。
- 以?
CPOL=1, CPHA=1
(Format B)為例:- 第一個時鐘沿(下降沿)不采樣,第二個時鐘沿(上升沿)?采樣數據。
- 注意?
MISO
?數據延遲:返回的?LSB
?是 “之前傳輸字符的低位”(時序對齊需要,外設必須支持)。
- CPOL(時鐘極性):
- 關鍵坑點:
- 時序匹配:主機和從機的?
CPOL、CPHA
?必須完全一致,否則數據采樣錯位(比如主機用 CPOL=1,從機用 CPOL=0,時鐘沿對不上,數據全錯)。 - MSB/LSB 順序:默認?
LSB FIRST=0
(先傳 MSB),如果外設要求先傳 LSB,必須設?LSB FIRST=1
(看 datasheet!)。
- 時序匹配:主機和從機的?
- 關聯下一張圖:對比?
CPOL=0
?的時序,理解四種模式的差異 → 看CPOL=0 時序圖(圖 4)。
4. 「SPI 時序圖(CPOL=0 系列)」
- 核心作用:補充?
CPOL=0
?時的時序差異,理解?“四種 SPI 模式”?的完整邏輯。 - 陰暗細節(與 CPOL=1 對比):
CPOL=0
:SCK
?空閑時是低電平,第一個時鐘沿是上升沿(從低→高)。CPOL=0, CPHA=0
(Format A):- 第一個時鐘沿(上升沿)采樣數據,
MOSI
?先發?MSB
,MISO
?同步返回。
- 第一個時鐘沿(上升沿)采樣數據,
CPOL=0, CPHA=1
(Format B):- 第一個時鐘沿(上升沿)不采樣,第二個時鐘沿(下降沿)?采樣數據。
MISO
?返回的?LSB*
?是 “之前傳輸字符的低位”(時序對齊邏輯和 CPOL=1 類似)。
- 關鍵總結:
- 四種模式對應?
CPOL
(0/1)和?CPHA
(0/1)的組合,必須和外設手冊一致(比如 SPI Flash 可能要求模式 0,OLED 可能要求模式 3)。 - 常用模式:模式 0(CPOL=0, CPHA=0)?和?模式 3(CPOL=1, CPHA=1),因為它們都在上升沿采樣(不管空閑電平,只要沿對齊即可,兼容性好)。
- 四種模式對應?
5. 「SPI 傳輸示例(0x56 時序)」
- 核心作用:用實際數據(0x56 = 0b0101 0110)演示?“時序圖如何對應二進制位”,把抽象時序落地。
- 陰暗細節(逐位解析):
- CS0 拉低:選中 SPI Flash,開始傳輸。
- SCK 時鐘與數據的對應:
- 0x56 的二進制是?
0b0101 0110
,MSB
?是第 7 位(0),LSB
?是第 0 位(0)。 - 每個?
SCK
?周期對應 1 個比特:- 第 1 個 SCK 下降沿(CPOL=1, CPHA=0 時):發?
0
(MSB),Flash 采樣。 - 第 2 個 SCK 下降沿:發?
1
,依此類推… - 第 8 個 SCK 下降沿:發?
0
(LSB),傳輸結束。
- 第 1 個 SCK 下降沿(CPOL=1, CPHA=0 時):發?
- 0x56 的二進制是?
- 采樣時機:Flash 在每個 SCK 的上升沿采樣(因為示例中可能用了模式 3?需要結合前面的模式圖驗證)。
- 關鍵驗證:
- 數一下 SCK 周期和數據位是否對應(8 個周期傳 8 位),理解?
MSB 先傳
?的規則。 - 對比前面的時序模式圖,看這個示例屬于哪種?
CPOL、CPHA
?組合(比如這里 SCK 空閑高電平 → CPOL=1;下降沿發數據,上升沿采樣 → 可能是模式 2 或 3?需要細扣)。
- 數一下 SCK 周期和數據位是否對應(8 個周期傳 8 位),理解?
6. 「SPI 模式總結表」
- 核心作用:把四種 SPI 模式的規則表格化,方便快速查詢和配置。
- 陰暗細節(表格逐行解析):
- 模式 0(CPOL=0, CPHA=0):
- SCK 空閑低電平,第一個時鐘沿(上升沿)采樣數據 → 常用,很多外設默認支持。
- 模式 1(CPOL=0, CPHA=1):
- SCK 空閑低電平,第二個時鐘沿(下降沿)采樣數據 → 部分外設(如某些傳感器)可能用。
- 模式 2(CPOL=1, CPHA=0):
- SCK 空閑高電平,第一個時鐘沿(下降沿)采樣數據 → 較少用,但某些舊設備可能要求。
- 模式 3(CPOL=1, CPHA=1):
- SCK 空閑高電平,第二個時鐘沿(上升沿)采樣數據 → 常用(和模式 0 互補,覆蓋上升沿采樣場景)。
- 模式 0(CPOL=0, CPHA=0):
- 關鍵技巧:
- 記不住四種模式?記住 “常用模式 0 和 3”,它們都在上升沿采樣(不管空閑電平),配置時先試這兩個模式,不行再查外設手冊。
7. 「SPI 傳輸示例(0x56 時序圖)」
- 核心作用:和圖 5 呼應,用更規范的時序圖展示?
0x56
?傳輸,驗證?“數據位與時鐘沿的對應關系”。 - 陰暗細節(與圖 5 對比):
- 圖 5 是手繪版,圖 7 是規范版,都展示?
0x56 = 0b0101 0110
?的傳輸。 - 注意?
CS0
?拉低的時機(傳輸前拉低,傳輸后拉高),以及?SCK
?周期數(8 個周期傳 1 字節)。
- 圖 5 是手繪版,圖 7 是規范版,都展示?
- 關鍵驗證:
- 數?
DO
?線上的電平是否和?0b0101 0110
?一致(第 1 個 SCK 周期是?0
,第 2 個是?1
… 第 8 個是?0
)。
- 數?
8. 「SPI 主機模式配置步驟」
- 核心作用:把 “軟件配置 SPI 控制器” 的步驟標準化,指導代碼怎么寫。
- 陰暗細節(逐步驟解析):
- 1. 配置波特率(BR [2:0]):
- 決定?
SCK
?頻率(f_SCK = f_PCLK / (BR + 1)
),不能超過外設最大頻率(比如 SPI Flash 最大支持 80MHz,就不能設太高)。 - 示例:
BR[2:0] = 011
?→ 分頻系數 4 →?f_SCK = 84MHz / 4 = 21MHz
(假設 PCLK=84MHz)。
- 決定?
- 2. 配置 CPOL、CPHA:
- 根據外設手冊選模式(比如 Flash 要求模式 0 → CPOL=0, CPHA=0)。
- 3. 配置數據幀格式(DFF):
DFF=0
?→ 8 位幀(常用);DFF=1
?→ 16 位幀(某些設備如 OLED 可能用)。
- 4. 配置 LSBFIRST:
LSBFIRST=0
?→ 先傳 MSB(默認,大多數外設要求);LSBFIRST=1
?→ 先傳 LSB(少數外設如某些傳感器可能用)。
- 5. 配置 NSS(片選):
- 硬件模式:
NSS
?引腳接高電平,靠硬件自動控制; - 軟件模式:設?
SSM=1, SSI=1
,用軟件控制?CS
(靈活,適合多設備)。
- 硬件模式:
- 6. 使能 SPI(MSTR、SPE):
MSTR=1
?→ 主機模式;SPE=1
?→ 使能 SPI 控制器。
- 1. 配置波特率(BR [2:0]):
- 關鍵坑點:
- 波特率不能亂設:太高會導致從機收不到數據(比如 Flash 最大支持 30MHz,你設成 50MHz,就會丟數據)。
- NSS 配置易錯:軟件模式下必須設?
SSM=1
,否則?NSS
?引腳會自動拉低,導致總線沖突。
(3)SPI_HAL庫編程
第一步:先搞懂 SPI 是啥 —— 就像 “多人打電話”
SPI 是單片機(比如 STM32)和其他設備(比如傳感器、顯示屏)之間 “傳數據” 的一種方式,就像幾個人用電話通話:
- 主機:STM32(相當于發起通話的人)
- 從機:被控制的設備(比如傳感器,相當于接電話的人)
- 線的作用:
- SCK 線:時鐘線(相當于 “喂喂喂” 的節奏,保證雙方語速一致)
- MOSI 線:主機發、從機收(主機說話的線)
- MISO 線:從機發、主機收(從機回話的線)
- NSS 線:片選線(主機想跟哪個從機說話,就拉低對應從機的 NSS 線,相當于 “小明,聽我說”)
第二步:用 CubeMX “搭線路”—— 相當于 “插電話線”
在寫代碼前,需要用 STM32CubeMX 軟件配置 SPI 的 “硬件線路”,就像提前插好電話線、設置好通話規則。
1. 配置 SPI 核心參數
打開 CubeMX,找到 SPI 外設(比如 SPI1),配置以下參數:
- 模式:選 “全雙工主機”(最常用,主機既能說也能聽)
- 幀格式:
- 數據長度:8 位(一次傳 1 個字節,像一次說一個字)
- 高位在前(MSB First,像說話從第一個字開始)
- 時鐘極性 / 相位:默認選 “低電平空閑,第一個邊沿采樣”(記不住沒關系,CubeMX 默認值一般能用)
- 時鐘分頻:比如 “分頻 8”(STM32 主頻 72MHz 的話,SPI 時鐘就是 9MHz,相當于說話的語速,不能太快否則從機聽不懂)
- 下面會自動顯示引腳:比如 SPI1 的 SCK=PA5、MOSI=PA7、MISO=PA6(這些是硬件固定的,不用改)
2. 配置片選引腳
NSS 線(片選)一般用軟件控制(更靈活),需要手動配置一個普通 GPIO 當片選:
- 選一個引腳(比如 PB9),模式設為 “推挽輸出”(能輸出高低電平)
- 初始狀態設為 “高電平”(默認不選中從機,相當于 “先不打電話”)
3. 生成代碼
配置完后,點 “Generate Code” 生成初始化代碼,CubeMX 會自動幫我們寫好 SPI 的基礎設置(不用自己寫)。
第三步:用查詢方式 “發消息”—— 相當于 “對著電話一直說,等對方回應”
最基礎的通信方式:發送數據時,STM32 會 “一直等” 到發送完成,再做下一步(類似打電話時一直說,直到說完才停)。
1. 發送數據函數
用HAL_SPI_Transmit()
函數發送,格式:
HAL_SPI_Transmit(&hspi1, 發送的數據地址, 數據長度, 超時時間);
&hspi1
:CubeMX 生成的 SPI1 結構體(相當于指定用哪部電話)- 發送的數據地址:比如
&data
(要發的數據存在哪里) - 數據長度:比如 1(發 1 個字節)
- 超時時間:比如 100(最多等 100ms,沒發完就報錯)
2. 接收數據函數
用HAL_SPI_Receive()
函數接收,格式類似:
HAL_SPI_Receive(&hspi1, 接收數據的緩沖區地址, 長度, 超時時間);
3. 收發同時進行
用HAL_SPI_TransmitReceive()
,一邊發一邊收(全雙工的特點):
HAL_SPI_TransmitReceive(&hspi1, 要發的數據, 接收緩沖區, 長度, 超時時間);
舉個例子:給從機發命令并讀回數據
uint8_t send_data = 0x55; // 要發的命令
uint8_t recv_data; // 用來存接收的數據HAL_GPIO_WritePin(PB9_GPIO_Port, PB9_Pin, GPIO_PIN_RESET); // 拉低PB9,選中從機(“小明,聽著”)
HAL_SPI_TransmitReceive(&hspi1, &send_data, &recv_data, 1, 100); // 發命令同時收數據
HAL_GPIO_WritePin(PB9_GPIO_Port, PB9_Pin, GPIO_PIN_SET); // 拉高PB9,釋放從機(“說完了”)
第四步:用中斷方式 “發消息”—— 相當于 “說完一段話就掛電話,對方聽完打回來”
查詢方式會讓 STM32 一直等著,效率低。中斷方式是:STM32 發起發送后,就去做別的事,等數據發完了,硬件會 “打斷” STM32,提醒它 “發送完成了”(類似發微信,不用一直盯著,收到回復再看)。
1. 中斷函數怎么用
用帶_IT
后綴的函數,比如:
- 發送:
HAL_SPI_Transmit_IT(&hspi1, 數據地址, 長度);
- 接收:
HAL_SPI_Receive_IT(&hspi1, 緩沖區, 長度);
- 收發同時:
HAL_SPI_TransmitReceive_IT(...);
這些函數調用后會立刻返回,STM32 可以去干別的(比如亮燈、讀按鍵)。
2. 中斷是怎么 “提醒” 的(對應 “圖 4→圖 3→圖 5→圖 2”)
中斷的流程像 “快遞送貨”:
(SPI1_IRQHandler):硬件中斷入口(相當于快遞員到家門口按門鈴),這是 STM32 芯片自帶的函數,會自動調用 HAL 庫的處理函數。
(HAL_SPI_IRQHandler):HAL 庫的中斷總處理(相當于家人聽到門鈴,去開門),它會檢查是發送中斷還是接收中斷,然后調用具體的處理函數。
(中斷初始化邏輯):在調用
_IT
函數時,HAL 庫會提前 “綁定” 好具體處理函數(比如 8 位數據對應SPI_2linesRxISR_8BIT
),相當于 “告訴家人,快遞來了怎么處理”。(SPI_2linesRxISR_8BIT):實際處理接收的函數(相當于家人接過快遞,拆開看),會把收到的字節存到緩沖區,直到收完所有數據。
3. 收到 “提醒” 后做什么 —— 回調函數
當中斷處理完數據(比如發送完成、接收完成),HAL 庫會自動調用 “回調函數”,我們可以在回調函數里寫后續操作(比如收到數據后計算、點燈)。
常用回調函數:
HAL_SPI_TxCpltCallback()
:發送完成回調HAL_SPI_RxCpltCallback()
:接收完成回調HAL_SPI_ErrorCallback()
:出錯時回調
這一步放 “圖 6”,它列出了各種回調函數,說明什么時候會被調用
舉個例子:用中斷接收數據
uint8_t recv_buf[5]; // 接收緩沖區// 啟動中斷接收(收5個字節)
HAL_GPIO_WritePin(PB9_GPIO_Port, PB9_Pin, GPIO_PIN_RESET); // 選中從機
HAL_SPI_Receive_IT(&hspi1, recv_buf, 5); // 啟動接收,立刻返回// 接收完成后,自動調用這個回調函數
void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) {if (hspi == &hspi1) { // 確認是SPI1的中斷HAL_GPIO_WritePin(PB9_GPIO_Port, PB9_Pin, GPIO_PIN_SET); // 釋放從機// 這里可以處理收到的recv_buf數據,比如打印、計算}
}
第五步:用 DMA 方式 “發消息”—— 相當于 “雇個快遞員,自己不用管”
如果要傳大量數據(比如發 1000 個字節),用查詢或中斷會占用 STM32 太多時間。DMA 方式相當于 “雇個快遞員”,讓 DMA 控制器直接搬運數據,STM32 全程不用插手(效率最高)。
DMA 函數怎么用
用帶_DMA
后綴的函數,比如:
- 發送:
HAL_SPI_Transmit_DMA(&hspi1, 數據地址, 長度);
- 接收:
HAL_SPI_Receive_DMA(&hspi1, 緩沖區, 長度);
- 收發同時:
HAL_SPI_TransmitReceive_DMA(...);
調用后,DMA 會自動搬數據,完成后通過中斷通知 STM32(和中斷方式一樣,會調用回調函數)。
(4)SPI_Flash_W25Q64操作方法
一、基礎概念與整體邏輯
W25Q64 是常用的 SPI 接口 Flash 存儲芯片,要操作它(讀、寫、擦除等),需遵循?“先理解芯片存儲結構 → 掌握 SPI 指令交互流程 → 按步驟實現讀寫擦操作”?的邏輯。
簡單說:
- 存儲結構:芯片像很多 “小格子”,有頁(256 字節)、扇區(4KB = 16 頁 )等劃分,操作要對應這些單位。
- SPI 交互:通過 SPI 總線發 “指令 + 地址 + 數據”,讓 Flash 執行讀、寫、擦除,還要用狀態寄存器判斷操作是否完成。
二、核心流程串聯
1. 讀數據流程(最基礎,先學它!)
作用:從 Flash 指定地址把數據讀出來,比如讀取之前存的配置、日志。
(1)讀操作時序
(9.5.1 讀數據 - 時序圖):
這是讀操作的完整 SPI 時序,步驟分解:- 拉低 /CS:選中 Flash 芯片(相當于敲門說 “我要操作你啦” )。
- 發讀指令(0x03):通過 MOSI 線發指令?
0x03
,告訴 Flash “我要讀數據” 。 - 發 24 位地址:接著發要讀取的地址(比如?
0x000000
?),告訴 Flash “從這個位置開始讀” 。 - 讀數據:地址發完后,Flash 會從 MISO 線把數據傳回來,讀一個字節后,內部地址自動 +1,可連續讀很多數據,直到 /CS 拉高。
補充解釋了 “發完地址后,Flash 持續輸出數據,直到操作結束”,幫你理解?“連續讀”?邏輯 —— 讀一個字節后,地址自動遞增,能一直讀到芯片末尾,不用每次重新發地址。
2. 寫數據流程(要擦除后才能寫,稍復雜 )
注意:Flash 特性是 “寫之前必須擦除”(因為只能從 1 改 0,擦除是把 0 改回 1 ),所以寫操作分?“擦除扇區 → 寫使能 → 燒寫頁”?三步。
(1)寫使能
作用:告訴 Flash “我要準備寫數據 / 擦除了,打開寫權限”,是寫、擦除操作的?必要前提?。
- (寫使能時序):
步驟:- 拉低 /CS → 選中芯片。
- 發寫使能指令?
0x06
?→ 告訴 Flash “允許我寫數據啦” 。 - 拉高 /CS → 結束操作。
為什么必須?:Flash 有 “寫保護”,發?0x06
?是解除保護的鑰匙,否則寫、擦除會失敗!
(2)擦除扇區
作用:寫數據前,必須把要寫的區域擦成 “全 1”,W25Q64 最小擦除單位是?扇區(4KB = 16 頁 )?。
- (擦除扇區時序):
步驟:- 拉低 /CS → 選中芯片。
- 發擦除指令?
0x20
?→ 告訴 Flash “我要擦除扇區” 。 - 發 24 位地址 → 指定要擦除的扇區(比如地址?
0x000000
?對應第 0 扇區 )。 - 拉高 /CS → 啟動擦除(擦除需要時間,不是立刻完成 )。
怎么判斷擦除完成?:擦除時 Flash 內部忙,要讀?狀態寄存器(后面講)?看?BUSY
?位,BUSY=0
?才代表擦除完!
(3)燒寫頁
作用:把數據寫入 Flash,最小寫入單位是?頁(256 字節 )?,可從頁內任意位置開始寫,寫超頁末尾會 “繞回頁開頭” 。
- (燒寫頁時序):
步驟:- 拉低 /CS → 選中芯片。
- 發頁編程指令?
0x02
?→ 告訴 Flash “我要寫數據到頁里” 。 - 發 24 位地址 → 指定要寫入的頁起始地址(比如?
0x000000
?對應第 0 頁 )。 - 發數據(最多 256 字節 )→ 把要存的數據通過 MOSI 發過去,寫超 256 字節會覆蓋頁開頭數據。
- 拉高 /CS → 啟動寫入(同樣要等狀態寄存器?
BUSY=0
?才完成 )。
3. 狀態寄存器
作用:不管是擦除還是寫入,Flash 都需要時間完成(尤其是擦除,可能要幾毫秒到幾十毫秒 )。通過讀?狀態寄存器?里的?BUSY
?位,能判斷操作是否完成。
- (狀態寄存器結構):
這是狀態寄存器的 8 位含義,重點看?最低位(S0)→ BUSY 位?:BUSY=1
:Flash 正在忙(擦除、寫入中 ),不能執行新操作。BUSY=0
:操作完成,可執行下一個指令。
怎么讀狀態寄存器?:發專門的 “讀狀態寄存器指令(0x05
?)”,流程類似讀數據:拉低 /CS → 發?0x05
?→ 讀 1 個字節(狀態寄存器值 )→ 拉高 /CS 。
4. 芯片存儲結構說明
作用:理解 “頁、扇區、塊” 的劃分,知道操作單位,避免寫錯區域或擦除范圍不對。
- (英文說明 - 存儲結構):
關鍵翻譯:- W25Q64 有?32768 頁?,每頁 256 字節(所以總容量 32768×256 = 8MB = 64Mbit,對應型號 )。
- 擦除單位:
- 扇區(4KB ):16 頁擦除一次(
0x20
?指令 )。 - 塊(32KB ):128 頁擦除一次(另一個指令,比如?
0x52
?,課程里沒細講但要知道有更大單位 )。 - 塊(64KB ):256 頁擦除一次(指令?
0xD8
?)。 - 整片擦除:全部內容擦除(指令?
0xC7
?)。
- 扇區(4KB ):16 頁擦除一次(
實際用:小數據寫入用 “頁編程”,大范圍擦除用 “扇區 / 塊擦除”,根據需求選。
三、完整操作流程總結(從讀 → 擦除 → 寫 )
把上面的步驟串起來,比如 “要寫數據到 Flash 某地址”,完整流程是:
擦除對應扇區:
- 發寫使能→ 發擦除扇區指令 + 地址→ 循環讀狀態寄存器,直到?
BUSY=0
。
- 發寫使能→ 發擦除扇區指令 + 地址→ 循環讀狀態寄存器,直到?
燒寫頁數據:
- 發寫使能→ 發頁編程指令 + 地址 + 數據→ 循環讀狀態寄存器,直到?
BUSY=0
?。
- 發寫使能→ 發頁編程指令 + 地址 + 數據→ 循環讀狀態寄存器,直到?
驗證數據(讀操作):
- 用讀指令讀剛才寫的地址,對比數據是否正確。
(5)W25Q64 SPI Flash 驅動開發(內部函數篇)保姆級筆記
這篇筆記只關注 “如何實現功能”,不糾結代碼規范,帶你你從 “新建文件” 到 “每個函數怎么用” 一步步看懂,零基礎也能跟著做!
一、前期準備:新建文件并添加到工程
1. 新建文件
打開你的工程文件夾(比如叫 “STM32_Project”),右鍵新建兩個文件:
- 一個叫?
driver_spi_flash.c
(放具體代碼邏輯) - 一個叫?
driver_spi_flash.h
(放函數聲明,后面會用到)
- 一個叫?
2. 添加到工程
打開編程軟件(比如 Keil MDK),右鍵 “Source Group”→“Add Files to Group”,選中剛新建的?
driver_spi_flash.c
,點 “Add”。
二、硬件配置回顧(代碼能跑的前提)
在寫代碼前,要先通過 CubeMX 配置好硬件,這些配置是代碼能正常運行的基礎:
1. SPI 外設配置
- 簡單說:STM32 作為 “主機”,通過 SPI1 和 W25Q64 通信,用 “中斷方式” 收發數據(后面代碼里會用到?
HAL_SPI_Transmit_IT
?這類函數)。
2. 片選引腳配置
- 作用:PB9 是控制 W25Q64 “是否工作” 的開關,拉低就是 “選中它”,拉高就是 “不選中”。
三、driver_spi_flash.c
?代碼逐行解析
從最基礎的 “選芯片” 到 “發指令”,每個函數都講清楚怎么用、為什么這么寫:
1. 頭文件和變量聲明
#include "driver_spi_flash.h" // 自己建的頭文件,后面會放函數聲明
#include "ascii_font.c" // 可能是字庫文件,暫時用不到可以不管
#include "stm32f1xx_hal.h" // HAL庫的核心文件,提供SPI、GPIO等函數// 聲明兩個等待函數(在其他文件里實現,比如spi.c)
void Wait_spi1_txcplt(void); // 等SPI發送完成
void Wait_spi1_txrxcplt(void); // 等SPI收發完成// 引用外部的SPI1配置結構體(CubeMX自動生成的,存著SPI的各種參數)
extern SPI_HandleTypeDef hspi1;
作用:引入必要的工具(函數、變量),讓下面的代碼能正常調用。
2. 內部函數:控制片選(選芯片 / 不選芯片)
用法:每次和 W25Q64 通信前,先用?spiFlash_select()
?選中它;通信結束后,用?spiFlash_deselect()
?釋放它。
3. 內部函數:寫使能(允許芯片被寫入 / 擦除)
// 發送“寫使能”指令(0x06),讓芯片允許后續的寫入或擦除操作
static int spiFlash_writeEnable(void)
{uint8_t BUF[1] = {0x06}; // 定義要發送的指令:0x06就是“寫使能”的意思// 用中斷方式發送這個指令:啟動發送后,函數會立刻返回,等發送完會觸發中斷HAL_SPI_Transmit_IT(&hspi1, BUF, 1);Wait_spi1_txcplt(); // 等待發送完成(這個函數會一直等,直到SPI發送完數據)
}
(片選拉低→發 0x06→片選拉高)
為什么要做:W25Q64 默認不允許寫入或擦除,必須先發這個指令 “解鎖”,否則后續操作會失敗。
4. 內部函數:讀狀態寄存器(判斷芯片是否忙)
// 讀芯片的狀態寄存器,判斷它是否在工作(比如正在擦除或寫入)
static int spiFlash_readstatus(void)
{uint8_t txBUF[2] = {0x05, 0xff}; // 要發送的內容:0x05是“讀狀態”指令,0xff是填充數uint8_t rxBUF[2] = {0, 0}; // 用來存接收到的數據(芯片返回的狀態)// 用中斷方式同時收發:發送指令的同時,接收芯片返回的狀態HAL_SPI_TransmitReceive_IT(&hspi1, txBUF, rxBUF, 2);Wait_spi1_txrxcplt(); // 等待收發完成return rxBUF[1]; // 返回狀態寄存器的值(第二個字節才是有效狀態)
}
(發 0x05 指令后,芯片返回狀態值)
作用:芯片擦除或寫入時會 “忙”,狀態寄存器的第 0 位是 “忙標志”(1 = 忙,0 = 空閑),讀它能知道芯片是否準備好接受新指令。
5. 內部函數:等待芯片空閑
// 一直等,直到芯片不忙(狀態寄存器的忙標志為0)
static int spiFlash_WriteRead(void)
{while(spiFlash_readstatus() & 1 == 1); // 讀狀態,如果忙就一直等
}
用法:在擦除或寫入操作后調用,等芯片完成當前工作,再進行下一步。
6. 宏定義與全局聲明
// 定義SPI操作超時時間(單位:毫秒)
#define SPI_FLASH_TIMEOUT 1000// 聲明SPI等待函數(用于等待傳輸完成)
void Wait_spi1_txcplt(int timeout); // 等待發送完成
void Wait_spi1_txrxcplt(int timeout); // 等待收發完成
void Wait_spi1_rxcplt(int timeout); // 等待接收完成// 聲明SPI句柄(在其他文件中初始化,如main.c)
extern SPI_HandleTypeDef hspi1;// 全局標志位(用于中斷與主程序同步,需在.c文件中定義)
static volatile uint8_t spi1_tx_done = 0; // 發送完成標志
static volatile uint8_t spi1_rx_done = 0; // 接收完成標志
static volatile uint8_t spi1_txrx_done = 0; // 收發完成標志
作用:定義超時時間、聲明等待函數和 SPI 句柄,以及用于中斷同步的標志位。
7. 等待函數實現
//SPI.C
static volatile int g_spil_tx_complete= 0;
static volatile int g_spil_txrx_complete= 0;
static volatile int g_spil_rx_complete= 0;
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi)
{if(hspi == &hspi1){g_spil_tx_complete =1;}
}
void Wait_spi1_txcplt(int timeout)
{
while (g_spil_tx_complete == 0 && timeout--)
{HAL_Delay(1);
}g_spil_tx_complete = 0;
}void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi)
{if(hspi == &hspi1){g_spil_txrx_complete =1;}
}void Wait_spi1_txrxcplt(int timeout)
{
while (g_spil_txrx_complete == 0 && timeout--)
{
HAL_Delay(1);
}g_spil_txrx_complete = 0;
}void Wait_spi1_rxcplt(int timeout)
{
while (g_spil_rx_complete == 0 && timeout--)
{HAL_Delay(1);
}g_spil_rx_complete = 0;
}
8. 對外函數
這些函數是給用戶直接調用的,比如讀 ID、擦除扇區等,雖然現在是空的,但框架要清楚:
(1)讀芯片 ID
int spi_flash_readID(void)
{uint8_t txBUF[2] = {0x9F, 0xff}; // 發送緩沖區:讀ID命令+填充字節uint8_t rxBUF[2] = {0, 0}; // 接收緩沖區spiFlash_select(); // 選中SPI閃存(拉低CS信號)// 以中斷方式發送2字節數據,同時接收2字節數據HAL_SPI_TransmitReceive_IT(&hspi1, txBUF, rxBUF, 2);Wait_spi1_txrxcplt(SPI_FLASH_TIMEOUT); // 等待收發完成(帶超時)spiFlash_deselect(); // 取消選中(拉高CS信號)return rxBUF[1]; // 返回接收的ID值
}
功能:讀取 SPI 閃存的芯片 ID,用于驗證芯片是否正常連接。
關鍵細節:
- 命令
0x9F
是 SPI 閃存的 "讀 ID 命令"(不同芯片可能有差異)。 - 發送緩沖區第二個字節
0xff
是占位符,SPI 通信為全雙工,發送的同時會接收數據。 - 芯片 ID 通過
rxBUF[1]
返回(具體位置取決于芯片規格,部分芯片可能返回多字節 ID)
(2)擦除扇區
int spi_flash_ErasesSector(uint32_t addr)
{uint8_t txBUF[4] = {0x20}; // 發送緩沖區:扇區擦除命令/* 寫使能 */spiFlash_writeEnable(); // 發送寫使能命令,允許擦除操作/* 填充地址 */txBUF[1] = (addr >> 16) & 0xff; // 地址高8位txBUF[2] = (addr >> 8) & 0xff; // 地址中8位txBUF[3] = (addr >> 0) & 0xff; // 地址低8位spiFlash_select(); // 選中芯片// 以中斷方式發送4字節命令(擦除命令+地址)HAL_SPI_Transmit_IT(&hspi1, txBUF, 4);Wait_spi1_txcplt(SPI_FLASH_TIMEOUT); // 等待發送完成spiFlash_deselect(); // 取消選中/* 等待擦除完成 */spiFlash_WriteRead(); // 循環等待芯片空閑(狀態寄存器忙標志位清零)return 0; // 返回成功狀態
}
功能:擦除指定地址addr
所在的扇區(扇區大小由芯片決定,通常為 4KB/64KB)。
關鍵細節:
- 命令
0x20
是扇區擦除指令,必須配合地址使用。 - 擦除前必須調用
spiFlash_writeEnable()
,否則操作無效(硬件保護機制)。 - 擦除是耗時操作(毫秒級),
spiFlash_WriteRead()
會等待操作完成后再返回。
(3)寫數據
int spi_flash_writeData(uint32_t addr, uint8_t *data, uint32_t len)
{uint8_t txBUF[4] = {0x02}; // 發送緩沖區:頁寫命令/* 寫使能 */spiFlash_writeEnable(); // 允許寫入操作/* 填充地址 */txBUF[1] = (addr >> 16) & 0xff; // 地址高8位txBUF[2] = (addr >> 8) & 0xff; // 地址中8位txBUF[3] = (addr >> 0) & 0xff; // 地址低8位spiFlash_select(); // 選中芯片/* 發送命令和地址 */HAL_SPI_Transmit_IT(&hspi1, txBUF, 4);Wait_spi1_txcplt(SPI_FLASH_TIMEOUT); // 等待命令發送完成/* 發送數據 */HAL_SPI_Transmit_IT(&hspi1, data, len); // 發送用戶數據Wait_spi1_txcplt(SPI_FLASH_TIMEOUT); // 等待數據發送完成spiFlash_deselect(); // 取消選中/* 等待寫入完成 */spiFlash_WriteRead(); // 等待芯片完成寫入操作return 0; // 返回成功狀態
}
功能:向指定地址addr
寫入長度為len
的字節數據(*data
)。
關鍵細節:
- 命令
0x02
是 "頁寫命令",一次寫入不能跨頁(頁大小通常為 256 字節)。 - 寫入前必須確保目標扇區已擦除(擦除后數據為
0xff
,才能寫入非0xff
值)。 - 分兩步發送:先發送命令和地址,再發送實際數據,符合 SPI 閃存的寫入時序要求。
(4)讀數據
int spi_flash_ReadData(uint32_t addr, uint8_t *datas, uint32_t len)
{uint8_t txBUF[4] = {0x03}; // 發送緩沖區:讀數據命令/* 填充地址 */txBUF[1] = (addr >> 16) & 0xff; // 地址高8位txBUF[2] = (addr >> 8) & 0xff; // 地址中8位txBUF[3] = (addr >> 0) & 0xff; // 地址低8位spiFlash_select(); // 選中芯片/* 發送命令和地址 */HAL_SPI_Transmit_IT(&hspi1, txBUF, 4);Wait_spi1_txcplt(SPI_FLASH_TIMEOUT); // 等待命令發送完成/* 讀取數據 */HAL_SPI_Receive_IT(&hspi1, datas, len); // 接收數據到datas緩沖區Wait_spi1_rxcplt(SPI_FLASH_TIMEOUT); // 等待接收完成spiFlash_deselect(); // 取消選中return 0; // 返回成功狀態
}
功能:從指定地址addr
讀取長度為len
的字節數據,存儲到*datas
緩沖區。
關鍵細節:
- 命令
0x03
是 "讀數據命令",支持連續讀取(不受頁限制)。 - 讀取操作無需寫使能(只讀操作無硬件保護)。
- 分兩步執行:先發送命令和地址,再接收數據,符合 SPI 閃存的讀取時序。
記得聲明
9.主函數
#include "main.h"
#include "dma.h"
#include "i2c.h"
#include "spi.h"
#include "usart.h"
#include "gpio.h"
#include "circle_buffer.h"
#include "driver_SPI_Flash.h"
void Wait_Tx_Complete(void); // 等待UART發送完成
void Wait_Rx_Complete(void); // 等待UART接收完成
void startuart1recv(void); // 啟動UART1接收中斷
int UART1getchar(uint8_t *pVal);// 從UART1獲取一個字符
void HAL_I2C_MasterTxCpltCallback(I2C_HandleTypeDef *hi2c);//主設備發送完成回調函數
/* USER CODE END 1 */
void Wait_i2c1Tx_Complete(void);
void HAL_I2C_MasterRxCpltCallback(I2C_HandleTypeDef *hi2c);//主設備接收完成回調函數
void Wait_i2c1Rx_Complete(void);
void HAL_I2C_MemTxCpltCallback(I2C_HandleTypeDef *hi2c);//主設備發送完成回調函數
void HAL_I2C_MemRxCpltCallback(I2C_HandleTypeDef *hi2c);//主設備發送完成回調函數
// 環形緩沖區相關變量:用于存儲按鍵數據
static uint8_t g_data_buf[100]; // 緩沖區的實際存儲空間(可以存100個字節)
static circle_buf g_key_bufs; // 環形緩沖區結構體(管理存儲空間的讀寫)int main(void)
{int len; // 臨時變量:存儲字符串長度// 定義要發送的字符串:\r\n是換行符(串口通信中常用)char *str = "Please enter a char: \r\n";char *str2 = "www.100ask.net";char c; // 存儲接收的字符char flash_buf[20];HAL_Init();SystemClock_Config();// 初始化環形緩沖區:circld_buf_init是拼寫錯誤,正確應為circle_buf_init// 參數:緩沖區結構體、大小100、存儲空間g_data_bufcircld_buf_init(&g_key_bufs, 100, g_data_buf);MX_GPIO_Init();MX_DMA_Init();MX_I2C1_Init();MX_USART1_UART_Init();MX_SPI1_Init();OLED_Init(); // 初始化OLED屏幕OLED_Clear(); // 清屏(清除OLED上的所有顯示)
OLED_Printchinese(5,6);
int id =spi_flash_readID();
OLED_PrintHex(0,0,id,1);startuart1recv(); // 啟動UART1接收中斷:讓UART1準備好接收數據,收到數據后會觸發中斷
spi_flash_ErasesSector(0);//每個扇區大小是4096
spi_flash_writeData(0,str2,strlen(str2)+1);
spi_flash_ReadData(0,flash_buf,20);
OLED_PrintString(0,2,flash_buf);while (1){}
}
四、函數調用流程示例(以 “擦除扇區” 為例)
用一個實際操作演示這些內部函數怎么配合:
- 先調用?
spiFlash_writeEnable()
?發寫使能指令(解鎖); - 調用?
spiFlash_select()
?選中芯片; - 發送 “擦除扇區” 指令和地址;
- 調用?
spiFlash_WriteRead()
?等待擦除完成; - 調用?
spiFlash_deselect()
?釋放芯片。
這樣一步步看下來,你能從 “文件怎么建” 到 “每個函數干什么” 再到 “函數怎么配合工作”,完全掌握這些內部函數的用法,后續完善對外函數時就會很輕松啦!