SPI 通訊(Serial Peripheral Interface)是嵌入式開發中最基礎也最常用的通訊協定之一。無論是 ESP32 還是 STM32,幾乎所有感測器、顯示器、SD 卡模組都透過 SPI 溝通。但很多開發者看完 SPI 時序圖還是一頭霧水——CLK 極性是什麼?CPOL、CPHA 到底怎麼設?
這篇文章會用最直觀的方式——時序圖 + 實戰程式碼,帶你徹底搞懂 SPI,並實際讓 ESP32 和 STM32 成功通訊。
目錄
- 一、什麼是 SPI?
- 二、SPI 時序圖解讀(附 Python 生成圖)
- 三、四種 SPI Mode 詳解
- 四、ESP32 作為 Master(Arduino IDE)
- 五、STM32 作為 Slave(STM32CubeIDE + HAL)
- 六、接線方式
- 七、用邏輯分析儀驗證
- 八、常見問題與排錯
一、什麼是 SPI?
SPI 是一種全雙工、同步、主從式的通訊協定,由 Motorola 在 1980 年代提出。它用四條線解決問題:
| 訊號線 | 全名 | 作用 |
|---|---|---|
| SCLK / SCK | Serial Clock | 時脈,由 Master 產生 |
| MOSI | Master Out Slave In | Master → Slave 資料線 |
| MISO | Master In Slave Out | Slave → Master 資料線 |
| CS / SS | Chip Select / Slave Select | 片選,低電位有效 |
與 I2C 相比,SPI 的優點是速度更快(可達 40MHz 以上)、全雙工;缺點是需要更多接腳(每多一個 Slave 多一條 CS)。
二、SPI 時序圖解讀
以下是用 Python + WaveDrom 產生的 SPI Mode 0 時序圖。這是最常見的模式(CPOL=0, CPHA=0):
看時序圖的關鍵:
- SCLK:Master 產生的時脈訊號。Mode 0 時,空閒狀態為低電位,在上升緣採樣資料。
- CS:拉低表示開始通訊,拉高表示結束。
- MOSI:Master 在每個 CLK 上升緣送出一個 bit,從 D7(MSB)開始。
- MISO:Slave 在同一個上升緣同步送回資料。
- 一個 byte 需要 8 個時脈週期。
時序圖本身就是用 Python 產生的,JSON 定義如下:
{
"signal": [
{"name": "SCLK", "wave": "P....|...."},
{"name": "CS", "wave": "1.0..|..0."},
{"name": "MOSI", "wave": "0.101|01.0"},
{"name": "MISO", "wave": "0..0.|.1.0"},
{"name": "Data", "wave": "x.==.|==.x",
"data": ["D7","D6","D5","D4","D3","D2","D1","D0"]}
]
}
三、四種 SPI Mode 詳解
SPI 的四種 Mode 由 CPOL(Clock Polarity)和 CPHA(Clock Phase)決定:
| Mode | CPOL | CPHA | 空閒 CLK | 資料採樣 | 常用裝置 |
|---|---|---|---|---|---|
| 0 | 0 | 0 | 低電位 | 上升緣 | W25Qxx、ST7735、MAX6675 |
| 1 | 0 | 1 | 低電位 | 下降緣 | 某些 ADC |
| 2 | 1 | 0 | 高電位 | 下降緣 | NRF24L01 |
| 3 | 1 | 1 | 高電位 | 上升緣 | 某些感測器 |
記法:Mode 0 和 Mode 3 都是上升緣採樣,差別只在空閒時 CLK 是低還是高。90% 的感測器都用 Mode 0,可以先以此為預設值。
四、ESP32 作為 Master(Arduino IDE)
以下程式讓 ESP32 作為 SPI Master,每秒發送一筆資料給 STM32:
#include <SPI.h>
#define CS_PIN 5
#define SCK_PIN 18
#define MOSI_PIN 23
#define MISO_PIN 19
#define SPI_FREQ 1000000
#define SPI_MODE SPI_MODE0
void setup() {
Serial.begin(115200);
SPI.begin(SCK_PIN, MISO_PIN, MOSI_PIN, CS_PIN);
SPI.beginTransaction(SPISettings(SPI_FREQ, MSBFIRST, SPI_MODE));
pinMode(CS_PIN, OUTPUT);
digitalWrite(CS_PIN, HIGH);
Serial.println("ESP32 SPI Master 已啟動");
}
void loop() {
uint8_t tx_data[] = {0xAA, 0xBB, 0xCC, 0xDD};
uint8_t rx_data[4] = {0};
digitalWrite(CS_PIN, LOW);
delayMicroseconds(10);
for (int i = 0; i < 4; i++) {
rx_data[i] = SPI.transfer(tx_data[i]);
}
digitalWrite(CS_PIN, HIGH);
delayMicroseconds(10);
Serial.print("TX: ");
for (int i = 0; i < 4; i++) {
Serial.printf("%02X ", tx_data[i]);
}
Serial.print("| RX: ");
for (int i = 0; i < 4; i++) {
Serial.printf("%02X ", rx_data[i]);
}
Serial.println();
delay(1000);
}
五、STM32 作為 Slave(STM32CubeIDE + HAL)
STM32 這邊使用 HAL 庫配置為 SPI Slave,中斷方式接收:
/* STM32 SPI Slave 接收程式 */
/* 使用 STM32CubeMX 配置 SPI1 為 Slave */
#include "main.h"
SPI_HandleTypeDef hspi1;
uint8_t rx_buffer[4];
uint8_t tx_buffer[4] = {0x11, 0x22, 0x33, 0x44};
volatile uint8_t spi_ready = 0;
void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) {
if (hspi->Instance == SPI1) {
spi_ready = 1;
}
}
int main(void) {
HAL_Init();
SystemClock_Config();
MX_SPI1_Init();
HAL_SPI_Receive_IT(&hspi1, rx_buffer, 4);
while (1) {
if (spi_ready) {
spi_ready = 0;
HAL_SPI_TransmitReceive_IT(&hspi1, tx_buffer, rx_buffer, 4);
printf("SPI 收到: ");
for (int i = 0; i < 4; i++) {
printf("%02X ", rx_buffer[i]);
}
printf("\r\n");
}
}
}
void MX_SPI1_Init(void) {
hspi1.Instance = SPI1;
hspi1.Init.Mode = SPI_MODE_SLAVE;
hspi1.Init.Direction = SPI_DIRECTION_2LINES;
hspi1.Init.DataSize = SPI_DATASIZE_8BIT;
hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;
hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;
hspi1.Init.NSS = SPI_NSS_HARD_INPUT;
hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
hspi1.Init.CRCPolynomial = 10;
HAL_SPI_Init(&hspi1);
}
六、接線方式
ESP32 與 STM32 的 SPI 接線非常直觀——MOSI 接 MOSI,MISO 接 MISO,重點是要共地:
| ESP32(Master) | → | STM32(Slave) |
|---|---|---|
| GPIO18 (SCK) | → | PA5 (SCK) |
| GPIO23 (MOSI) | → | PA7 (MOSI) |
| GPIO19 (MISO) | → | PA6 (MISO) |
| GPIO5 (CS) | → | PA4 (NSS) |
| GND | → | GND |
⚠️ 注意電壓:ESP32 是 3.3V 邏輯,STM32 也是 3.3V(F4/H7 系列),可以直接連接。如果是 5V 的型號,需要電位轉換器。
七、用邏輯分析儀驗證
如果你有 USB 邏輯分析儀(如 Saleae、Kingst、DSLogic),接上 SCLK、MOSI、MISO、CS,你應該會看到:
- CS 先拉低
- SCLK 產生 8 個脈波
- MOSI 上看到 0xAA
- MISO 上看到 0x11
- CS 拉高 → 結束
用 Python + pyusb 也可以直接解析邏輯分析儀的資料,之後會寫一篇專門的教學。
八、常見問題與排錯
Q1:收到全 0xFF 或全 0x00
最常見的原因:CS 沒拉對。用示波器或邏輯分析儀確認 CS 是否有確實拉低。其次是 Mode 不匹配——Master 用 Mode 0,Slave 用 Mode 1 就會收到 garbage。
Q2:資料偶爾掉 byte
通常是時脈太快或接線太長。SPI 在麵包板上超過 20cm 就開始不穩,降頻到 500KHz 試試。另外,在 SCK 上串一個 100Ω 電阻可以抑制反射。
Q3:STM32 Slave 沒反應
檢查 STM32CubeMX 中 NSS 的配置。Slave 模式下,NSS 要設為 Hardware Input,不是 Software。如果用 Software 模式,CS 訊號不會被硬體自動識別。
Q4:通訊不穩定,斷斷續續
檢查電源。ESP32 的 WiFi 發射時會吃掉大量電流,如果電源供應不足,SPI 就會出錯。建議 ESP32 和 STM32 各自獨立供電。
總結
SPI 是嵌入式開發的必修課,但其實並不複雜:
📖 延伸閱讀:MQTT 通訊教學 · I2C 通訊教學 · RS485 通訊教學
- 四條線、一個 Master、一個或多個 Slave
- Mode 0 最常見,先用這個
- CS 要拉對,時脈不要太快
- 有懷疑就先上邏輯分析儀看波形
下一篇會講 ESP32 + STM32 I2C 通訊,敬請期待。
如果你覺得這篇文章有幫助,歡迎留言或分享。有任何問題也歡迎在下面提出。
文章評論