SPI(Serial Peripheral Interface)是一种广泛应用的高速、全双工、同步串行通信协议。本质上是主从结构的总线协议,适用于MCU与各种外设间的短距离高速通信。

SPI协议基础

硬件连接

SPI通信需要四根信号线:

信号 全称 方向 说明
SCLK Serial Clock Master→Slave 时钟信号,由主机产生
MOSI Master Out Slave In Master→Slave 主机发送数据
MISO Master In Slave Out Slave→Master 主机接收数据
CS/SS Chip Select / Slave Select Master→Slave 片选信号,选择通信的从设备
        MCU (Master)                Slave Device
        ==========                  =============
    +-------------+               +-------------+
    |         SCLK|------------->|SCLK         |
    |         MOSI|------------->|MOSI         |
    |         MISO|<-------------|MISO         |
    |          CS |------------->|CS           |
    +-------------+               +-------------+

多从设备连接

         MCU (Master)
         ==========
    +---------------+
    |           SCLK|-----------------------------------+
    |           MOSI|-----------------------------------+
    |           MISO|<----------------------------------+
    |            CS0 |-------------------+              |
    |            CS1 |--------------------------+       |
    |            CS2 |-----------------------------+   |
    +---------------+                                |  |
        |   |   |                                |   |  |
        v   v   v                                v   v  v
    +-----+-----+-----+                     +-----+-----+-----+
    | CS0 | CS1 | CS2 |                     | SS0 | SS1 | SS2 |
    +-----+-----+-----+                     +-----+-----+-----+
  • 独立片选模式:每从设备独占一个CS引脚,最多可连接n个从设备
  • 级联模式:多个从设备共享同一CS,数据线串联

SPI工作模式

SPI有四种工作模式,由时钟极性(CPOL)和时钟相位(CPHA)共同决定:

时钟极性 (CPOL)

CPOL 空闲时钟状态 时钟有效状态
0 低电平 高电平
1 高电平 低电平

时钟相位 (CPHA)

CPHA 数据采样时机 数据输出时机
0 第一个时钟边沿 第二个时钟边沿
1 第二个时钟边沿 第一个时钟边沿

四种模式详解

/*
 * 四种SPI模式
 *
 * CPOL=0, CPHA=0 (Mode 0)
 * - 空闲SCLK=0
 * - 第一个边沿(上升沿)采样
 * - 下降沿输出
 *
 * CPOL=0, CPHA=1 (Mode 1)
 * - 空闲SCLK=0
 * - 下降沿采样
 * - 上升沿输出
 *
 * CPOL=1, CPHA=0 (Mode 2)
 * - 空闲SCLK=1
 * - 下降沿采样
 * - 上升沿输出
 *
 * CPOL=1, CPHA=1 (Mode 3)
 * - 空闲SCLK=1
 * - 上升沿采样
 * - 下降沿输出
 */

// 常用模式0的时序
/*
 SCLK  __|---|___|---|___|---|___|---|___
        |   |   |   |   |   |   |   |
 MOSI   ---< D7 >--< D6 >--< D5 >--< D4 >---
             |   |   |   |   |   |   |   |
 MISO   ---< D7 >--< D6 >--< D5 >--< D4 >---

        ^       ^       ^       ^
      采样    采样    采样    采样
      (上升沿)
*/

模式选择指南

模式 CPOL CPHA 适用场景
Mode 0 0 0 最常用,大多数SPI器件默认模式
Mode 1 0 1 部分存储芯片
Mode 2 1 0 较少使用
Mode 3 1 1 高速通信,部分传感器

GPIO模拟SPI

当MCU硬件SPI不可用或需要更多控制时,可用GPIO模拟SPI协议:

/*
 * GPIO模拟SPI - 基于STM32 HAL
 */

// 引脚定义
#define SPI_GPIO_PORT  GPIOA
#define SPI_SCK_PIN    GPIO_PIN_5
#define SPI_MOSI_PIN    GPIO_PIN_7
#define SPI_MISO_PIN    GPIO_PIN_6
#define SPI_CS_PORT     GPIOA
#define SPI_CS_PIN      GPIO_PIN_4

