Skip to main content

STM32 教學系列 第 9 節:SPI 通訊 + DMA

🎯 學習目標

  1. 理解 SPI 高速序列通訊 - 掌握主從模式與時序配置
  2. 實現 SPI 感測器通訊 - 與加速度計、存儲卡等設備通訊
  3. 配置 SPI + DMA - 實現高速資料轉移

🎓 SPI 基礎概念

SPI 是什麼?

SPI (Serial Peripheral Interface) 是高速同步序列通訊協定。特點:

  • 速度快:最高 54 MHz(STM32F446)
  • 同步通訊:使用時鐘信號同步
  • 全雙工:可同時收發
  • 主從結構:一主多從

SPI 的 4 條信號線

訊號 名稱 方向 功能
MOSI Master Out Slave In 主 → 從 主發送、從接收
MISO Master In Slave Out 從 → 主 從發送、主接收
SCK Serial Clock 主 → 從 時脈(由主產生)
CS/NSS Chip Select 主 → 從 晶片選擇(低有效)

SPI 時序圖

CS ╲_____________________________╱
    
SCK ╲__╱‾╲_╱‾╲_╱‾╲_╱‾╲_╱‾╲_╱‾╲_

    B7  B6  B5  B4  B3  B2  B1  B0
MOSI ─X─┬─X─┬─X─┬─X─┬─X─┬─X─┬─X─┬─X─
       └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─

MISO ─X─┬─X─┬─X─┬─X─┬─X─┬─X─┬─X─┬─X─
       └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─┘ └─

STM32F446 SPI 資源

特性 規格
SPI 模組 3 個(SPI1、SPI2、SPI3)
最高速度 54 MHz(SPI1)、27 MHz(SPI2/3)
DMA 支援 是(分別用於收發)
模式 SPI、I²S、Simplex(單向)

🛠️ 硬體接線

所需元件

元件 數量 說明
SPI 從設備(MPU6050、SD卡等) 1 任選
跳線 4 MOSI、MISO、SCK、CS

接線圖

Nucleo 接腳 SPI1 功能 從設備連接
PA5 SCK 時鐘 SCL 或 SCK
PA6 MISO 接收 MISO 或 DO
PA7 MOSI 發送 MOSI 或 DIN
PA4 CS 選擇 CS 或 CE

麵包板接線參考

Nucleo F446RC          從設備(如 MPU6050)
─────────────          ───────────────
PA5 (SCK) ──────────── SCL
PA6 (MISO) ──────────── SDA/MISO
PA7 (MOSI) ──────────── MOSI
PA4 (CS)  ──────────── CS
3.3V ──────────────── VCC
GND ──────────────── GND

⚙️ CubeMX 配置步驟

步驟 1:啟用 SPI1

  1. 開啟 CubeMX
  2. 在 Pinout 圖中選擇以下接腳:
    • PA5:配置為 SPI1_SCK
    • PA6:配置為 SPI1_MISO
    • PA7:配置為 SPI1_MOSI
    • PA4:配置為 GPIO_Output(用於 CS 控制)

步驟 2:配置 SPI 參數

  1. 在左側選擇 ConnectivitySPI1
  2. Mode:Full-Duplex Master
  3. Configuration 中設定:
    • Frame Format:Motorola(通常使用)
    • Data Size:8 Bits
    • Prescaler:8(設定 SPI 速度 = 168MHz / 8 = 21 MHz)
    • CPOL (Clock Polarity):Low
    • CPHA (Clock Phase):1 Edge(根據從設備選擇)
    • NSS (Chip Select Mode):Software(手動控制 CS)

步驟 3:配置 DMA(用於高速傳輸)

  1. DMA Settings 中:

    • Add → 配置 Tx DMA:DMA2 Stream3 Channel3
    • Add → 配置 Rx DMA:DMA2 Stream2 Channel3
  2. 分別設定:

    • Mode:Normal
    • Increment Address:Enable (Memory)
    • Data Width:Byte

