一、SPI 协议基础与主从模式

SPI(Serial Peripheral Interface)由 Motorola 提出,是一种高速全双工同步串行总线。它通常使用 4 根信号线:

  • SCK:时钟线,由主设备产生,决定通信速率与同步节奏。
  • MOSI:主输出从输入(Master Out Slave In)。
  • MISO:主输入从输出(Master In Slave Out)。
  • CS:片选信号,低电平有效,由主设备控制选择从设备。

SPI 采用主从模式:一个主设备可以挂多个从设备,但同一时刻只能有一个从设备被选中(通过 CS 引脚)。主设备始终控制 SCK 和 CS,完成数据的发送与接收。

全双工通信意味着主设备发送数据的同时也能接收数据。SPI 没有定义应用层协议,因此读写具体外设(如 Flash、传感器)时必须遵循外设的数据手册时序。

二、SPI 时序:极性(CPOL)与相位(CPHA)

SPI 的四种工作模式由时钟极性 CPOL时钟相位 CPHA 决定:

  • CPOL:决定 SCK 空闲时的电平。CPOL=0 时空闲为低电平,CPOL=1 时空闲为高电平。
  • CPHA:决定数据采样边沿。CPHA=0 时在第一个时钟边沿采样(数据更新在边沿前),CPHA=1 时在第二个边沿采样。

两种参数组合成 Mode 0~Mode 3。

| CPOL | CPHA | 模式 | 采样边沿 | 常见应用 | |------|------|------|----------|----------| | 0 | 0 | Mode 0 | 上升沿 | W25Q64 等 Flash | | 0 | 1 | Mode 1 | 下降沿 | - | | 1 | 0 | Mode 2 | 下降沿 | - | | 1 | 1 | Mode 3 | 上升沿 | 部分传感器 |

注意:主从设备的 CPOL/CPHA 必须一致,否则通信数据会错位。

三、STM32 SPI 配置步骤

以 STM32F1 系列 + HAL 库为例,配置 SPI 的典型步骤:

  1. 使能 GPIO 和 SPI 时钟
  2. 配置 GPIO 复用功能,将 SCK、MOSI、MISO 映射到 SPI 引脚,CS 通常用普通 GPIO 控制。
  3. 配置 SPI 句柄:设置波特率、时钟极性/相位、数据格式等。
  4. 调用 HAL_SPI_Init() 初始化
  5. 编写读写函数,使用 HAL_SPI_Transmit/Receive 进行数据传输。

使用 STM32CubeMX 可快速完成时钟和引脚配置,但底层原理仍需理解。以下代码展示 HAL 库初始化:

SPI_HandleTypeDef hspi1;

void SPI_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_16; // 分频
    hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;     // 高位先行
    HAL_SPI_Init(&hspi1);
}

CS 引脚控制建议使用普通 GPIO,例如:

#define FLASH_CS_LOW()   HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET)
#define FLASH_CS_HIGH()  HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET)

四、读写 Flash 实战(以 W25Q64 为例)

W25Q64 是一款常见的 SPI NOR Flash,容量 64Mbit(8MB),支持标准 SPI 指令。对 Flash 的操作通常先发送指令地址,再读写数据。常用指令:

  • 读 ID:0x90(读取制造商/设备 ID)
  • 写使能:0x06
  • 读数据:0x03
  • 页编程:0x02(一页最多 256 字节)
  • 擦除扇区:0x20(一次擦除 4KB)

下面给出基于 HAL 库的完整示例代码。

/* 发送一个字节同时接收一个字节 */
uint8_t SPI_ExchangeByte(uint8_t txData)
{
    uint8_t rxData = 0;
    HAL_SPI_TransmitReceive(&hspi1, &txData, &rxData, 1, HAL_MAX_DELAY);
    return rxData;
}

/* 读取 Flash ID */
uint16_t Flash_ReadID(void)
{
    uint8_t cmd[4] = {0x90, 0x00, 0x00, 0x00};
    uint8_t id[2] = {0};
    FLASH_CS_LOW();
    HAL_SPI_Transmit(&hspi1, cmd, 4, HAL_MAX_DELAY);
    HAL_SPI_Receive(&hspi1, id, 2, HAL_MAX_DELAY);
    FLASH_CS_HIGH();
    return (id[0] << 8) | id[1]; // W25Q64 返回 0xEF17
}

/* 写使能 */
void Flash_WriteEnable(void)
{
    uint8_t cmd = 0x06;
    FLASH_CS_LOW();
    HAL_SPI_Transmit(&hspi1, &cmd, 1, HAL_MAX_DELAY);
    FLASH_CS_HIGH();
}

/* 读取指定地址数据 */
void Flash_ReadData(uint32_t addr, uint8_t *buf, uint32_t len)
{
    uint8_t cmd[4] = {0x03, (addr >> 16) & 0xFF, (addr >> 8) & 0xFF, addr & 0xFF};
    FLASH_CS_LOW();
    HAL_SPI_Transmit(&hspi1, cmd, 4, HAL_MAX_DELAY);
    HAL_SPI_Receive(&hspi1, buf, len, HAL_MAX_DELAY);
    FLASH_CS_HIGH();
}

/* 页编程(写一页) */
void Flash_PageProgram(uint32_t addr, uint8_t *data, uint16_t len)
{
    uint8_t cmd[4] = {0x02, (addr >> 16) & 0xFF, (addr >> 8) & 0xFF, addr & 0xFF};
    Flash_WriteEnable();
    FLASH_CS_LOW();
    HAL_SPI_Transmit(&hspi1, cmd, 4, HAL_MAX_DELAY);
    HAL_SPI_Transmit(&hspi1, data, len, HAL_MAX_DELAY);
    FLASH_CS_HIGH();
    // 可等待 Flash 内部完成(读取状态寄存器)
}

使用时注意:Flash 写前必须先擦除,且页编程不能跨页。实际应用中还需添加状态寄存器的轮询等待,以确保操作完成。

五、注意事项

  • 时钟极性和相位必须匹配:错误的 CPOL/CPHA 会导致数据错位,调试时可用逻辑分析仪观察。
  • 片选时序:多数 Flash 要求 CS 在指令发送期间保持低电平,完成后拉高,不能随意翻转。
  • 波特率匹配:SPI 频率不要超过外设上限(W25Q64 最快可达 150MHz,但 STM32 时钟要配置合理)。
  • DMA 传输:大数据读写建议使用 HAL_SPI_TransmitReceive_DMA 提高效率,注意 DMA 与 CPU 的同步。
  • 引脚复用:使用 STM32CubeMX 时,确保 GPIO 复用为 SPI 功能,否则数据发不出去。
  • Flash 扇区擦除:W25Q64 需先擦后写,且擦除等待时间较长,务必加延时或状态轮询。

掌握 SPI 主从模式与时序后,无论是连接 Flash、SD 卡还是其他传感器,都能得心应手。建议在实际项目中根据硬件手册调整 CPOL/CPHA,并用示波器验证时序波形。