第 50 天:SPI 协议原理与主从设备通信流程


关键词:SPI 通信、主从协议、时序图、CPOL/CPHA、全双工、片选机制、STM32 SPI、ESP32 SPI、驱动开发


摘要

SPI(Serial Peripheral Interface)是一种高效、全双工、同步串行通信协议,广泛应用于 Flash、LCD 屏、传感器与无线模块等设备间的高速数据交互场景。与 I²C 相比,SPI 具备更高的数据吞吐能力、更强的灵活性以及更低的协议复杂度,但同时对时序控制和信号完整性提出了更高要求。

本篇围绕 SPI 的协议原理与主从通信流程进行深入讲解,结合 STM32 与 ESP32 两大平台的驱动实践,拆解片选控制、数据位顺序、时钟极性与相位等关键参数的配置方法,并提供可直接复用的通信框架代码与工程调试建议,帮助嵌入式开发者在实际项目中稳定、高效地部署 SPI 外设。


目录

一、SPI 通信结构与逻辑模型
二、主从设备时序详解:SCK、MISO、MOSI、CS
三、CPOL/CPHA 配置与 SPI 模式兼容策略
四、STM32 平台 SPI 主机初始化与数据交换实践
五、ESP32 平台 SPI 配置与总线设备管理封装
六、片选控制机制与多从设备接入架构设计
七、工程案例:SPI 接入 W25Q Flash 并实现读写验证
八、小结与进阶:DMA 模式、双 SPI、QPI 与高速通信优化


一、SPI 通信结构与逻辑模型

SPI(Serial Peripheral Interface)是一种由 Motorola 首创的全双工、同步式串行通信接口协议,现已成为嵌入式系统中最常用的高速外设总线之一。它以简洁的电气结构、高速的数据吞吐能力和灵活的主从扩展机制,在 MCU 与各类外设之间提供了极高的通信效率。

本节将从 SPI 总线的物理信号线出发,讲解其典型拓扑结构、逻辑传输机制以及在不同应用中的通信角色定义。


1. SPI 总线基本结构

标准的 SPI 通信由主设备(Master)控制总线,其它为从设备(Slave)。总线信号线包括以下 4 根:

信号线 全称 方向(相对主机) 功能说明
SCK Serial Clock 主机输出 主设备生成的时钟信号
MOSI Master Out Slave In 主机输出 主设备发送的数据线,从设备接收
MISO Master In Slave Out 主机输入 从设备发送的数据线,主设备接收
CS/SS Chip Select / Slave Select 主机输出 激活某个从设备的使能信号(低电平有效)

SPI 是全双工通信:在每个 SCK 周期,主设备向从设备发送一位数据,同时接收从设备响应的一位数据。


2. 拓扑结构:一主多从(Multi-Slave)

在大多数项目中,SPI 拓扑采用“一主多从”结构,每个从设备需独立的 CS 控制线:

MCU
 ├── SCK ─────────┬────────┬────────┐
 ├── MOSI ────────┼────────┼────────┤
 ├── MISO ◄───────┼────────┼────────┤
 ├── CS0  ───────► Flash
 ├── CS1  ───────► Display
 └── CS2  ───────► Sensor
  • 所有从设备共享 SCK/MOSI/MISO
  • 每个设备由主控通过 独立的 CS 引脚进行选通。

3. SPI 通信事务的基本流程

一次典型的 SPI 读写事务包含:

  1. 主机拉低 CS,启动通信;
  2. 主机输出时钟(SCK)并通过 MOSI 发送数据;
  3. 同步地,从机在 MISO 返回数据;
  4. 通信完成后,主机释放 CS 信号。

这种通信机制具有如下特性:

  • 同步传输:主设备驱动 SCK 节拍,数据在时钟边沿同步移动;
  • 无握手机制:不使用 ACK,应答等机制,由主机全程控制;
  • 高速度:SPI 通常支持几 Mbps 到数十 Mbps 的速率,远高于 I²C;
  • 简单物理层:信号线少,便于板级走线,适合高速接口扩展;

4. 逻辑传输与数据格式

每次 SPI 数据交换均为固定位宽传输(通常为 8 位),数据格式包含以下约定:

  • 位顺序(Bit Order):通常为 MSB first,也有设备使用 LSB first;
  • CPOL / CPHA:时钟极性/相位,定义数据采样与变更时机(详见下一节);
  • CS 动作边界:部分外设要求通信帧以拉低 CS 起始、拉高 CS 结束为边界。

5. SPI 与 UART / I2C 的对比

特性 SPI UART I2C
通信方式 同步 / 全双工 异步 / 半双工 同步 / 半双工
信号线 4 根(典型) 2 根 2 根
多从支持 独立 CS 控制 支持地址广播
速率上限 高(10Mbps+) 中(几 Mbps) 中低(400kbps~1Mbps)
硬件复杂度 高(含仲裁)

6. SPI 的常见应用场景

SPI 凭借其高速与低延迟特性,在以下外设中被广泛采用:

  • Flash 存储器(如 W25Q64、AT45DB);
  • LCD 屏幕(如 ILI9341、ST7789);
  • 无线通信芯片(如 NRF24L01、SX1278);
  • 工业传感器(如陀螺仪 MPU9250、加速度计 ADXL345);
  • ADC/DAC 采集芯片(如 MCP3008、DAC8552);

SPI 的高吞吐、低协议负载优势,使其成为嵌入式数据交互核心接口之一。

二、主从设备时序详解:SCK、MISO、MOSI、CS

