0x6A Logbook

0x6A Logbook
Shi6a的筆記本
  1. 首頁
  2. 自動化技巧
  3. 正文

ESP32 與 STM32 SPI 通訊實戰:從時序圖看懂資料傳輸

2026 年 5 月 21 日 7點熱度 0人點贊 0條評論

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):

SPI Mode 0 時序圖
圖 1: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 通訊,敬請期待。


如果你覺得這篇文章有幫助,歡迎留言或分享。有任何問題也歡迎在下面提出。

標籤: 教學
最後更新:2026 年 5 月 21 日

shi6a

這個人很懶,什麼都沒留下

點贊
下一篇 >

文章評論

razz evil exclaim smile redface biggrin eek confused idea lol mad twisted rolleyes wink cool arrow neutral cry mrgreen drooling persevering
取消回覆

COPYRIGHT © 2026 0x6A Logbook. ALL RIGHTS RESERVED.

Theme Kratos Made By Seaton Jiang