// GPIO初始化
void SPI_GPIO_Init(void) {
    GPIO_InitTypeDef GPIO_InitStruct = {0};

    // 使能GPIO时钟
    __HAL_RCC_GPIOA_CLK_ENABLE();

    // SCK和MOSI配置为输出
    GPIO_InitStruct.Pin = SPI_SCK_PIN | SPI_MOSI_PIN;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
    HAL_GPIO_Init(SPI_GPIO_PORT, &GPIO_InitStruct);

    // MISO配置为输入
    GPIO_InitStruct.Pin = SPI_MISO_PIN;
    GPIO_InitStruct.Mode = GPIO_MODE_INPUT;
    GPIO_InitStruct.Pull = GPIO_NOPULL;
    HAL_GPIO_Init(SPI_GPIO_PORT, &GPIO_InitStruct);

    // CS配置为输出
    GPIO_InitStruct.Pin = SPI_CS_PIN;
    GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
    HAL_GPIO_Init(SPI_CS_PORT, &GPIO_InitStruct);

    // 初始状态:CS=1, SCK=0
    HAL_GPIO_WritePin(SPI_CS_PORT, SPI_CS_PIN, GPIO_PIN_SET);
    HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_SCK_PIN, GPIO_PIN_RESET);
}

// SPI模式0:CPOL=0, CPHA=0
void SPI_GPIO_Init_Mode0(void) {
    // Mode 0: 空闲SCK=0, 第一个边沿采样
    HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_SCK_PIN, GPIO_PIN_RESET);
}

// SPI模式3:CPOL=1, CPHA=1
void SPI_GPIO_Init_Mode3(void) {
    // Mode 3: 空闲SCK=1, 第二个边沿采样
    HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_SCK_PIN, GPIO_PIN_SET);
}

// 发送一个字节
void SPI_GPIO_SendByte(uint8_t data) {
    for (int i = 7; i >= 0; i--) {
        // 设置MOSI数据
        if (data & (1 << i)) {
            HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_MOSI_PIN, GPIO_PIN_SET);
        } else {
            HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_MOSI_PIN, GPIO_PIN_RESET);
        }

        // 产生时钟边沿 - Mode 0先采样再输出
        // 实际上Mode 0第一个边沿(上升沿)采样,所以先拉高SCK再拉低
        HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_SCK_PIN, GPIO_PIN_SET);  // 采样点
        delay_us(1);
        HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_SCK_PIN, GPIO_PIN_RESET); // 完成一位
        delay_us(1);
    }
}

// 接收一个字节
uint8_t SPI_GPIO_RecvByte(void) {
    uint8_t data = 0;

    for (int i = 7; i >= 0; i--) {
        // Mode 0: 上升沿采样
        HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_SCK_PIN, GPIO_PIN_SET);
        delay_us(1);

        if (HAL_GPIO_ReadPin(SPI_GPIO_PORT, SPI_MISO_PIN) == GPIO_PIN_SET) {
            data |= (1 << i);
        }

        HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_SCK_PIN, GPIO_PIN_RESET);
        delay_us(1);
    }

    return data;
}

// 全双工收发
uint8_t SPI_GPIO_Transfer(uint8_t data) {
    uint8_t received = 0;

    for (int i = 7; i >= 0; i--) {
        // 设置MOSI
        if (data & (1 << i)) {
            HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_MOSI_PIN, GPIO_PIN_SET);
        } else {
            HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_MOSI_PIN, GPIO_PIN_RESET);
        }

        // 时钟上升沿采样MISO
        HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_SCK_PIN, GPIO_PIN_SET);
        delay_us(1);

        if (HAL_GPIO_ReadPin(SPI_GPIO_PORT, SPI_MISO_PIN)) {
            received |= (1 << i);
        }

        // 时钟下降沿
        HAL_GPIO_WritePin(SPI_GPIO_PORT, SPI_SCK_PIN, GPIO_PIN_RESET);
        delay_us(1);
    }

    return received;
}