SPI 的高速与简洁源于其明确的同步时序控制:通过主设备产生的时钟信号(SCK)驱动数据的传输,辅以片选信号(CS)定义通信边界。SPI 中每一位数据的传输均由 SCK 控制,并通过 MOSI(主发)与 MISO(主收)线在固定节拍下完成数据同步。

本节将逐一剖析四条核心信号线的工作逻辑,并结合实际通信波形理解 SPI 的典型传输行为,帮助开发者准确掌握设备通信时机与边界控制。


1. CS(Chip Select)片选线控制逻辑

功能:
  • 用于选择特定从设备
  • 通常为低电平有效(Active Low);
  • 高电平时,从设备应保持高阻状态,不参与总线传输。
要点:
  • 通信开始:主控将目标设备对应的 CS 拉低;
  • 通信过程中:CS 保持为低;
  • 通信结束:主控将 CS 拉高,设备释放总线。

建议:在一次完整 SPI 读写中,CS 应围绕每个命令进行精确控制,有些设备(如 Flash)要求 CS 上升沿表示“命令终止”。


2. SCK(Serial Clock)时钟信号

功能:
  • 主控输出的同步时钟;
  • 控制每一位数据的“采样”和“切换”时机;
  • 频率由主控设定(可达几十 MHz);
注意:
  • SPI 是同步协议,MOSI 与 MISO 上的数据变化必须严格同步于 SCK 的边沿;

  • SCK 的极性(CPOL)与相位(CPHA)决定了:

    • 空闲时钟电平;
    • 哪个时钟边沿用于采样数据。

3. MOSI(Master Out, Slave In)

功能:
  • 主设备向从设备发送数据的通道;
  • 在有效 SCK 边沿之前,主机必须将 MOSI 上的数据准备好。

4. MISO(Master In, Slave Out)

功能:
  • 从设备向主设备返回数据的通道;
  • 在有效 SCK 边沿前,从设备必须完成数据准备(从设备响应必须及时)。

5. SPI 通信帧波形示意(标准 8 位)

以 STM32 设置 SPI 为 CPOL=0、CPHA=0 模式为例(SPI Mode 0):

CPOL=0: 时钟空闲为低  
CPHA=0: 采样在上升沿,数据在下降沿变化

时序示意:

    SCK:  _-_-_-_-_-_-_-_-_
   MOSI: X|D7|D6|D5|D4|D3|D2|D1|D0|
   MISO: X|R7|R6|R5|R4|R3|R2|R1|R0|
    CS : ________|¯¯¯¯¯¯¯¯¯¯¯

每一位的传输周期为 1 个 SCK 周期,在 SCK 的有效边沿上采样。


6. SPI 四种模式(CPOL/CPHA 组合)

SPI 通信的行为高度依赖于 CPOL(时钟极性)与 CPHA(时钟相位)两个参数的组合。它们控制数据采样与变更的边沿。

SPI 模式 CPOL CPHA 描述说明
Mode 0 0 0 时钟空闲为低,上升沿采样下降沿改变
Mode 1 0 1 时钟空闲为低,下降沿采样上升沿改变
Mode 2 1 0 时钟空闲为高,下降沿采样上升沿改变
Mode 3 1 1 时钟空闲为高,上升沿采样下降沿改变

主从设备必须工作在相同的 SPI 模式下,否则数据采样会出现错位或乱码。


7. 多位传输说明

SPI 每次通信的位宽可以配置为 8 位、16 位甚至 32 位(视平台驱动支持),多个字节可连续写入 TX FIFO(发送缓冲),平台硬件控制连续发送时序,驱动层只需关心写入顺序。


8. SPI 时序中的关键工程关注点

项目 建议或注意事项
SCK 频率 一般建议小于外设最大支持频率的 80%
数据稳定性 SCK 有效边沿采样,另一边沿变更数据
CS 电平控制 使用 GPIO 控制或硬件片选口,必须严格匹配设备时序要求
空闲电平 CPOL 必须与外设默认状态匹配
数据读取延迟问题 某些外设需等待内部处理,应加入延迟或 Dummy Byte 探测

通过对 SPI 信号时序的掌握,可以更准确地与外设建立通信,并处理多从设备的兼容问题。

三、CPOL/CPHA 配置与 SPI 模式兼容策略

SPI 协议本身虽简单高效,但其最容易导致设备通信失败的核心参数就在于 CPOL(时钟极性)CPHA(时钟相位)。不同外设对 SPI 模式(CPOL/CPHA 的组合)要求可能存在差异,若配置错误,将导致采样错误、数据错位甚至通信完全失败。

本节将系统讲解 CPOL/CPHA 的逻辑含义、四种 SPI 模式的通信差异,并结合实际工程经验,总结主流设备的兼容建议与调试策略。


1. CPOL/CPHA 的定义与作用

参数 取值 作用说明
CPOL 0 空闲时钟为 低电平(SCK idle = Low)
CPOL 1 空闲时钟为 高电平(SCK idle = High)
CPHA 0 第一个时钟边沿采样数据
CPHA 1 第二个时钟边沿采样数据

这两个参数控制:

  • 数据 何时采样(数据被读取的时刻);
  • 数据 何时输出(数据被改变的时刻);
  • SPI 总线在空闲时 SCK 的默认电平状态。

2. 四种 SPI 模式组合一览

SPI 模式 CPOL CPHA 时钟空闲 有效边沿(采样) 非有效边沿(输出)
Mode 0 0 0 上升沿采样 下降沿更新数据
Mode 1 0 1 下降沿采样 上升沿更新数据
Mode 2 1 0 下降沿采样 上升沿更新数据
Mode 3 1 1 上升沿采样 下降沿更新数据

SPI 模式必须与外设的数据采样逻辑 一一对应。不同厂商定义的默认 SPI 模式不完全一致,需以 芯片手册或数据手册为准


