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

🎯 學習目標 
 
 理解 SPI 高速序列通訊 - 掌握主從模式與時序配置 
 實現 SPI 感測器通訊 - 與加速度計、存儲卡等設備通訊 
 配置 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 
 
 開啟 CubeMX 
 在 Pinout 圖中選擇以下接腳：
 
 PA5 ：配置為 SPI1_SCK 
 PA6 ：配置為 SPI1_MISO 
 PA7 ：配置為 SPI1_MOSI 
 PA4 ：配置為 GPIO_Output （用於 CS 控制） 
 
 
 
 步驟 2：配置 SPI 參數 
 
 在左側選擇 Connectivity → SPI1 
 Mode ：Full-Duplex Master 
 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（用於高速傳輸） 
 
 
 在 DMA Settings 中： 
 
 Add → 配置 Tx DMA：DMA2 Stream3 Channel3 
 Add → 配置 Rx DMA：DMA2 Stream2 Channel3 
 
 
 
 分別設定： 
 
 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 高速通訊完成！🚀