// 完整的SPI读写操作
uint16_t SPI_GPIO_WriteRead(uint8_t *tx_buf, uint8_t *rx_buf, uint16_t len) {
    HAL_GPIO_WritePin(SPI_CS_PORT, SPI_CS_PIN, GPIO_PIN_RESET);

    for (uint16_t i = 0; i < len; i++) {
        rx_buf[i] = SPI_GPIO_Transfer(tx_buf[i]);
    }

    HAL_GPIO_WritePin(SPI_CS_PORT, SPI_CS_PIN, GPIO_PIN_SET);

    return len;
}

硬件SPI配置

使用MCU内置的SPI控制器,效率更高:

/*
 * STM32 HAL库硬件SPI配置
 */

// SPI句柄
SPI_HandleTypeDef hspi1;

// SPI1初始化 (APB2时钟 84MHz)
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;   // 8位数据
    hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;  // CPOL=0
    hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;      // CPHA=0, Mode 0
    hspi1.Init.NSS = SPI_NSS_SOFT;             // 软件片选
    hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8; // 84/8=10.5MHz
    hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;    // 高位先行
    hspi1.Init.TIMode = SPI_TIMODE_DISABLE;     // 非TI模式
    hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
    hspi1.Init.CRCPolynomial = 7;

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

// SPI引脚配置 (复用功能)
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();

        // PA5: SCK, PA6: MISO, PA7: MOSI
        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);
    }
}

典型外设驱动实现

Flash存储芯片驱动 (W25Qxx)

/*
 * W25Q64 Flash芯片驱动 (SPI接口)
 * 容量: 8MB (64Mbit)
 */

#define W25Q_OK         0
#define W25Q_ERROR      1

// W25Qxx命令
#define W25Q_READ_STATUS_REG1    0x05
#define W25Q_WRITE_ENABLE         0x06
#define W25Q_WRITE_DISABLE        0x04
#define W25Q_READ_DATA           0x03
#define W25Q_FAST_READ           0x0B
#define W25Q_PAGE_PROGRAM         0x02
#define W25Q_SECTOR_ERASE_4KB    0x20
#define W25Q_BLOCK_ERASE_64KB    0xD8
#define W25Q_CHIP_ERASE          0xC7
#define W25Q_JEDEC_ID            0x9F

typedef struct {
    SPI_HandleTypeDef *hspi;
    GPIO_TypeDef *cs_port;
    uint16_t cs_pin;
    uint8_t status;
} W25Qxx_HandleTypeDef;

W25Qxx_HandleTypeDef g_w25q;

// 初始化
int W25Q_Init(SPI_HandleTypeDef *hspi, GPIO_TypeDef *cs_port, uint16_t cs_pin) {
    g_w25q.hspi = hspi;
    g_w25q.cs_port = cs_port;
    g_w25q.cs_pin = cs_pin;

    HAL_GPIO_WritePin(cs_port, cs_pin, GPIO_PIN_SET);

    // 读取ID验证通信
    uint8_t id[3];
    W25Q_ReadJEDECID(id);

    if (id[0] != 0xEF || id[1] != 0x40 || id[2] != 0x17) {
        return W25Q_ERROR;
    }

    return W25Q_OK;
}

// 等待Flash空闲
void W25Q_WaitBusy(void) {
    uint8_t status;
    do {
        W25Q_ReadStatus(&status, 1);
    } while (status & 0x01);
}

// 读取状态寄存器
void W25Q_ReadStatus(uint8_t *status, uint8_t len) {
    HAL_GPIO_WritePin(g_w25q.cs_port, g_w25q.cs_pin, GPIO_PIN_RESET);

    uint8_t cmd = W25Q_READ_STATUS_REG1;
    HAL_SPI_Transmit(g_w25q.hspi, &cmd, 1, 100);
    HAL_SPI_Receive(g_w25q.hspi, status, len, 100);

    HAL_GPIO_WritePin(g_w25q.cs_port, g_w25q.cs_pin, GPIO_PIN_SET);
}