3. 示例波形分析(以 SPI Mode 0 为例)

SCK : __-__-__-__-__-__-__-__-__
MOSI:  D7 D6 D5 D4 D3 D2 D1 D0
采样边沿:上升沿 → 主机/从机均在 SCK 上升沿读取输入
输出边沿:下降沿 → 数据在下降沿前被稳定到总线上

若 CPHA 设置错误,会导致数据还未稳定时就被采样,造成位移或乱码问题


4. 主流外设的默认 SPI 模式参考

设备/芯片型号 默认 SPI 模式 说明
W25Q Flash 系列 Mode 0 必须在上升沿采样
ILI9341 LCD 控制器 Mode 0 通常使用模式 0,部分模块可兼容 Mode 3
MPU9250 传感器 Mode 3 必须使用 CPOL=1, CPHA=1
ADXL345 加速度计 Mode 3 时钟高电平空闲,下降沿采样
MAX6675 温度采集 Mode 0 输出数据有效时间要求 Mode 0
MFRC522 RFID 模块 Mode 0 推荐 CPOL=0, CPHA=0

注意:不同模块供应商即便使用相同芯片,驱动时序可能因电路设计略有差异,应根据实际波形验证。


5. STM32 和 ESP32 中 CPOL/CPHA 配置方法

STM32(以 HAL 为例):
hspi1.Init.CLKPolarity = SPI_POLARITY_LOW;     // CPOL = 0
hspi1.Init.CLKPhase    = SPI_PHASE_1EDGE;       // CPHA = 0
  • CLKPolarity: LOW = CPOL=0,HIGH = CPOL=1
  • CLKPhase: 1EDGE = CPHA=0,2EDGE = CPHA=1
ESP32(ESP-IDF):
spi_device_interface_config_t devcfg = {
    .mode = 0, // 0~3 对应 SPI 模式
    ...
};

6. SPI 模式调试策略

  • 观察时序波形:使用逻辑分析仪分析 MOSI/MISO 与 SCK 的关系;
  • 逐模式尝试:当文档不明确时,依次尝试 Mode 0~3,确认数据是否对齐;
  • 注意数据位反转问题:若数据顺序颠倒,还需确认位顺序是 MSB first 还是 LSB first;
  • 确保主从一致:主控配置和从设备数据手册描述的模式需完全匹配;
  • 部分设备不支持动态切换模式:初始化后不可修改 SPI 模式,应在配置前一次性设定。

7. 推荐工程实践建议

建议项 理由与实践好处
建立 SPI 设备兼容表 标明每类设备使用的模式,便于模块复用
封装 SPI 设备驱动抽象层 每个设备封装自身 SPI 模式,避免全局干扰
波形调试后进行模式固化 避免因开发板更换或版本升级引起模式错乱
主控端支持多个 SPI 控制器配置不同模式 一主多从支持异构设备通信需求

SPI 模式虽然看似简单,但实则是造成通信失败的高频隐患。掌握 CPOL/CPHA 的真实含义、阅读外设时序图并结合逻辑分析仪调试,是 SPI 工程实践中不可或缺的能力。

四、STM32 平台 SPI 主机初始化与数据交换实践

SPI 在 STM32 微控制器上的支持非常完善,既可以通过 HAL 库快速开发,也可使用 LL(Low Layer)库或直接操作寄存器实现高性能控制。在实际项目中,SPI 常用于驱动 Flash、LCD 屏、传感器等高速外设,因此高效的初始化与稳定的数据交互流程对于系统性能尤为关键。

本节将围绕 STM32 SPI 主机的实际工程应用,逐步讲解 HAL 方式的初始化、通信过程与调试要点,并提供完整的工程级驱动模板供复用。


1. SPI 初始化流程(基于 STM32 HAL)

以 STM32F4/F1/F7 系列为例,SPI 外设通常通过 SPI1~SPI3 等实例调用。配置流程如下:

(1)使用 STM32CubeMX 配置 SPI 接口:
  • 选择 SPIx 作为 Master;
  • 设置数据大小为 8-bit;
  • 设定 SCK 极性/相位(对应 SPI 模式);
  • 设置波特率分频(根据设备支持速率);
  • 开启 NSS 软件管理(除非使用硬件 CS);
  • 分配 GPIO 引脚:SCK、MOSI、MISO(通常为复用功能);
(2)HAL 初始化代码示例:
SPI_HandleTypeDef hspi1;

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;     // CPOL = 0
  hspi1.Init.CLKPhase = SPI_PHASE_1EDGE;         // CPHA = 0
  hspi1.Init.NSS = SPI_NSS_SOFT;
  hspi1.Init.BaudRatePrescaler = SPI_BAUDRATEPRESCALER_16;
  hspi1.Init.FirstBit = SPI_FIRSTBIT_MSB;
  hspi1.Init.TIMode = SPI_TIMODE_DISABLE;
  hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
  HAL_SPI_Init(&hspi1);
}

⚠ 注意:某些外设(如 Flash)对 SCK 频率敏感,建议初期调试从低频(如 1MHz)开始。


2. SPI 数据交换流程

STM32 SPI 支持轮询(Polling)中断(Interrupt)DMA(Direct Memory Access) 三种数据交互方式,建议优先从轮询方式入门。