步驟 4:啟用中斷

NVIC Settings 中勾選:

  • SPI1 global interrupt
  • DMA2 Stream2 global interrupt(Rx)
  • DMA2 Stream3 global interrupt(Tx)

步驟 5:生成程式碼

點擊 Generate Code


💻 完整程式碼

main.c - SPI DMA 通訊

/* STM32 Lesson 09 - SPI with DMA
 * 功能:使用 SPI + DMA 進行高速通訊
 * 難度:高級
 */

#include "main.h"
#include "spi.h"
#include "dma.h"
#include "usart.h"
#include "gpio.h"
#include <stdio.h>
#include <string.h>

/* 定義 */
#define SPI_TX_BUFFER_SIZE 256
#define SPI_RX_BUFFER_SIZE 256
#define CS_PORT GPIOA
#define CS_PIN GPIO_PIN_4

/* 函數宣告 */
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
static void MX_SPI1_Init(void);
static void MX_DMA_Init(void);
static void MX_USART1_UART_Init(void);
void UART_Print(const char *format, ...);
void SPI_CS_Enable(void);
void SPI_CS_Disable(void);
void SPI_Transmit_DMA(uint8_t *tx_data, uint16_t size);
void SPI_Receive_DMA(uint8_t *rx_data, uint16_t size);

/* 全域變數 */
SPI_HandleTypeDef hspi1;
DMA_HandleTypeDef hdma_spi1_rx;
DMA_HandleTypeDef hdma_spi1_tx;
UART_HandleTypeDef huart1;

uint8_t spi_tx_buffer[SPI_TX_BUFFER_SIZE];
uint8_t spi_rx_buffer[SPI_RX_BUFFER_SIZE];

volatile uint8_t spi_tx_complete = 0;
volatile uint8_t spi_rx_complete = 0;

int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_DMA_Init();
  MX_SPI1_Init();
  MX_USART1_UART_Init();

  UART_Print("\r\n=== SPI DMA Test ===\r\n");

  /* 準備發送資料 */
  for (uint16_t i = 0; i < 8; i++)
  {
    spi_tx_buffer[i] = 0xA0 + i;  /* 填充測試資料 */
  }

  while (1)
  {
    UART_Print("Sending via SPI DMA...\r\n");
    
    /* 使用 DMA 發送 */
    SPI_Transmit_DMA(spi_tx_buffer, 8);
    
    /* 等待完成 */
    while (!spi_tx_complete);
    spi_tx_complete = 0;
    
    UART_Print("Sent: ");
    for (uint16_t i = 0; i < 8; i++)
    {
      UART_Print("0x%02X ", spi_tx_buffer[i]);
    }
    UART_Print("\r\n");

    HAL_Delay(1000);
  }
}

/**
  * @brief SPI 片選使能(拉低)
  */
void SPI_CS_Enable(void)
{
  HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_RESET);
  HAL_Delay(1);  /* 等待片選穩定 */
}

/**
  * @brief SPI 片選禁用(拉高)
  */
void SPI_CS_Disable(void)
{
  HAL_Delay(1);  /* 等待最後一位傳完 */
  HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_SET);
}

/**
  * @brief 使用 DMA 發送資料
  */
void SPI_Transmit_DMA(uint8_t *tx_data, uint16_t size)
{
  SPI_CS_Enable();
  HAL_SPI_Transmit_DMA(&hspi1, tx_data, size);
}

/**
  * @brief 使用 DMA 接收資料
  */
void SPI_Receive_DMA(uint8_t *rx_data, uint16_t size)
{
  SPI_CS_Enable();
  HAL_SPI_Receive_DMA(&hspi1, rx_data, size);
}

/**
  * @brief SPI 發送完成中斷回調
  */
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi)
{
  if (hspi->Instance == SPI1)
  {
    spi_tx_complete = 1;
    SPI_CS_Disable();
  }
}