// 发送命令
void W25Q_SendCommand(uint8_t cmd) {
    HAL_GPIO_WritePin(g_w25q.cs_port, g_w25q.cs_pin, GPIO_PIN_RESET);
    HAL_SPI_Transmit(g_w25q.hspi, &cmd, 1, 100);
}

// 读取JEDEC ID
void W25Q_ReadJEDECID(uint8_t *id) {
    W25Q_SendCommand(W25Q_JEDEC_ID);
    HAL_SPI_Receive(g_w25q.hspi, id, 3, 100);
    HAL_GPIO_WritePin(g_w25q.cs_port, g_w25q.cs_pin, GPIO_PIN_SET);
}

// 使能写操作
void W25Q_WriteEnable(void) {
    W25Q_SendCommand(W25Q_WRITE_ENABLE);
    HAL_GPIO_WritePin(g_w25q.cs_port, g_w25q.cs_pin, GPIO_PIN_SET);
}

// 扇区擦除 (4KB)
int W25Q_EraseSector(uint32_t addr) {
    W25Q_WaitBusy();
    W25Q_WriteEnable();

    uint8_t cmd[4] = {W25Q_SECTOR_ERASE_4KB,
                      (addr >> 16) & 0xFF,
                      (addr >> 8) & 0xFF,
                      addr & 0xFF};

    HAL_GPIO_WritePin(g_w25q.cs_port, g_w25q.cs_pin, GPIO_PIN_RESET);
    HAL_SPI_Transmit(g_w25q.hspi, cmd, 4, 100);
    HAL_GPIO_WritePin(g_w25q.cs_port, g_w25q.cs_pin, GPIO_PIN_SET);

    W25Q_WaitBusy();
    return W25Q_OK;
}

// 页编程 (256字节)
int W25Q_PageProgram(uint32_t addr, uint8_t *data, uint32_t len) {
    if (len > 256) return W25Q_ERROR;

    W25Q_WaitBusy();
    W25Q_WriteEnable();

    uint8_t cmd[4] = {W25Q_PAGE_PROGRAM,
                      (addr >> 16) & 0xFF,
                      (addr >> 8) & 0xFF,
                      addr & 0xFF};

    HAL_GPIO_WritePin(g_w25q.cs_port, g_w25q.cs_pin, GPIO_PIN_RESET);
    HAL_SPI_Transmit(g_w25q.hspi, cmd, 4, 100);
    HAL_SPI_Transmit(g_w25q.hspi, data, len, 100);
    HAL_GPIO_WritePin(g_w25q.cs_port, g_w25q.cs_pin, GPIO_PIN_SET);

    W25Q_WaitBusy();
    return W25Q_OK;
}

// 读取数据
int W25Q_ReadData(uint32_t addr, uint8_t *buf, uint32_t len) {
    W25Q_WaitBusy();

    uint8_t cmd[4] = {W25Q_READ_DATA,
                      (addr >> 16) & 0xFF,
                      (addr >> 8) & 0xFF,
                      addr & 0xFF};

    HAL_GPIO_WritePin(g_w25q.cs_port, g_w25q.cs_pin, GPIO_PIN_RESET);
    HAL_SPI_Transmit(g_w25q.hspi, cmd, 4, 100);
    HAL_SPI_Receive(g_w25q.hspi, buf, len, 100);
    HAL_GPIO_WritePin(g_w25q.cs_port, g_w25q.cs_pin, GPIO_PIN_SET);

    return W25Q_OK;
}

常见问题与解决方案

1. 时钟速率过高

/*
 * 问题:时钟速率过高导致数据错乱
 *
 * 原因:
 * - MCU时钟过快
 * - 线缆过长
 * - 从设备不支持高速
 *
 * 解决:根据从设备最高支持频率降低波特率
 */