(1)发送数据(Master 写从设备):
uint8_t tx_data[2] = {0x9F, 0x00};  // 示例:读取 Flash ID 命令
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);  // 拉低 CS
HAL_SPI_Transmit(&hspi1, tx_data, 1, HAL_MAX_DELAY);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);    // 释放 CS
(2)接收数据(Master 读从设备):
uint8_t rx_data[3] = {0};
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);
HAL_SPI_Receive(&hspi1, rx_data, 3, HAL_MAX_DELAY);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);
(3)发送并接收(全双工):
uint8_t tx_data = 0x9F;
uint8_t rx_data[3] = {0};
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET);
HAL_SPI_Transmit(&hspi1, &tx_data, 1, HAL_MAX_DELAY);
HAL_SPI_Receive(&hspi1, rx_data, 3, HAL_MAX_DELAY);
HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET);

或者使用全双工接口一次完成:

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

3. SPI 通信封装建议

为了提高复用性与工程维护性,建议封装如下结构:

typedef struct {
    SPI_HandleTypeDef *spi_handle;
    GPIO_TypeDef *cs_port;
    uint16_t cs_pin;
} spi_device_t;

void spi_write(spi_device_t *dev, uint8_t *data, size_t len)
{
    HAL_GPIO_WritePin(dev->cs_port, dev->cs_pin, GPIO_PIN_RESET);
    HAL_SPI_Transmit(dev->spi_handle, data, len, HAL_MAX_DELAY);
    HAL_GPIO_WritePin(dev->cs_port, dev->cs_pin, GPIO_PIN_SET);
}

4. 通信故障排查技巧

问题表现 排查方向
无响应、全 FF/00 SPI 模式不匹配,CS 未正确控制
偶尔乱码 时钟频率过高、CPOL/CPHA 配置错误
HAL 函数卡住 忘记配置中断优先级,或硬件连接异常
多从设备冲突 CS 管脚控制错误、逻辑未隔离
读取 Flash ID 异常 指令时序错误,未释放/拉高片选信号

使用逻辑分析仪是调试 SPI 问题的高效手段,推荐在时序不明、无响应时立即接入。


5. HAL 与裸机方式的选择建议

特性 HAL 驱动 寄存器裸机驱动
上手难度 较低,CubeMX 自动生成 需要熟悉 SPI 寄存器手册
移植效率 高,支持多个芯片系列 中,需重写不同平台寄存器地址
性能控制 适中,抽象层封装增加一定开销 高,可实现精细优化
异常调试 比较容易定位 HAL 状态返回值 对寄存器状态依赖大,调试难度较高

STM32 平台提供了稳定的 SPI 外设支持,通过 HAL 驱动能够快速完成初始化与设备通信流程。后续若对通信性能有极致追求,可切换为 DMA 模式或裸机优化。

五、ESP32 平台 SPI 配置与总线设备管理封装

ESP32 提供强大的 SPI 硬件控制器,支持主/从双角色切换、DMA 加速、四线全双工、QSPI(四线)与 VSPI/HSPI 双控制器并存等特性,非常适合连接多种 SPI 外设,如 Flash、LCD、触控 IC、无线模块等。

本节将基于 ESP-IDF 框架,实战讲解 ESP32 平台的 SPI 主机初始化、设备注册、读写操作及驱动封装方式,重点关注多设备片选管理和总线复用机制,帮助开发者在复杂项目中实现结构清晰、性能可靠的 SPI 通信框架。


1. ESP32 SPI 控制器简介

ESP32 芯片集成两个 SPI 主控制器:

控制器 通常用途 IDF 设备名
HSPI 通用高速外设 SPI2_HOST
VSPI 通用高速外设 SPI3_HOST

SPI1 控制器通常用于访问 Flash,不建议复用给用户设备。


2. SPI 总线初始化(主机)

在 ESP-IDF 中,SPI 主机初始化由以下两步构成:

  • 初始化总线(spi_bus_initialize());
  • 挂载设备(spi_bus_add_device());
示例代码:
#include "driver/spi_master.h"
#include "driver/gpio.h"

#define PIN_NUM_MISO  19
#define PIN_NUM_MOSI  23
#define PIN_NUM_CLK   18

void spi_bus_init(void)
{
    spi_bus_config_t buscfg = {
        .miso_io_num = PIN_NUM_MISO,
        .mosi_io_num = PIN_NUM_MOSI,
        .sclk_io_num = PIN_NUM_CLK,
        .quadwp_io_num = -1,
        .quadhd_io_num = -1,
        .max_transfer_sz = 4096,
    };

    esp_err_t ret = spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO);
    assert(ret == ESP_OK);
}

3. 挂载 SPI 外设设备(slave)

ESP-IDF 将每个外设看作一个“设备对象”,并为其分配独立的片选引脚。

示例代码(添加设备):
#define PIN_NUM_CS  5

spi_device_handle_t spi_dev;

void spi_device_init(void)
{
    spi_device_interface_config_t devcfg = {
        .clock_speed_hz = 8 * 1000 * 1000,       // 8 MHz
        .mode = 0,                                // SPI 模式 0
        .spics_io_num = PIN_NUM_CS,              // CS 引脚
        .queue_size = 4,                         // 事务队列大小
    };

    esp_err_t ret = spi_bus_add_device(SPI2_HOST, &devcfg, &spi_dev);
    assert(ret == ESP_OK);
}

每个设备的配置(速率、SPI 模式、CS 管脚)是独立的,ESP32 会自动进行片选调度与模式切换。


4. SPI 通信函数(发送 / 接收 / 全双工)

ESP-IDF 采用事务化方式进行 SPI 通信,主接口为:

esp_err_t spi_device_transmit(spi_device_handle_t handle, spi_transaction_t *trans);
示例:发送命令并读取响应
uint8_t tx_data[1] = { 0x9F };  // 读取 Flash ID 命令
uint8_t rx_data[3] = { 0 };