/**
  * @brief SPI 接收完成中斷回調
  */
void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi)
{
  if (hspi->Instance == SPI1)
  {
    spi_rx_complete = 1;
    SPI_CS_Disable();
  }
}

/**
  * @brief UART 列印函數
  */
void UART_Print(const char *format, ...)
{
  char buffer[100];
  va_list args;
  va_start(args, format);
  vsnprintf(buffer, sizeof(buffer), format, args);
  va_end(args);
  
  HAL_UART_Transmit(&huart1, (uint8_t *)buffer, strlen(buffer), HAL_MAX_DELAY);
}

/**
  * @brief 系統時脈配置
  */
void SystemClock_Config(void)
{
  RCC_OscInitTypeDef RCC_OscInitStruct = {0};
  RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};

  __HAL_RCC_PWR_CLK_ENABLE();
  __HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE1);

  RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
  RCC_OscInitStruct.HSEState = RCC_HSE_ON;
  RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
  RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
  RCC_OscInitStruct.PLL.PLLM = 8;
  RCC_OscInitStruct.PLL.PLLN = 336;
  RCC_OscInitStruct.PLL.PLLP = 2;
  RCC_OscInitStruct.PLL.PLLQ = 7;

  if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
  {
    Error_Handler();
  }

  RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_SYSCLK
                              | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2;
  RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
  RCC_ClkInitStruct.AHBCLKDivider = RCC_SYSCLK_DIV1;
  RCC_ClkInitStruct.APB1CLKDivider = RCC_HCLK_DIV4;
  RCC_ClkInitStruct.APB2CLKDivider = RCC_HCLK_DIV2;

  if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_5) != HAL_OK)
  {
    Error_Handler();
  }
}

/**
  * @brief GPIO 初始化
  */
static void MX_GPIO_Init(void)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};

  __HAL_RCC_GPIOA_CLK_ENABLE();

  /* 配置 PA4 為 CS 輸出 */
  GPIO_InitStruct.Pin = GPIO_PIN_4;
  GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
  GPIO_InitStruct.Pull = GPIO_NOPULL;
  GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
  HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);
  
  /* 初始化 CS 為高電位(未選擇) */
  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);
}

/**
  * @brief SPI1 初始化
  */
static void MX_SPI1_Init(void)
{
  hspi1.Instance = SPI1;
  hspi1.Init.Mode = SPI_MODE_MASTER;
  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_SOFT;
  hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8;
  hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
  hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
  hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
  hspi1.Init.CRCPolynomial = 10;

  if (HAL_SPI_Init(&hspi1) != HAL_OK)
  {
    Error_Handler();
  }
}

/**
  * @brief DMA 初始化
  */
static void MX_DMA_Init(void)
{
  __HAL_RCC_DMA2_CLK_ENABLE();
  
  HAL_NVIC_SetPriority(DMA2_Stream2_IRQn, 1, 0);
  HAL_NVIC_EnableIRQ(DMA2_Stream2_IRQn);
  
  HAL_NVIC_SetPriority(DMA2_Stream3_IRQn, 1, 0);
  HAL_NVIC_EnableIRQ(DMA2_Stream3_IRQn);
}

/**
  * @brief USART1 初始化
  */
static void MX_USART1_UART_Init(void)
{
  huart1.Instance = USART1;
  huart1.Init.BaudRate = 115200;
  huart1.Init.WordLength = UART_WORDLENGTH_8B;
  huart1.Init.StopBits = UART_STOPBITS_1;
  huart1.Init.Parity = UART_PARITY_NONE;
  huart1.Init.Mode = UART_MODE_TX_RX;

  if (HAL_UART_Init(&huart1) != HAL_OK)
  {
    Error_Handler();
  }
}

/**
  * @brief SPI1 MSP 初始化
  */
