
前言:SPI 是嵌入式開發的必修課,但它其實不難
SPI 由 Motorola 在 1980 年代提出,定位很明確:全雙工、同步、主從式的通訊。它用四條線解決問題,換來的是比 I2C 快得多的速度(可達 40 MHz 以上)。
缺點也很老實:每多一個 Slave 就多一條 CS 線。所以常見的配置是「一個 Master、少數幾個 Slave」,而不是像 I2C 那樣掛一整排。
四條線解決什麼問題
| 訊號線 | 全名 | 作用 |
|---|---|---|
| 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 | 片選,低電位有效 |
把方向畫出來就更容易理解為什麼它叫「全雙工」:

時序圖:Mode 0 的一個 byte
Mode 0(CPOL = 0、CPHA = 0)是最常見的組合,看懂它就懂八成了:

四個觀察重點:CS 拉低表示開始、SCLK 由 Master 產生、MOSI 在每個上升緣送出一個 bit(從 D7 開始)、MISO 在同一個上升緣同步送回資料。一個 byte 就是 8 個時脈週期。

四種 Mode:CPOL 與 CPHA 的組合
| 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 環境下的 SPI Master 寫起來很短,重點只有「設定」與「選取 Slave」兩件事:
#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
Slave 端用 CubeMX 配置成 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(Master) | → | STM32(Slave) |
|---|---|---|
| GPIO18 (SCK) | → | PA5 (SCK) |
| GPIO23 (MOSI) | → | PA7 (MOSI) |
| GPIO19 (MISO) | → | PA6 (MISO) |
| GPIO5 (CS) | → | PA4 (NSS) |
| GND | → | GND |

這一段看起來像廢話,但 SPI 的問題有一半在這裡:MOSI 接 MOSI、MISO 接 MISO、務必共地、確認兩邊邏輯電壓相同。
用邏輯分析儀驗證
時序圖本身可以用 Python + WaveDrom 產生,先定義再驗證:
{
"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"]}
]
}
接上邏輯分析儀(Saleae、Kingst、DSLogic 等)之後,你應該看到:

四個常見問題

其中 Q3 最隱蔽:STM32 的 NSS 在 Slave 模式下必須設為 Hardware Input。設成 Software 的話,硬體不會自動識別 CS 訊號,你會覺得「明明線都對,它就是不反應」。
總結
SPI 的知識點其實只有幾條,但每一條都會在實作時咬人:
- 四條線、一個 Master:SCLK、MOSI、MISO、每個 Slave 一條 CS
- 全雙工:送與收共用同一組時脈,同時發生
- Mode 0 是預設:90% 的感測器都用它,不確定就先試這個
- CS 要拉對:收到全 0xFF 或全 0x00 時,第一個要查的就是它
- 時脈別亂催:麵包板超過 20 cm 就開始不穩,必要時降到 500 kHz
把這五點記住,剩下的就只剩查 datasheet 上的 Mode 與時序參數了。
文章評論