跳到主要内容

示例:SPI 主机通信

重要提示:关于开发板的兼容性

本教程的核心逻辑适用于所有 ESP32 开发板,但所有操作步骤均以 微雪 ESP32-S3-Zero 迷你开发板 为例进行讲解。如果您使用其他型号的开发板,请根据实际情况修改相应设置。

本教程介绍如何使用乐鑫 ESP-IDF 框架的 SPI 主机驱动,演示 bus + device 的双层模型、片选 (CS) 自动管理,以及如何选择引脚以获得最高时钟速率。示例将 SPI2 总线的 MOSI 与 MISO 用一根杜邦线短接,构成自发自收的环回回路。

1. SPI 外设

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
CS010
SCLK12
MISO13
MOSI11

只要驱动里指定的引脚与上表完全一致,驱动会自动走 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 引脚的设备,驱动会自动管理。

通用步骤:

  1. 包含头文件

    #include "driver/spi_master.h"

    并在 main/CMakeLists.txt 中声明依赖:REQUIRES esp_driver_spi

  2. 初始化总线:填写 spi_bus_config_t(MOSI / MISO / SCLK 引脚、最大传输字节数、DMA 通道),调用 spi_bus_initialize()。未使用的引脚(如纯写场景的 MISO)传 -1

  3. 添加设备:填写 spi_device_interface_config_t(CS 引脚、SPI 模式 0~3、时钟速率、队列深度等),调用 spi_bus_add_device(),获得设备句柄。

  4. 发起传输:构造 spi_transaction_t(数据长度、TX/RX 缓冲区),通过以下两种 API 之一发送:

    • spi_device_polling_transmit():轮询,调用线程忙等到结束。适合短传输、低延迟场景。
    • spi_device_transmit():中断驱动,调用线程阻塞让出 CPU。适合长传输或频率较低的传输。
  5. (可选)释放资源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 电路

需要使用的器件有:

用一根杜邦线把 GPIO11(MOSI)GPIO13(MISO) 短接。SCLK(GPIO12)与 CS(GPIO10)不需要外部接线,驱动会自动输出对应信号。

ESP32-S3-Zero 引脚图

ESP32-S3-Zero-Pinout

接线图

2.2 创建项目

  1. 创建一个项目。如果不清楚如何操作,请参考 从模板创建项目

  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 构建并烧录

  1. 配置烧录选项

    首先,在构建和烧录之前,请务必检查并设置正确的目标设备、串口和烧录方式。参考 第 2 节 运行示例 - 1.3 配置项目

    VS Code 工具栏

  2. 点击 VS Code 一键构建烧录监视图标 一键自动依次执行构建、烧录和监视。

  3. 烧录完成后,确保 GPIO11 和 GPIO13 已用杜邦线短接,串口监视器中应看到周期性的"Loopback OK":

    I (266) main_task: Calling app_main()
    I (276) example: Loopback OK: 16 bytes echoed back correctly
    I (1276) example: Loopback OK: 16 bytes echoed back correctly
    I (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_tspi_device_interface_config_tspi_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

3. 参考链接