spi_transaction_t t = {
    .length = 8 * sizeof(tx_data),
    .tx_buffer = tx_data,
    .rxlength = 8 * sizeof(rx_data),
    .rx_buffer = rx_data,
};

esp_err_t ret = spi_device_transmit(spi_dev, &t);

5. SPI 总线设备封装结构建议(工程级)

为实现通用、可复用、多设备支持,建议封装如下设备结构体:

typedef struct {
    spi_device_handle_t handle;
    spi_host_device_t host;
    int cs_gpio;
    int spi_mode;
    int clock_speed_hz;
} spi_device_config_t;

配套初始化封装:

esp_err_t spi_device_setup(spi_device_config_t *dev)
{
    spi_bus_config_t buscfg = {
        .miso_io_num = ...,
        .mosi_io_num = ...,
        .sclk_io_num = ...,
        ...
    };
    spi_bus_initialize(dev->host, &buscfg, SPI_DMA_CH_AUTO);

    spi_device_interface_config_t devcfg = {
        .clock_speed_hz = dev->clock_speed_hz,
        .mode = dev->spi_mode,
        .spics_io_num = dev->cs_gpio,
        ...
    };
    return spi_bus_add_device(dev->host, &devcfg, &dev->handle);
}

这样可动态配置任意 SPI 外设,实现平台级设备管理与驱动抽象。


6. 调试与常见问题排查

问题表现 可能原因
数据错位 / 无响应 SPI 模式不匹配(CPOL/CPHA 设置错误)
全部返回 0xFF 从设备未被片选或响应时间不足
esp_err_invalid_arg SPI 总线未初始化或参数非法
低速正常,高速异常 SPI 时钟过高,设备时序不满足或 PCB 线长干扰

推荐使用 logic analyzer 抓取 MOSI、MISO、SCK、CS 波形,分析数据与时钟的边沿匹配情况。


7. 高级应用扩展方向

  • DMA 模式优化大数据吞吐
  • 全双工 SPI LCD 屏刷新加速
  • QPI 模式支持四线传输
  • SPI Slave 模拟从机行为(ESP32 支持)
  • 多个 SPI 控制器分别驱动不同设备

ESP32 的 SPI 驱动设计体现了模块化与工程抽象的思路,合理利用设备对象与事务结构,可以构建高可维护性的多外设通信系统。

六、片选控制机制与多从设备接入架构设计

在嵌入式系统中,一主多从的 SPI 架构非常常见:一个主控 MCU(或 SoC)通过共享的 SPI 总线(SCK、MOSI、MISO)控制多个外设,如 Flash、屏幕、传感器等。片选(CS)信号的管理策略是系统通信稳定性和可扩展性的关键。

本节将深入分析 SPI 片选机制的底层实现逻辑,分别探讨 STM32 与 ESP32 等主流平台下的多从设备接入方式,结合典型架构设计策略,帮助你构建稳定可靠的 SPI 多设备系统。


1. 片选信号(CS)基本机制

  • CS(Chip Select) 信号是 SPI 中主控用来激活具体从设备的手段;

  • 一条 SPI 总线上可以挂多个设备,但每个从设备都需要独立的 CS 引脚

  • 通信过程必须确保:

    • 只有目标从设备的 CS 被拉低;
    • 其他从设备保持 CS 高电平并将 MISO 线处于高阻态(避免总线冲突)。

2. SPI 多从系统的典型拓扑结构

        ┌────────┐
MOSI ───┤        │
MISO ◄──┤  MCU   ├── CS0 ───► Flash
SCK  ───┤        ├── CS1 ───► Display
        └────────┘── CS2 ───► Sensor

所有从设备共用 SPI 总线(MOSI/MISO/SCK),每个设备单独配置一个 CS 控制引脚。


3. STM32 平台下的 CS 管理策略

STM32 通常将 CS 管脚作为 GPIO 管脚手动控制,实现灵活设备管理:

示例封装(HAL):
void spi_select(GPIO_TypeDef *cs_port, uint16_t cs_pin)
{
    HAL_GPIO_WritePin(cs_port, cs_pin, GPIO_PIN_RESET);  // 片选使能
}

void spi_deselect(GPIO_TypeDef *cs_port, uint16_t cs_pin)
{
    HAL_GPIO_WritePin(cs_port, cs_pin, GPIO_PIN_SET);    // 片选关闭
}

在多设备通信时,通过封装结构体关联设备对应的 CS 引脚:

typedef struct {
    SPI_HandleTypeDef *hspi;
    GPIO_TypeDef *cs_port;
    uint16_t cs_pin;
} spi_device_t;

这样可通过统一驱动框架支持多个 SPI 从设备。


4. ESP32 平台的设备片选机制

ESP-IDF 支持硬件自动片选控制,每个 SPI 设备注册时可绑定一个 CS 引脚:

配置:
spi_device_interface_config_t devcfg = {
    .spics_io_num = GPIO_NUM_5,   // 对应设备的 CS 脚
    ...
};
  • 系统自动在 spi_device_transmit() 前拉低 CS;
  • 完成后拉高;
  • 多设备共用一套 spi_bus_initialize() 配置,各自独立管理。
多设备挂载:
spi_device_handle_t dev_flash, dev_lcd;

spi_bus_add_device(SPI2_HOST, &devcfg_flash, &dev_flash);
spi_bus_add_device(SPI2_HOST, &devcfg_lcd, &dev_lcd);

调用时只需指定目标设备句柄即可:

spi_device_transmit(dev_flash, &transaction);

5. 软件片选(Soft-CS)机制

有些场景(如 DMA 驱动、GPIO 不可硬件绑定 CS)下需要手动控制 CS,即软件片选