// STM32分频设置
#define SPI_BAUDRATEPRESCALER_2      0x00   // 42MHz (最快)
#define SPI_BAUDRATEPRESCALER_4      0x08   // 21MHz
#define SPI_BAUDRATEPRESCALER_8      0x10   // 10.5MHz
#define SPI_BAUDRATEPRESCALER_16     0x18   // 5.25MHz
#define SPI_BAUDRATEPRESCALER_256    0x70   // ~328KHz (最慢)

// 根据从设备选择合适分频
hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_8; // 10.5MHz通常安全

2. 模式不匹配

/*
 * 问题:读取数据全为0xFF或错误值
 *
 * 原因:主机与从机的CPOL/CPHA不匹配
 *
 * 解决:仔细阅读从设备数据手册,确认正确模式
 */

// 常见设备的SPI模式
/*
 * W25Qxx Flash:    Mode 0 或 Mode 3 (支持两种)
 * SD卡:            Mode 0
 * NRF24L01:        Mode 0
 * OLED (SSD1306):   Mode 0
 * ADXL345:         Mode 3
 * MCP3008:          Mode 0
 */

// 调试建议:先用示波器观察SCLK和MOSI/MISO波形

3. CS片选时序问题

/*
 * 问题:片选时序不对导致通信失败
 *
 * 常见问题:
 * - 片选提前拉高
 * - 片选持续时间不够
 * - 多字节传输时片选中途释放
 */

// 正确做法:整个传输过程保持CS低
void SPI_TransferFull(uint8_t *tx_buf, uint8_t *rx_buf, uint16_t len) {
    HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_RESET);  // CS低

    HAL_SPI_TransmitReceive(&hspi1, tx_buf, rx_buf, len, 1000);

    // 等待传输完成
    while (HAL_SPI_GetState(&hspi1) != HAL_SPI_STATE_READY);

    HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_SET);   // CS高
}

// 反面例子:多字节分开传输
void BAD_SpiTransfer(uint8_t *data, uint16_t len) {
    for (uint16_t i = 0; i < len; i++) {
        HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_RESET);
        HAL_SPI_Transmit(&hspi1, &data[i], 1, 100);
        HAL_GPIO_WritePin(CS_PORT, CS_PIN, GPIO_PIN_SET);  // 错误!每字节CS都释放
    }
}

4. 数据对齐问题

/*
 * 问题:16位/32位数据通信时高低字节错位
 *
 * 原因:大小端(MSB/LSB)设置不一致
 *
 * 解决:统一设置为高位先行(MSB First)
 */

hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;  // 高位先行

// 验证方法:发送0x80,观察第一位是否正确
uint8_t test = 0x80;  // 二进制: 10000000
// 如果第一位是1,则MSB优先正确

性能优化

DMA加速

/*
 * 使用DMA进行SPI传输,减少CPU干预
 */

SPI_HandleTypeDef hspi1;
DMA_HandleTypeDef hdma_spi1_tx;
DMA_HandleTypeDef hdma_spi1_rx;

// DMA初始化
void SPI_DMA_Init(void) {
    __HAL_RCC_DMA2_CLK_ENABLE();

    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_MEDIUM;

    HAL_DMA_Init(&hdma_spi1_tx);
    __HAL_LINKDMA(&hspi1, hdmatx, hdma_spi1_tx);

    // 同样配置RX DMA...
}

// DMA传输
HAL_StatusTypeDef SPI_TransmitDMA(uint8_t *data, uint16_t len) {
    return HAL_SPI_Transmit_DMA(&hspi1, data, len);
}

// DMA完成回调
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) {
    // 传输完成,可以通知其他任务
}

总结

使用SPI协议的关键点:

  1. 确认工作模式:仔细阅读从设备数据手册,选择正确的CPOL和CPHA
  2. 控制时钟速率:确保不超过从设备支持的最大频率
  3. 注意片选时序:整个传输过程保持CS有效
  4. 大小端一致:主机与从机的位顺序要匹配
  5. 错误处理:实现超时检测和错误恢复机制

希望这篇深度解析能帮助你更好地掌握SPI通信协议!