第 50 天:SPI 协议原理与主从设备通信流程
SPI(Serial Peripheral Interface)是一种高效、全双工、同步串行通信协议,广泛应用于 Flash、LCD 屏、传感器与无线模块等设备间的高速数据交互场景。与 I²C 相比,SPI 具备更高的数据吞吐能力、更强的灵活性以及更低的协议复杂度,但同时对时序控制和信号完整性提出了更高要求。
第 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 读写事务包含:
- 主机拉低 CS,启动通信;
- 主机输出时钟(SCK)并通过 MOSI 发送数据;
- 同步地,从机在 MISO 返回数据;
- 通信完成后,主机释放 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,即软件片选:
使用流程:
gpio_set_level(cs_pin, 0):拉低;spi_device_queue_trans()或spi_device_transmit();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 管理。每一篇都不讲概念空话,只做实战经验沉淀,让你一步步成为真正的模型运营专家。
🌟 如果本文对你有帮助,欢迎三连支持!
👍 点个赞,给我一些反馈动力
⭐ 收藏起来,方便之后复习查阅
🔔 关注我,后续还有更多实战内容持续更新
更多推荐




所有评论(0)