使用流程:
  1. gpio_set_level(cs_pin, 0):拉低;
  2. spi_device_queue_trans()spi_device_transmit()
  3. gpio_set_level(cs_pin, 1):拉高。

使用软件片选需要确保通信帧边界控制一致,避免提前释放或拉 CS 太长时间。


6. 多设备结构体封装建议(通用框架)

typedef struct {
    spi_host_device_t host;
    spi_device_handle_t handle;
    int cs_gpio;
    const char* name;
} spi_dev_t;

使用数组或链表统一管理:

spi_dev_t spi_devices[] = {
    { .host = SPI2_HOST, .cs_gpio = 5, .name = "W25Q64" },
    { .host = SPI2_HOST, .cs_gpio = 15, .name = "ST7789" },
    { .host = SPI2_HOST, .cs_gpio = 21, .name = "MPU9250" }
};

7. 多设备 SPI 总线设计注意事项

项目 建议配置或说明
CS 引脚数量 每个设备占用 1 个 GPIO,用 GPIO 充分评估
SPI 模式统一性 不同设备 SPI 模式不同,建议用多 SPI 控制器
数据速率限制 SPI 总线受最慢设备限制(可分设备设速率)
总线拓扑长度与干扰 SPI 为高速接口,布线要短直并加终端电阻
使用 SPI 总线锁机制 防止多线程并发操作同一 SPI 总线

8. 多 SPI 控制器分离策略(高并发场景)

ESP32 支持 VSPI / HSPI 两套主控制器,推荐将频繁访问与偶尔访问设备分开:

总线 适用设备
VSPI (SPI3) 显示屏、LCD
HSPI (SPI2) Flash、传感器

这样可避免频繁切换设备导致的资源抢占与驱动干扰。


SPI 多设备系统的核心是统一管理、精确片选、并发控制与架构解耦,合理封装驱动与结构体能显著提升系统可维护性与工程效率。

七、工程案例:SPI 接入 W25Q Flash 并实现读写验证

W25Q 系列 Flash(如 W25Q32、W25Q64)广泛用于嵌入式系统中的数据存储、引导加载、资源映射等场景,其基于 SPI 接口进行通信,支持标准指令集与扇区页操作,具备典型的工程代表性。

本节以 W25Q Flash 为例,展示从 SPI 初始化、指令发送到读写校验的完整工程实践,分别在 STM32 和 ESP32 平台下提供关键代码,实现跨平台的 Flash 驱动构建。


1. W25Q Flash 简要特性

特性项 参数说明
接口协议 SPI(Mode 0, 支持 QSPI)
支持容量 4Mb ~ 512Mb
页大小 256 字节
擦除单位 4KB(扇区)、64KB(块)
支持命令 读 ID、读数据、页编程、扇区擦除等

2. 通信初始化配置(ESP32 为例)

SPI 主机初始化与设备挂载:

spi_device_handle_t flash_handle;

void w25q_spi_init(void) {
    spi_bus_config_t buscfg = {
        .mosi_io_num = 23,
        .miso_io_num = 19,
        .sclk_io_num = 18,
        .max_transfer_sz = 4096,
    };
    spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO);

    spi_device_interface_config_t devcfg = {
        .clock_speed_hz = 8 * 1000 * 1000,
        .mode = 0,
        .spics_io_num = 5,
        .queue_size = 3,
    };
    spi_bus_add_device(SPI2_HOST, &devcfg, &flash_handle);
}

3. 读取芯片 ID 命令验证

Flash 芯片通常支持 JEDEC ID 查询指令 0x9F

uint8_t cmd = 0x9F;
uint8_t id[3] = {0};

spi_transaction_t t = {
    .length = 8,              // 1 字节命令
    .tx_buffer = &cmd,
    .rxlength = 24,           // 3 字节返回
    .rx_buffer = id,
};
spi_device_transmit(flash_handle, &t);

// 打印厂商 ID / 设备 ID
printf("JEDEC ID: %02X %02X %02X\n", id[0], id[1], id[2]);

常见结果(W25Q64):

  • 厂商 ID:EF
  • 设备 ID:40 17 (64Mbit)

4. 写使能与页编程流程

Flash 写操作前必须执行 写使能命令(0x06),并轮询写完成。

void w25q_write_enable() {
    uint8_t cmd = 0x06;
    spi_transaction_t t = {
        .length = 8,
        .tx_buffer = &cmd,
    };
    spi_device_transmit(flash_handle, &t);
}

页编程指令 0x02 示例(地址为 0x000000):

void w25q_page_program(uint32_t addr, uint8_t *data, size_t len) {
    uint8_t buf[4] = {
        0x02,
        (addr >> 16) & 0xFF,
        (addr >> 8) & 0xFF,
        addr & 0xFF,
    };

    spi_transaction_t t = {
        .length = (4 + len) * 8,
        .tx_buffer = NULL,
    };

    uint8_t send_buf[4 + len];
    memcpy(send_buf, buf, 4);
    memcpy(send_buf + 4, data, len);
    t.tx_buffer = send_buf;

    w25q_write_enable();
    spi_device_transmit(flash_handle, &t);
}

5. 数据读取操作(0x03)

读取地址数据:

void w25q_read(uint32_t addr, uint8_t *dst, size_t len) {
    uint8_t cmd[4] = {
        0x03,
        (addr >> 16) & 0xFF,
        (addr >> 8) & 0xFF,
        addr & 0xFF,
    };

    spi_transaction_t t = {
        .length = 8 * 4,
        .rxlength = 8 * len,
        .tx_buffer = cmd,
        .rx_buffer = dst,
    };

    spi_device_transmit(flash_handle, &t);
}

6. 写入并校验示例流程

