示例:SPI 主机通信
本教程的核心逻辑适用于所有 ESP32 开发板,但所有操作步骤均以 微雪 ESP32-S3-Zero 迷你开发板 为例进行讲解。如果您使用其他型号的开发板,请根据实际情况修改相应设置。
本教程介绍如何使用乐鑫 ESP-IDF 框架的 SPI 主机驱动,演示 bus + device 的双层模型、片选 (CS) 自动管理,以及如何选择引脚以获得最高时钟速率。示例将 SPI2 总线的 MOSI 与 MISO 用一根杜邦线短接,构成自发自收的环回回路。
1. SPI 外设
SPI(Serial Peripheral Interface,串行外设接口)是一种高速、全双工的同步串行通信协议,常用于连接 Flash、显示屏、SD 卡、传感器等需要较高带宽的外设。其特点:
- 四线接口:SCK(时钟)、MOSI(主出从入)、MISO(主入从出)、CS(片选)。
- 主从架构:主设备产生时钟、控制 CS、发起所有传输。从设备依靠 CS 区分多机。
- 全双工:一个时钟周期内可同时收发 1 bit 数据。
- 速率高:通常可达数十 MHz,远高于 I2C 与 UART。
ESP32-S3 内置 4 个 SPI 控制器:
| 控制器 | 用途 |
|---|---|
| SPI0、SPI1 | 内部使用,访问外部 Flash 与 PSRAM。应用层不可使用。 |
| SPI2(即 FSPI) | 通用 SPI 控制器,可作主机或从机。本示例使用。 |
| SPI3 | 通用 SPI 控制器,可作主机或从机。 |
1.1 IO MUX 与 GPIO 矩阵对 SPI 速度的影响
ESP32-S3 上每个 SPI 控制器都有一组专用 IO MUX 默认引脚,也可以通过 GPIO 矩阵把信号路由到任意其他 GPIO(参见 GPIO 矩阵与 IO_MUX 管脚)。两种方式带来的最高时钟速率不同:
| 引脚路由方式 | ESP32-S3 上 SPI 主机可靠工作的最高时钟 |
|---|---|
| IO MUX 默认管脚 | 80 MHz |
| GPIO 矩阵 | 40 MHz |
SPI2(FSPI)在 ESP32-S3 上的 IO MUX 默认引脚:
| 信号 | GPIO |
|---|---|
| CS0 | 10 |
| SCLK | 12 |
| MISO | 13 |
| MOSI | 11 |
只要驱动里指定的引脚与上表完全一致,驱动会自动走 IO MUX,无需任何额外配置。否则会自动改走 GPIO 矩阵,速率受限于 40 MHz。
本示例使用上述默认引脚,以便在需要高速率时直接受益于 IO MUX。
1.2 通用步骤:bus + device 模型
ESP-IDF 的 SPI 主机驱动采用 bus + device 两层抽象:
- 总线 (bus) 对应 SPI 控制器与共享的 SCK / MOSI / MISO 三根信号线,由
spi_host_device_t标识(如SPI2_HOST)。 - 设备 (device) 对应挂在总线上的某个具体从机,由
spi_device_handle_t描述。每个设备拥有独立的 CS 引脚、时钟频率、SPI 模式与队列长度等配置——驱动会在每次传输前自动切换到当前设备的配置。
这意味着同一根总线可以挂多个不同时钟速率、不同 CS 引脚的设备,驱动会自动管理。
通用步骤:
-
包含头文件
#include "driver/spi_master.h"并在
main/CMakeLists.txt中声明依赖:REQUIRES esp_driver_spi。 -
初始化总线:填写
spi_bus_config_t(MOSI / MISO / SCLK 引脚、最大传输字节数、DMA 通道),调用spi_bus_initialize()。未使用的引脚(如纯写场景的 MISO)传-1。 -
添加设备:填写
spi_device_interface_config_t(CS 引脚、SPI 模式 0~3、时钟速率、队列深度等),调用spi_bus_add_device(),获得设备句柄。 -
发起传输:构造
spi_transaction_t(数据长度、TX/RX 缓冲区),通过以下两种 API 之一发送:spi_device_polling_transmit():轮询,调用线程忙等到结束。适合短传输、低延迟场景。spi_device_transmit():中断驱动,调用线程阻塞让出 CPU。适合长传输或频率较低的传输。
-
(可选)释放资源:
spi_bus_remove_device()再spi_bus_free()。
CS 由驱动自动管理。 只要在 device 配置中设置了 spics_io_num,每次 *_transmit() 之前驱动会自动拉低 CS、传输完成后拉高,无需在用户代码里手动操作 GPIO。
2. 示例项目
本示例将 SPI2 配置为主机,把 MOSI(GPIO11)和 MISO(GPIO13)用一根杜邦线短接,构成环回回路。程序在一次传输中既发送一段固定数据,又接收回来,最后比对发送与接收是否一致。
由于 SPI 是全双工的,一次 transaction 中 TX 和 RX 同时进行——MOSI 上发出的每个 bit,在同一个时钟周期立刻通过跳线回到 MISO 被接收。比较两侧数据是否一致,即可验证整条 SPI 链路是否工作正常。
2.1 电路
需要使用的器件有:
- 杜邦线(或跳线)
- ESP32 开发板(微雪 ESP32-S3-Zero 迷你开发板)
用一根杜邦线把 GPIO11(MOSI) 与 GPIO13(MISO) 短接。SCLK(GPIO12)与 CS(GPIO10)不需要外部接线,驱动会自动输出对应信号。
ESP32-S3-Zero 引脚图