void HAL_SPI_MspInit(SPI_HandleTypeDef* hspi)
{
  GPIO_InitTypeDef GPIO_InitStruct = {0};

  if(hspi->Instance == SPI1)
  {
    __HAL_RCC_SPI1_CLK_ENABLE();
    __HAL_RCC_GPIOA_CLK_ENABLE();

    GPIO_InitStruct.Pin = GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7;
    GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
    GPIO_InitStruct.Alternate = GPIO_AF5_SPI1;
    HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

    hdma_spi1_rx.Instance = DMA2_Stream2;
    hdma_spi1_rx.Init.Channel = DMA_CHANNEL_3;
    hdma_spi1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY;
    hdma_spi1_rx.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_spi1_rx.Init.MemInc = DMA_MINC_ENABLE;
    hdma_spi1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
    hdma_spi1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
    hdma_spi1_rx.Init.Mode = DMA_NORMAL;
    hdma_spi1_rx.Init.Priority = DMA_PRIORITY_HIGH;
    HAL_DMA_Init(&hdma_spi1_rx);

    __HAL_LINKDMA(hspi, hdmarx, hdma_spi1_rx);

    hdma_spi1_tx.Instance = DMA2_Stream3;
    hdma_spi1_tx.Init.Channel = DMA_CHANNEL_3;
    hdma_spi1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
    hdma_spi1_tx.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_spi1_tx.Init.MemInc = DMA_MINC_ENABLE;
    hdma_spi1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
    hdma_spi1_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
    hdma_spi1_tx.Init.Mode = DMA_NORMAL;
    hdma_spi1_tx.Init.Priority = DMA_PRIORITY_HIGH;
    HAL_DMA_Init(&hdma_spi1_tx);

    __HAL_LINKDMA(hspi, hdmatx, hdma_spi1_tx);

    HAL_NVIC_SetPriority(SPI1_IRQn, 1, 0);
    HAL_NVIC_EnableIRQ(SPI1_IRQn);
  }
}

/**
  * @brief SPI1 中斷處理
  */
void SPI1_IRQHandler(void)
{
  HAL_SPI_IRQHandler(&hspi1);
}

/**
  * @brief DMA2 Stream2 中斷處理
  */
void DMA2_Stream2_IRQHandler(void)
{
  HAL_DMA_IRQHandler(&hdma_spi1_rx);
}

/**
  * @brief DMA2 Stream3 中斷處理
  */
void DMA2_Stream3_IRQHandler(void)
{
  HAL_DMA_IRQHandler(&hdma_spi1_tx);
}

void Error_Handler(void)
{
  while(1);
}

🔍 測試與除錯

預期結果

通訊驗證

  • UART 輸出 "Sending via SPI DMA..."
  • 每秒列印一次發送的資料(0xA0 ~ 0xA7)
  • LED 應正常工作(如有連接)

常見問題

問題 原因 解決方案
無 UART 輸出 UART 未初始化 檢查 MX_USART1_UART_Init()
SPI 無反應 DMA 配置錯誤 驗證 DMA Stream 和 Channel 設定
傳輸速度慢 Prescaler 過大 減小 Prescaler 值提高速度
資料錯誤 CPOL/CPHA 不匹配 根據從設備調整時序

📚 進階應用

SPI 與 SD 卡通訊

/* SD 卡初始化 */
#define SD_CS_PORT GPIOA
#define SD_CS_PIN GPIO_PIN_4

void SD_Init(void)
{
  SPI_CS_Enable();
  /* 發送初始化命令 CMD0 */
  uint8_t cmd[6] = {0x40, 0x00, 0x00, 0x00, 0x00, 0x95};
  HAL_SPI_Transmit(&hspi1, cmd, 6, 100);
  SPI_CS_Disable();
}

🔗 延伸學習

下節預覽 - 第 10 節:CANbus 通訊(兩板間通訊)

第 10 節將實現:

  • CAN 協定與識別符
  • TJA1050 收發器接線
  • Nucleo ↔ Nucleo 雙板通訊

✨ SPI 高速通訊完成!🚀