void flash_write_test() {
    uint8_t tx_buf[16] = "Hello, W25Q64!";
    uint8_t rx_buf[16] = {0};

    w25q_page_program(0x000000, tx_buf, sizeof(tx_buf));
    vTaskDelay(pdMS_TO_TICKS(10));  // 等待写完成

    w25q_read(0x000000, rx_buf, sizeof(rx_buf));

    if (memcmp(tx_buf, rx_buf, sizeof(tx_buf)) == 0) {
        printf("Flash 写入校验成功\n");
    } else {
        printf("Flash 写入失败,数据不匹配\n");
    }
}

7. 注意事项与调试建议

项目 建议或说明
SPI 模式 W25Q 默认 SPI Mode 0(CPOL=0, CPHA=0)
写入前必须使能写操作 每次编程/擦除前都需发送 0x06 写使能命令
写入时间非即时 通常需延时或读取状态寄存器轮询写完成
不支持跨页写 页编程操作不能跨越 256 字节边界
擦除操作需先完成 写入前如需擦除,需发送 0x20 或 0xD8 等指令

使用逻辑分析仪可直观看到时序、数据帧是否正确,有助于识别写入失败、片选错误等问题。


通过该案例可以掌握 SPI 外设驱动的完整流程:设备初始化、命令发送、数据交换与校验验证。W25Q Flash 是典型的 SPI 设备模板,其通信行为对大多数 SPI 传感器、扩展模块具备高度参考价值。

八、小结与进阶:DMA 模式、双 SPI、QPI 与高速通信优化

经过本章节的系统实践,我们已完成对 SPI 总线通信原理、片选控制、多从设备架构,以及 W25Q Flash 实战驱动的全面解析。在实际产品开发中,若系统存在大量数据传输、多个外设并行通信或更高传输速率需求,则需进一步引入更高阶的优化手段,如 DMA、双 SPI 总线、QPI 模式等。

本节围绕这些工程常用的进阶策略进行总结与拓展,帮助你构建具备高并发、高吞吐、高可靠性的 SPI 通信系统。


1. 使用 DMA 提升传输效率

在 HAL 或 ESP-IDF 驱动下,SPI 默认使用阻塞模式完成数据交换,主 CPU 会一直等待传输完成。若频繁进行大容量 SPI 操作(如 Flash 连续写入),阻塞式传输会严重拖慢系统主频利用率。

解决方案:使用 DMA 加速 SPI 传输

在 STM32 中(以 HAL 为例):
HAL_SPI_Transmit_DMA(&hspi1, tx_buf, len);
  • 可异步处理传输,释放主核执行其他任务;
  • 需在 .ioc 中开启 SPI DMA 通道支持;
  • 注意:中断优先级设置正确,DMA 缓冲区不可使用局部变量。
ESP32 中(ESP-IDF 默认 SPI 支持 DMA):

只需在 spi_bus_initialize() 中指定:

spi_bus_initialize(..., SPI_DMA_CH_AUTO);

ESP32 SPI 驱动内部会判断数据大小,自动选择 DMA 或 CPU 传输方式。


2. 双 SPI 控制器并发传输

ESP32 和 STM32 等芯片普遍具备多个独立的 SPI 控制器(如 SPI1/SPI2 或 VSPI/HSPI),我们可以将不同外设挂载在不同 SPI 控制器上,实现总线并发。

示例:
  • SPI2(HSPI):用于 Flash 存储、LCD 刷屏等高频操作;
  • SPI3(VSPI):连接低频传感器(如温湿度、触控);

在使用 FreeRTOS 的系统中,不同任务可独立访问各 SPI 控制器,避免互斥锁冲突。


3. QPI 模式:四线 SPI(Quad SPI)读写

QPI(Quad Peripheral Interface)是 W25Q Flash 系列支持的一种增强模式,通信时使用 4 条数据线(IO0~IO3),大幅提升吞吐速度(理论上达原 SPI 的 4 倍)。

特点:
  • 指令、地址、数据传输全部采用 4-bit 模式;
  • QPI 模式需通过发送特定命令(如 0x38)开启;
  • ESP32 中默认使用 SPI 访问 Flash,可配置为 QSPI 提升启动速度。

工程中如需支持 QPI,需要硬件连接支持 IO2、IO3 并确保总线复用正确。


4. SPI 总线吞吐优化建议

优化方向 具体建议说明
减少通信次数 批量写入或多字节连续读取,减少片选切换次数
SPI 速率配置 合理设置 clock_speed_hz,多数设备支持 8~40MHz
最小化传输开销 使用 spi_device_queue_trans() 异步队列提升并发性
DMA 缓冲区对齐 使用 heap_caps_malloc() 分配 PSRAM / DMA 能力缓存
调整任务优先级 确保 SPI 操作任务优先级不被中断延迟干扰

5. 推荐工具与调试手段

  • 逻辑分析仪(如 Saleae):捕捉 SCK、MOSI、MISO、CS 信号进行波形比对;
  • Flash 模拟测试工具:验证指令序列执行正确性;
  • FreeRTOS Trace Viewer:分析 SPI 任务响应延迟;
  • 带 CRC 校验的数据结构:防止通信干扰导致数据误判;

6. 可扩展场景与设计模板

场景 推荐架构或方式
多 SPI 外设 + 实时系统 多 SPI 控制器 + 任务互斥 + 驱动封装层
大数据高速写入 SPI + DMA + 双缓冲区管理
引导加载存储(Bootloader) SPI Flash 驱动精简化、位操作优化
同时驱动 LCD 屏 + 触控芯片 SPI2 驱动 LCD(DMA 刷屏)、SPI3 驱动触控(低速 SPI)
接入高速传感器(如 IMU) 使用 SPI + 中断 + 多字节 Burst 模式持续采集