2.2 创建项目
-
创建一个项目。如果不清楚如何操作,请参考 从模板创建项目。
-
查看 SPI 主机驱动 API 参考。根据文档中的指引完成以下步骤。
首先在 main.c 中包含头文件:
#include "driver/spi_master.h"然后在 main/CMakeLists.txt 中声明
esp_driver_spi组件:idf_component_register(SRCS "main.c"INCLUDE_DIRS "."REQUIRES esp_driver_spi)
2.3 示例代码
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_err.h"
#include "driver/gpio.h"
#include "driver/spi_master.h"
static const char *TAG = "example";
#define SPI_HOST_USED SPI2_HOST // 使用 SPI2 (FSPI)
#define PIN_MOSI GPIO_NUM_11 // FSPI IO MUX 默认引脚
#define PIN_MISO GPIO_NUM_13
#define PIN_SCLK GPIO_NUM_12
#define PIN_CS GPIO_NUM_10
#define SPI_CLOCK_HZ (1 * 1000 * 1000) // 1 MHz,便于示波器观察
#define SPI_BUF_SIZE 16 // 收发缓冲区大小,单位字节
static spi_device_handle_t dev_handle;
static void spi_init(void)
{
// 1. 初始化总线
spi_bus_config_t bus_cfg = {
.mosi_io_num = PIN_MOSI,
.miso_io_num = PIN_MISO,
.sclk_io_num = PIN_SCLK,
.quadwp_io_num = -1, // 本示例不使用,固定为 -1
.quadhd_io_num = -1,
.max_transfer_sz = SPI_BUF_SIZE, // 单次最大传输字节数,与实际缓冲区对齐
};
ESP_ERROR_CHECK(spi_bus_initialize(SPI_HOST_USED, &bus_cfg, SPI_DMA_CH_AUTO));
// 2. 添加设备
spi_device_interface_config_t dev_cfg = {
.clock_speed_hz = SPI_CLOCK_HZ,
.mode = 0, // SPI mode 0 (CPOL=0, CPHA=0)
.spics_io_num = PIN_CS, // 由驱动自动管理 CS
.queue_size = 1,
};
ESP_ERROR_CHECK(spi_bus_add_device(SPI_HOST_USED, &dev_cfg, &dev_handle));
}
void app_main(void)
{
spi_init();
// 准备发送与接收缓冲区
uint8_t tx_buf[SPI_BUF_SIZE] = {
0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF,
};
uint8_t rx_buf[SPI_BUF_SIZE];
while (1) {
memset(rx_buf, 0, sizeof(rx_buf));
spi_transaction_t trans = {
.length = sizeof(tx_buf) * 8, // 单位是 bit
.tx_buffer = tx_buf,
.rx_buffer = rx_buf,
};
ESP_ERROR_CHECK(spi_device_polling_transmit(dev_handle, &trans));
// 环回时,rx_buf 应与 tx_buf 完全一致
if (memcmp(tx_buf, rx_buf, sizeof(tx_buf)) == 0) {
ESP_LOGI(TAG, "Loopback OK: %d bytes echoed back correctly",
(int)sizeof(tx_buf));
} else {
ESP_LOGW(TAG, "Loopback mismatch! Check jumper between GPIO%d and GPIO%d.",
PIN_MOSI, PIN_MISO);
ESP_LOG_BUFFER_HEX(TAG, tx_buf, sizeof(tx_buf));
ESP_LOG_BUFFER_HEX(TAG, rx_buf, sizeof(rx_buf));
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
2.4 构建并烧录
-
配置烧录选项
首先,在构建和烧录之前,请务必检查并设置正确的目标设备、串口和烧录方式。参考 第 2 节 运行示例 - 1.3 配置项目。
-
点击
一键自动依次执行构建、烧录和监视。
-
烧录完成后,确保 GPIO11 和 GPIO13 已用杜邦线短接,串口监视器中应看到周期性的"Loopback OK":
I (266) main_task: Calling app_main()I (276) example: Loopback OK: 16 bytes echoed back correctlyI (1276) example: Loopback OK: 16 bytes echoed back correctlyI (2276) example: Loopback OK: 16 bytes echoed back correctly...断开跳线后,将看到
Loopback mismatch!警告,并以十六进制打印发送和接收的数据。
2.5 代码解析
1. 包含头文件
#include "driver/gpio.h"
#include "driver/spi_master.h"
driver/spi_master.h:SPI 主机驱动入口。包含spi_bus_config_t、spi_device_interface_config_t、spi_transaction_t等所有需要的类型与 API。属于esp_driver_spi组件。driver/gpio.h:提供GPIO_NUM_x枚举。
2. 定义常量
#define SPI_HOST_USED SPI2_HOST
#define PIN_MOSI GPIO_NUM_11
#define PIN_MISO GPIO_NUM_13
#define PIN_SCLK GPIO_NUM_12
#define PIN_CS GPIO_NUM_10
#define SPI_CLOCK_HZ (1 * 1000 * 1000)
SPI2_HOST:选用 SPI2 控制器。也可改用SPI3_HOST,但 SPI3 在 ESP32-S3 上没有专用 IO MUX 引脚,所有引脚都走 GPIO 矩阵,时钟上限 40 MHz。SPI1_HOST在应用层不可用(被 Flash 占用)。PIN_MOSI/PIN_MISO/PIN_SCLK/PIN_CS:使用 SPI2 在 ESP32-S3 上的 IO MUX 默认引脚(详见 1.1 节)。如果改用其他 GPIO,驱动自动走 GPIO 矩阵,时钟上限降至 40 MHz。- 环回不需要外接从机,但 CS 仍由驱动正常输出(GPIO10 会随每次传输被拉低、传完拉高)。保留
PIN_CS是为了让代码结构与挂载真实从机时一致——后续直接改 SPI 模式和速率即可,不用回头补 CS 引脚配置。 SPI_CLOCK_HZ:1 MHz。本示例为环回验证,速率不是瓶颈,使用较低频率便于在示波器或逻辑分析仪上观察波形。
3. SPI 初始化(spi_init)
按"总线 → 设备"的顺序完成。
-
总线配置(
spi_bus_initialize)spi_bus_config_t bus_cfg = {.mosi_io_num = PIN_MOSI,.miso_io_num = PIN_MISO,.sclk_io_num = PIN_SCLK,.quadwp_io_num = -1,.quadhd_io_num = -1,.max_transfer_sz = SPI_BUF_SIZE,};spi_bus_initialize(SPI_HOST_USED, &bus_cfg, SPI_DMA_CH_AUTO);mosi_io_num/miso_io_num/sclk_io_num:总线共享的三根信号线。未使用的(如纯写场景下的 MISO)填-1。quadwp_io_num/quadhd_io_num:本示例未使用,固定传-1。max_transfer_sz:单次传输最大字节数。0 表示使用默认值(4092)。设小一些可节省 DMA 描述符内存——本示例与实际缓冲区SPI_BUF_SIZE对齐。SPI_DMA_CH_AUTO:让驱动自动分配一个 DMA 通道,传输大块数据时由 DMA 后台搬运,CPU 不用等。
-
设备配置(
spi_bus_add_device)spi_device_interface_config_t dev_cfg = {.clock_speed_hz = SPI_CLOCK_HZ,.mode = 0,.spics_io_num = PIN_CS,.queue_size = 1,};spi_bus_add_device(SPI_HOST_USED, &dev_cfg, &dev_handle);clock_speed_hz:该设备的 SCLK 频率,由驱动根据时钟源自动选择最接近的分频。mode:SPI 模式 0~3,对应 CPOL/CPHA 组合。mode 0 即 CPOL=0、CPHA=0:SCLK 空闲时为低电平,数据在第一个时钟边沿(上升沿)采样,是绝大多数 SPI 从机的默认模式。如果从机数据手册要求其他模式(如 mode 3),改这个值即可。spics_io_num:CS 引脚号。设置后,每次传输前驱动自动拉低、传输后拉高。若设为-1,则由用户自行控制 CS。queue_size:可同时排队的传输事务数量。轮询模式下设为 1 即可;中断模式下设大些可实现流水线传输。
得到的 dev_handle 用于后续所有传输调用。
4. 主循环:发起一次环回传输
spi_transaction_t.length 与 .rxlength 的单位都是 bit,不是 byte。基于 sizeof() 计算长度时需要 * 8。
spi_transaction_t trans = {
.length = sizeof(tx_buf) * 8,
.tx_buffer = tx_buf,
.rx_buffer = rx_buf,
};
spi_device_polling_transmit(dev_handle, &trans);
.length:传输长度,单位是 bit,不是 byte。16 字节就是 128 bit。.tx_buffer:要发送的数据缓冲区。.rx_buffer:接收数据的缓冲区。同时设置 TX 和 RX 即为全双工:发送 N bit 的同时接收 N bit。如果只想发不想收,可省略.rx_buffer(传 NULL)。spi_device_polling_transmit():发起一次轮询传输,调用线程忙等到完成。本示例每秒一次小数据传输,使用轮询 API 最简单。
由于 MOSI 与 MISO 已短接,主机在 MOSI 上发出的每个 bit 在同一个时钟边沿立即出现在 MISO 上并被采样回来。理想情况下 rx_buf 应与 tx_buf 完全相同。用 memcmp() 校验即可。
若 length > rxlength,驱动会自动认为 rxlength = length,即接收和发送等长。若想只发送 N bit、只接收 M bit(M < N),需显式设置 .rxlength = M * 8。