总结

SPI 是嵌入式通信中极为核心的总线协议,其基础通信机制清晰,但实际工程中涉及的设备差异性、时序控制与资源复用挑战,要求开发者对平台能力、外设协议、电气连接、驱动结构等都有深入理解。

通过 DMA、双 SPI、QPI 模式等技术手段,我们可构建更高速、低功耗、抗干扰的通信系统,在复杂项目中实现高性能的数据交互与设备控制能力。

个人简介
在这里插入图片描述
作者简介:全栈研发,具备端到端系统落地能力,专注人工智能领域。
个人主页:观熵
个人邮箱:privatexxxx@163.com
座右铭:愿科技之光,不止照亮智能,也照亮人心!

专栏导航

观熵系列专栏导航:
具身智能:具身智能
国产 NPU × Android 推理优化:本专栏系统解析 Android 平台国产 AI 芯片实战路径,涵盖 NPU×NNAPI 接入、异构调度、模型缓存、推理精度、动态加载与多模型并发等关键技术,聚焦工程可落地的推理优化策略,适用于边缘 AI 开发者与系统架构师。
DeepSeek国内各行业私有化部署系列:国产大模型私有化部署解决方案
智能终端Ai探索与创新实践:深入探索 智能终端系统的硬件生态和前沿 AI 能力的深度融合!本专栏聚焦 Transformer、大模型、多模态等最新 AI 技术在 智能终端的应用,结合丰富的实战案例和性能优化策略,助力 智能终端开发者掌握国产旗舰 AI 引擎的核心技术,解锁创新应用场景。
企业级 SaaS 架构与工程实战全流程:系统性掌握从零构建、架构演进、业务模型、部署运维、安全治理到产品商业化的全流程实战能力
GitHub开源项目实战:分享GitHub上优秀开源项目,探讨实战应用与优化策略。
大模型高阶优化技术专题
AI前沿探索:从大模型进化、多模态交互、AIGC内容生成,到AI在行业中的落地应用,我们将深入剖析最前沿的AI技术,分享实用的开发经验,并探讨AI未来的发展趋势
AI开源框架实战:面向 AI 工程师的大模型框架实战指南,覆盖训练、推理、部署与评估的全链路最佳实践
计算机视觉:聚焦计算机视觉前沿技术,涵盖图像识别、目标检测、自动驾驶、医疗影像等领域的最新进展和应用案例
国产大模型部署实战:持续更新的国产开源大模型部署实战教程,覆盖从 模型选型 → 环境配置 → 本地推理 → API封装 → 高性能部署 → 多模型管理 的完整全流程
Agentic AI架构实战全流程:一站式掌握 Agentic AI 架构构建核心路径:从协议到调度,从推理到执行,完整复刻企业级多智能体系统落地方案!
云原生应用托管与大模型融合实战指南
智能数据挖掘工程实践
Kubernetes × AI工程实战
TensorFlow 全栈实战:从建模到部署:覆盖模型构建、训练优化、跨平台部署与工程交付,帮助开发者掌握从原型到上线的完整 AI 开发流程
PyTorch 全栈实战专栏: PyTorch 框架的全栈实战应用,涵盖从模型训练、优化、部署到维护的完整流程
深入理解 TensorRT:深入解析 TensorRT 的核心机制与部署实践,助力构建高性能 AI 推理系统
Megatron-LM 实战笔记:聚焦于 Megatron-LM 框架的实战应用,涵盖从预训练、微调到部署的全流程
AI Agent:系统学习并亲手构建一个完整的 AI Agent 系统,从基础理论、算法实战、框架应用,到私有部署、多端集成
DeepSeek 实战与解析:聚焦 DeepSeek 系列模型原理解析与实战应用,涵盖部署、推理、微调与多场景集成,助你高效上手国产大模型
端侧大模型:聚焦大模型在移动设备上的部署与优化,探索端侧智能的实现路径
行业大模型 · 数据全流程指南:大模型预训练数据的设计、采集、清洗与合规治理,聚焦行业场景,从需求定义到数据闭环,帮助您构建专属的智能数据基座
机器人研发全栈进阶指南:从ROS到AI智能控制:机器人系统架构、感知建图、路径规划、控制系统、AI智能决策、系统集成等核心能力模块
人工智能下的网络安全:通过实战案例和系统化方法,帮助开发者和安全工程师识别风险、构建防御机制,确保 AI 系统的稳定与安全
智能 DevOps 工厂:AI 驱动的持续交付实践:构建以 AI 为核心的智能 DevOps 平台,涵盖从 CI/CD 流水线、AIOps、MLOps 到 DevSecOps 的全流程实践。
C++学习笔记?:聚焦于现代 C++ 编程的核心概念与实践,涵盖 STL 源码剖析、内存管理、模板元编程等关键技术
AI × Quant 系统化落地实战:从数据、策略到实盘,打造全栈智能量化交易系统
大模型运营专家的Prompt修炼之路:本专栏聚焦开发 / 测试人员的实际转型路径,基于 OpenAI、DeepSeek、抖音等真实资料,拆解 从入门到专业落地的关键主题,涵盖 Prompt 编写范式、结构输出控制、模型行为评估、系统接入与 DevOps 管理。每一篇都不讲概念空话,只做实战经验沉淀,让你一步步成为真正的模型运营专家。


🌟 如果本文对你有帮助,欢迎三连支持!

👍 点个赞,给我一些反馈动力
⭐ 收藏起来,方便之后复习查阅
🔔 关注我,后续还有更多实战内容持续更新

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