【ESP32-IDF】高级外设开发4:SPI

系列文章目录

持续更新中…



前言

在嵌入式开发中,SPI(串行外设接口)是一种常用的高速同步串行通信接口。ESP32 作为一款高性能 MCU,集成了多组 SPI 控制器,支持多主从设备连接、双全工/半双工通信以及 DMA 数据传输等高级功能。本篇文章将深入解析 ESP32(以 ESP32-S3 为例)的 SPI 外设,从硬件架构到软件使用,帮助开发者掌握其高级应用技巧。


一、SPI概述

SPI 协议是由摩托罗拉公司提出的通讯协议 (Serial Peripheral Interface),即串行外围设备接口,是一种高速全双工的通信总线。它被广泛地使用在 ADC设备、LCD 等设备与 MCU 间,要求通讯速率较高的场合。

1.主要功能

ESP32 系列芯片集成了 4 个 SPI 控制器(SPI0/1/2/3)。其中 SPI0 和 SPI1 主要用于内部连接 Flash 和 PSRAM,不提供给用户;SPI2 和 SPI3 则作为通用 SPI(GP-SPI)对外开放,可用于连接各类 SPI 从设备。

SPI 控制器支持主机/从机模式,默认作为主机使用,可独立配置时钟频率(ESP32-S3 默认时钟源为 80 MHz APB,可分频得到常用频率,部分模式下最高支持 80MHz,特定八线模式下可达 120MHz)、数据传输模式(SPI Mode0/1/2/3,对应时钟极性CPOL和相位CPHA组合)、传输字节序(MSB或LSB优先)等。SPI 支持 全双工 同步收发(默认 MOSI/MISO 双线),也支持 半双工 模式(如三线 SPI,共用单数据线收发)。
GP-SPI2 和 GP-SPI3 支持的数据模式
在这里插入图片描述
ESP32 的 SPI 控制器具备多线并行传输能力,可工作在 Dual SPI、Quad SPI、Octal SPI等模式以提升吞吐量(常用于高速闪存、显示屏等外设)。在数据传输方面,SPI 硬件提供发送/接收 FIFO 缓冲,并且可以结合 DMA(直接存储器访问)实现大数据块的高速搬运,减少CPU负担。通过上述功能组合,ESP32 的 SPI 接口既适用于传感器等中低速外设,也能胜任显示屏、存储等大数据量高速场景。

2.SPI控制器架构

ESP32-S3 内部的 4 个 SPI 控制器架构如图所示
在这里插入图片描述
SPI0 和 SPI1 控制器共享一套外部总线信号(称作 SPI 主存储总线,包括 D/Q 数据线、CS0CS2 片选、CLK 时钟、WP/HD 辅助线等),通过硬件仲裁器实现对外部 Flash/PSRAM 的访问(SPI0 常用于缓存操作,SPI1 用于向 Flash 写入等)。由于这两路总线承载着程序存储器,IDF 驱动并不支持用户直接操作 SPI0/1 控制器。SPI2 和 SPI3 控制器则拥有各自独立的通用信号总线(通常也称 HSPI 和 VSPI),可自由映射到支持输出的任意 GPIO

在 ESP32-S3 中,SPI2 控制器提供6 条片选线(CS0-CS5),SPI3 提供3 条片选线(CS0-CS2),意味着单个 SPI 主机最多可挂载 6 个或 3 个从设备。硬件会根据片选自动控制总线占用,实现多设备的时分复用。此外,SPI1~SPI3 控制器共用两个 DMA 通道资源:当启用 DMA 传输时,硬件可在这两个 DMA 信道上调度数据搬运,从而实现高吞吐的连续读写。

每个 SPI 控制器内部包含发送/接收 FIFO(深度为 64 字节)、时钟分频器、模式控制逻辑等模块。在主模式下,ESP32-S3 的 SPI 控制器通过配置寄存器即可自动完成从 拉低 CS、发送指令/地址、读写数据,到释放 CS 的整个事务序列,期间支持硬件插入空周期以及精确的时序控制。ESP32 的 SPI 硬件架构为多主多从、高速大数据传输提供了灵活且强大的支撑。

3.SPI通信模式

SPI 通信由主设备产生时钟并发起传输。ESP32 的 SPI 主控模式下支持 4 种时序模式(Mode0/1/2/3),分别对应时钟空闲电平和数据采样时机的不同组合:(0,0)、(0,1)、(1,0)、(1,1)。这些模式可以通过设备配置的 mode 参数来设置,以匹配不同 SPI 从设备的时序要求。
GP-SPI 功能块图:
在这里插入图片描述
数据线方面,默认 SPI 使用 MISO 和 MOSI 两根数据线实现全双工通信——在一个时钟周期内,主机从 MOSI 发送1比特的同时,也从 MISO 接收1比特。如果外设不需要同时发送数据,或硬件只有单数据引脚,可以将 SPI 配置为 半双工 模式,此时主机可以使用同一引脚(连接在 SPI 的 MOSI 引脚上)分时发送和接收数据,即经典“三线 SPI”接口(CLK、DATA、CS)。

开启半双工模式的方法是在设备配置的标志位中设置 SPI_DEVICE_HALFDUPLEX(IDF 会自动管理数据线方向)。除了标准的单比特串行,ESP32 的 GP-SPI 控制器还支持 多路并行模式:例如 Dual SPI(双线)和 Quad SPI(四线)模式。在这些模式下,主机将同时使用 2 个或 4 个数据引脚进行并行传输,大幅提高有效带宽。这通常用于与支持多I/O模式的存储芯片或显示屏通信。要使用并行模式,需要硬件上将 SPI 的 WP/HD 等引脚连接到从设备,并在驱动中启用相应的总线标志(如 SPICOMMON_BUSFLAG_DUAL/QUAD 等)以及在事务中设置 SPI_TRANS_MODE_DIO/QIO 标志。

并行模式通常意味着通信只能半双工进行(因为数据线被复用为输出),因此驱动要求在多线模式下设备标志需包含 SPI_DEVICE_HALFDUPLEX。在大多数应用中,标准 4 线 SPI 已能满足需求,而当追求极致速度时,可考虑使用并行模式并合理调整时序以确保可靠性。

4.SPI数据帧与事务

**SPI 的主从通信以事务为单位完成。**一次完整的 SPI 主机事务通常包括以下阶段:主机拉低 CS(片选)以选中目标从设备,然后依次发送命令码(可选,016位)、地址(可选,064位)、插入若干空等待周期(Dummy,满足从设备时序要求),接着进入数据传输阶段,包括发送数据(写阶段)和/或接收数据(读阶段),最后主机释放 CS 结束该事务。

这些阶段是否存在及长度,取决于设备配置和每次事务配置。例如,对某些存储器或显示屏操作,可能需要在正式数据之前发出命令或地址信息;而对一般传感器可能只需简单的读写数据而无额外命令。ESP-IDF 提供的 spi_device_interface_config_t 结构体中有专门的字段用于配置默认的命令位数和地址位数,以及每次事务可以按需调整的数据长度。在执行事务时,驱动会根据这些配置自动完成前序命令/地址的发送,然后进行数据阶段。
主机模式下数据流控制:
在这里插入图片描述
SPI 的读写可以同时发生:在全双工模式下,当主机发送每一比特时,从设备的输出比特会同步被采集,这样读阶段和写阶段实际上重叠进行,事务总时长取决于两者中较长的一个。如果不希望同时读写(例如从设备要求先发后收),可以使用半双工模式,在事务配置中分别指定发送数据长度和接收数据长度,驱动将按先发送后接收的顺序完成。对于不需要的读或写相位,可以将对应缓冲区指针设为 NULL,SPI 控制器将自动跳过该阶段。
从机模式下数据流控制:

在这里插入图片描述
总的来说,ESP32 SPI 通过硬件支持灵活的事务分段和自动片选控制,使复杂协议的实现更加简洁高效。

5.DMA与传输性能

当进行小数据量传输时(比如几字节),SPI 主机驱动可以直接通过 CPU 向硬件 FIFO 寄存器写入/读取数据完成通信;这种方式开销低、速度快。但是对于较大数据块(几十上百字节乃至数KB),频繁的中断和字节搬运会给 CPU 带来较大负担。

为此,ESP32-S3 的 SPI 控制器支持 DMA(直接内存访问)传输:通过给 spi_bus_initialize 提供 DMA通道参数,驱动将在发送/接收超过 FIFO 深度的数据时,自动启用 DMA 控制器将数据块搬运到 SPI FIFO。这使得单次事务可以发送非常长的数据(IDF 默认在 DMA 模式下单次传输可达约4092字节,理论上可调整受内存限制),同时将 CPU 从逐字节搬运中解放出来。ESP32-S3 的 SPI2 和 SPI3 控制器各自配备 DMA请求接口(共享2个 DMA信道),通过配置可分别占用一个 DMA通道实现并行数据传输。需要注意:如果选择使用 DMA,则发送/接收缓冲区必须放在可被DMA访问的内存区域(例如内部 SRAM,并避免使用cache映射的PSRAM),并且最好满足 4 字节对齐,以发挥DMA最大效率。
主机模式下 DAM 控制的分段配置传输:
在这里插入图片描述

IDF 中提供了 heap_caps_malloc等API用于分配DMA合规的内存,也可以使用 spi_bus_dma_memory_alloc 辅助分配函数。另外,为确保高速下数据稳定,驱动允许用户设置从设备的信号采样延迟或调整采样点位置。通常在低于8MHz时无需调整,而更高速率下根据线长、电平翻转等情况,适当的延时设置可以改善可靠性。引脚选择方面,SPI 若使用GPIO矩阵映射引脚,由于引入了约 2ns 的延迟,稳定工作的最高全双工频率约为 26MHz,半双工约 40MHz;若全部使用IO_MUX指定的原生引脚,则可支持全双工 40MHz、半双工 80MHz的速率,甚至更高(实际最大受限于时钟源及从设备性能)。

因此在设计高速 SPI 总线时,尽量选用芯片的硬件接口管脚并降低连线电容,以获取最佳信号质量和速度。总体而言,通过 DMA、高速引脚和合理的时序配置,ESP32的 SPI 主接口可在高吞吐与低CPU占用之间取得良好平衡,满足苛刻的数据传输需求。

6.中断与驱动事件

ESP-IDF 的 SPI 主机驱动对底层硬件中断和状态变化进行了封装,用户通常不需要直接处理 SPI 中断。驱动在后台利用中断检测事务完成、队列调度等事件,并提供了回调机制供用户在特定时机(传输前后)执行操作。

在主模式下,如果采用异步队列接口(spi_device_queue_trans),驱动会在每个事务完成时通过中断将结果放入内部队列,用户可以通过 spi_device_get_trans_result 等API等待或轮询完成事件。在设备配置结构中还可以指定 pre_cb 和 post_cb 回调函数,分别会在每次事务开始前和结束后被ISR调用,可用于控制引脚、电源管理等(注意需放置于IRAM以满足中断上下文要求)。

当多个任务共享同一 SPI 设备时,由于 SPI 驱动线程非安全(访问同一设备需串行化),一种方式是使用 spi_device_acquire_bus/release_bus 手动锁定总线;更简单的做法是保证每个 SPI设备仅由一个任务访问,或在应用层对共享访问加互斥锁。合理利用驱动提供的中断/队列机制,可以实现非阻塞的 SPI 通信和多设备的高效调度,充分发挥 SPI 总线的并发能力和数据吞吐。

二、SPI类型定义及相关API

需包含的公共头文件:#include “driver/spi_master.h”
SPI类型定义

// ==========================================================  
// SPI 主机编号类型  
// ==========================================================  
typedef int spi_host_device_t;          // SPI 主机控制器代号  
#define SPI1_HOST  (0)                 // SPI1(一般用于内部Flash,不开放)  
#define SPI2_HOST  (1)                 // SPI2(用户可用,一般默认HSPI)  
#define SPI3_HOST  (2)                 // SPI3(用户可用,一般默认VSPI)  
#define SPI_HOST_MAX  (3)  

// SPI主机别名(兼容旧称呼)  
#define HSPI_HOST  SPI2_HOST  
#define VSPI_HOST  SPI3_HOST  

// ==========================================================  
// SPI DMA通道选择  
// ==========================================================  
typedef enum {  
    SPI_DMA_DISABLED = 0,   // 不启用 DMA,将受限于 FIFO 长度  
    SPI_DMA_CH1      = 1,   // 使用 DMA通道1  
    SPI_DMA_CH2      = 2,   // 使用 DMA通道2  
    SPI_DMA_CH_AUTO  = 3    // 自动分配可用 DMA通道  
} spi_dma_chan_t;  

// ==========================================================  
// SPI 总线初始化配置结构  
// ==========================================================  
typedef struct {  
    int mosi_io_num;         // MOSI 引脚编号(主出从入数据线),-1表示不使用  
    int miso_io_num;         // MISO 引脚编号(主入从出数据线),-1表示不使用  
    int sclk_io_num;         // SCLK 引脚编号(时钟线),-1表示不使用  
    int quadwp_io_num;       // WP 引脚编号(写保护,用于Quad模式),-1表示不使用  
    int quadhd_io_num;       // HD 引脚编号(保持,用于Quad模式),-1表示不使用  
    int data4_io_num;        // 数据线4(Octal模式),-1表示不使用  
    int data5_io_num;        // 数据线5(Octal模式),-1表示不使用  
    int data6_io_num;        // 数据线6(Octal模式),-1表示不使用  
    int data7_io_num;        // 数据线7(Octal模式),-1表示不使用  
    bool data_io_default_level;  // 空闲时数据线默认电平(输出使能时),一般为false  
    int max_transfer_sz;     // 单次最大传输字节数(DMA模式下默认4092字节,非DMA模式下默认为 SOC_SPI_MAXIMUM_BUFFER_SIZE)  
    uint32_t flags;          // 总线能力标志位(SPICOMMON_BUSFLAG_* 的组合,用于校验硬件能力)  
    esp_intr_cpu_affinity_t isr_cpu_id; // 中断分配到的CPU核心(默认不指定)  
    int intr_flags;          // 中断分配标志(ESP_INTR_FLAG_LEVELx/IRAM等,一般使用默认0或ESP_INTR_FLAG_IRAM)  
} spi_bus_config_t;  

// ==========================================================  
// SPI 从设备接口配置结构(设备初始化时提供)  
// ==========================================================  
typedef struct {  
    uint8_t  command_bits;   // 命令阶段默认位宽(0~16位)  
    uint8_t  address_bits;   // 地址阶段默认位宽(0~64位)  
    uint8_t  dummy_bits;     // 地址阶段后插入的空等待时钟周期数  
    uint8_t  mode;           // SPI 模式(0~3,对应 (CPOL, CPHA))  
    // uint8_t  非预留字段 (占位,以4字节对齐)  
    uint16_t duty_cycle_pos; // 时钟正脉冲占空比(1~256,对应占空比百分比=该值/256,128=50%)  
    uint16_t cs_ena_pretrans;// 传输开始前 CS 提前拉低的时钟周期数(0~16),半双工模式下有效  
    uint8_t  cs_ena_posttrans;// 传输结束后 CS 保持低电平的时钟周期数(0~16)  
    int      clock_speed_hz; // 时钟频率(Hz)  
    int      input_delay_ns; // 输入信号延迟(ns)补偿(从属设备数据准备时间,0表示不延迟)  
    // 新版clock_source和sample_point省略,使用默认APB时钟源  
    int      spics_io_num;   // 该设备使用的CS引脚(GPIO编号),-1表示不由驱动控制  
    uint32_t flags;          // 设备标志位(SPI_DEVICE_*,如 LSBFIRST/3WIRE/HALFDUPLEX 等)  
    int      queue_size;     // 事务队列长度(驱动可同时挂起的未完成事务数)  
    transaction_cb_t pre_cb; // 每次传输开始前的回调(中断内调用,需放IRAM)  
    transaction_cb_t post_cb;// 每次传输结束后的回调(中断内调用,需放IRAM)  
} spi_device_interface_config_t;  

// SPI 设备标志位宏(部分常用列举)  
#define SPI_DEVICE_TXBIT_LSBFIRST   (1<<0)  // 发送数据使用LSB优先(默认MSB先)  
#define SPI_DEVICE_RXBIT_LSBFIRST   (1<<1)  // 接收数据使用LSB优先  
#define SPI_DEVICE_BIT_LSBFIRST     (SPI_DEVICE_TXBIT_LSBFIRST | SPI_DEVICE_RXBIT_LSBFIRST)  // 发送接收均LSB优先  
#define SPI_DEVICE_3WIRE            (1<<2)  // 启用3线模式(共用MOSI引脚收发,等效半双工)  
#define SPI_DEVICE_POSITIVE_CS      (1<<3)  // CS信号极性取反(默认低有效,设置此标志后为高有效)  
#define SPI_DEVICE_HALFDUPLEX       (1<<4)  // 半双工模式(发送完再接收,不同时进行)  
// ...(其他标志如 SPI_DEVICE_NO_DUMMY/SPI_DEVICE_CLK_AS_CS 等,可根据需要使用)  

// ==========================================================  
// SPI 事务描述结构(每次传输参数)  
// ==========================================================  
typedef struct {  
    uint32_t flags;      // 事务标志(SPI_TRANS_*,如 VARIABLE_ADDR/CMD, CS_KEEP_ACTIVE 等)  
    uint16_t cmd;        // 本次事务使用的命令值(实际发送的位宽由 device 的 command_bits 定义)  
    uint64_t addr;       // 本次事务使用的地址值(实际发送位宽由 address_bits 定义)  
    size_t   length;     // 本次发送数据总长度(bit为单位)  
    size_t   rxlength;   // 本次接收数据长度(bit为单位,不大于length,全双工为0则默认为length)  
    void    *user;       // 用户自定义指针(可用来标识事务来源等)  
    const void *tx_buffer; // 发送数据缓冲区指针(若无发送则可为 NULL)  
    uint8_t tx_data[4];  // 短数据直接存放在此(设置 SPI_TRANS_USE_TXDATA 时启用)  
    void    *rx_buffer;  // 接收数据缓冲区指针(若不需要接收可为 NULL)  
    uint8_t rx_data[4];  // 短数据直接接收至此(设置 SPI_TRANS_USE_RXDATA 时启用)  
} spi_transaction_t;  

// SPI 事务标志位(常用)  
#define SPI_TRANS_VARIABLE_CMD  (1<<0)  // 本次事务使用非常规长度的命令阶段(spi_transaction_ext_t 扩展)  
#define SPI_TRANS_VARIABLE_ADDR (1<<1)  // 使用非常规长度的地址阶段  
#define SPI_TRANS_USE_TXDATA    (1<<2)  // 使用 tx_data 中的数据而非缓冲区指针  
#define SPI_TRANS_USE_RXDATA    (1<<3)  // 接收数据直接存入 rx_data  
#define SPI_TRANS_CS_KEEP_ACTIVE (1<<4) // 事务结束后保持 CS 拉低(需配合后续事务或手动控制)  

// SPI 设备句柄类型  
typedef struct spi_device_t *spi_device_handle_t;  

SPI相关API

// ======================= SPI 总线控制 =======================  
/**
 * @brief  初始化 SPI 总线  
 *
 * @param host_id    SPI 主机端口号 (SPI2_HOST / SPI3_HOST)  
 * @param bus_config 指向总线配置结构体的指针  
 * @param dma_chan   DMA通道选择:
 *                   - SPI_DMA_DISABLED 不使用DMA(限制传输长度)  
 *                   - SPI_DMA_CHx 使用指定 DMA通道 (ESP32-S3 通常用 1 或 2)  
 *                   - SPI_DMA_CH_AUTO 由驱动自动分配可用 DMA通道  
 *
 * @note  SPI0/SPI1 为内部总线,驱动不支持初始化 (调用此函数会返回错误)。  
 *        调用成功后,即完成GPIO引脚矩阵配置、FIFO和中断初始化等。  
 *        如果指定了 DMA 通道,需确保后续使用的传输缓冲区在 DMA 可访问内存中。  
 * @return  
 *       - ESP_OK: 初始化成功  
 *       - ESP_ERR_INVALID_ARG: 参数非法  
 *       - ESP_ERR_NOT_FOUND: 无可用DMA通道(当请求AUTO时)  
 *       - ESP_ERR_INVALID_STATE: 指定主机已经初始化过  
 */  
esp_err_t spi_bus_initialize(spi_host_device_t host_id, const spi_bus_config_t *bus_config, spi_dma_chan_t dma_chan);  

/**
 * @brief  释放 SPI 总线  
 *
 * @param host_id SPI 主机端口号  
 * @note  调用此函数前,需确保该总线上的所有设备已被移除 (spi_bus_remove_device)。  
 * @return  
 *       - ESP_OK: 释放成功  
 *       - ESP_ERR_INVALID_ARG: 参数非法  
 *       - ESP_ERR_INVALID_STATE: 总线未初始化 或 上面仍挂有未移除的设备  
 */  
esp_err_t spi_bus_free(spi_host_device_t host_id);  

// ======================= SPI 设备控制 =======================  
/**
 * @brief  向 SPI 总线挂载一个从属设备  
 *
 * @param host_id   SPI 主机端口号 (SPI2_HOST / SPI3_HOST)  
 * @param dev_config 指向设备接口配置结构体的指针  
 * @param handle    输出:返回的设备句柄地址  
 *
 * @note  此函数会根据 dev_config 分配并初始化一个 SPI 从设备,
 *        包括为该设备分配一个 CS (片选) 引脚并通过 GPIO Matrix 连接。  
 *        ESP32-S3 的 SPI2 支持最多 6 个 CS,引脚编号通常为 CS0~CS5;SPI3 支持 3 个。  
 *        如果超出主机可用的 CS 插槽数量,将返回 ESP_ERR_NOT_FOUND 错误。  
 *        默认支持最高 40MHz (IO_MUX引脚) / 26MHz (GPIO矩阵) 的速度,全双工模式下矩阵引脚建议不超过 26MHz:contentReference[oaicite:15]{index=15}。  
 * @return  
 *       - ESP_OK: 设备添加成功  
 *       - ESP_ERR_INVALID_ARG: 参数非法(比如配置冲突)  
 *       - ESP_ERR_NOT_FOUND: 主机没有空余 CS 插槽可用  
 *       - ESP_ERR_NO_MEM: 内存分配失败  
 */  
esp_err_t spi_bus_add_device(spi_host_device_t host_id, const spi_device_interface_config_t *dev_config, spi_device_handle_t *handle);  

/**
 * @brief  从 SPI 总线上移除一个从属设备  
 *
 * @param handle 要移除的设备句柄  
 * @return  
 *       - ESP_OK: 移除成功  
 *       - ESP_ERR_INVALID_ARG: 参数非法  
 *       - ESP_ERR_INVALID_STATE: 设备已被移除或未曾添加  
 *
 * @note 调用后该设备占用的CS引脚和资源将释放,可用于添加新设备 
 */  
esp_err_t spi_bus_remove_device(spi_device_handle_t handle);  

// ======================= SPI 数据传输 =======================  
/**
 * @brief  阻塞方式发送一个 SPI 事务  
 *
 * @param handle      设备句柄(spi_bus_add_device取得)  
 * @param trans_desc  指向事务描述结构体的指针(需提前填充好发送/接收缓冲等)  
 * @return  
 *       - ESP_OK: 传输成功,数据已发送/接收完成  
 *       - ESP_ERR_INVALID_ARG: 参数非法(如 trans_desc 内容不合法)  
 *
 * @note  此函数相当于依次调用 spi_device_queue_trans() 和 spi_device_get_trans_result(),  
 *        内部会等待传输完成再返回。因此不应在已有挂起事务未完成时再次调用本函数。  
 *        默认情况下,同一设备上的串行调用是线程安全的,但若多个任务并发访问同一设备句柄,需要自行确保互斥。  
 */  
esp_err_t spi_device_transmit(spi_device_handle_t handle, spi_transaction_t *trans_desc);  

/**
 * @brief  轮询方式发送 SPI 事务  
 *
 * @param handle      设备句柄  
 * @param trans_desc  事务描述结构指针  
 * @return ESP_OK 表示传输完成且成功  
 *
 * @note  本函数与 spi_device_transmit 类似,也会等待传输完成,但采用 “忙轮询” 方式驱动硬件而不使用中断。  
 *        这种方式适用于短小事务且对实时性要求高的场景,可避免中断调度的延迟。  
 *        调用前后无需 acquire_bus,但不同任务并发仍需注意互斥。  
 */  
esp_err_t spi_device_polling_transmit(spi_device_handle_t handle, spi_transaction_t *trans_desc);  

/**
 * @brief  异步排队一个 SPI 事务(中断驱动)  
 *
 * @param handle      设备句柄  
 * @param trans_desc  事务描述结构指针  
 * @param ticks_to_wait 等待可用队列空间的超时时间(RTOS ticks,portMAX_DELAY 表示永不超时)  
 * @return  
 *       - ESP_OK: 事务已成功加入队列  
 *       - ESP_ERR_TIMEOUT: 在指定时间内队列无空闲,事务未加入  
 *       - ESP_ERR_NO_MEM: 内部申请DMA临时缓冲失败(极少见)  
 *       - ESP_ERR_INVALID_ARG: 参数非法,或指定了不支持的标志组合等  
 *       - ESP_ERR_INVALID_STATE: 前一个事务未完成(正常不会发生,因为队列有容量控制)  
 *
 * @note  将事务加入驱动队列后即立即返回,SPI硬件会通过中断在后台执行传输。  
 *        可以通过 spi_device_get_trans_result 获取完成的事务结果。  
 *        同一设备上的事务将按调用顺序依次执行;多个设备间则由驱动自动仲裁总线。  
 */  
esp_err_t spi_device_queue_trans(spi_device_handle_t handle, spi_transaction_t *trans_desc, TickType_t ticks_to_wait);  

/**
 * @brief  获取一个已完成的 SPI 异步事务结果  
 *
 * @param handle       设备句柄  
 * @param trans_desc   输出:指向完成的事务描述指针的存放地址  
 * @param ticks_to_wait 最长等待时间(RTOS ticks)  
 * @return  
 *       - ESP_OK: 成功获取到已完成的事务  
 *       - ESP_ERR_INVALID_ARG: 参数非法  
 *       - ESP_ERR_TIMEOUT: 在指定时间内没有事务完成  
 *
 * @note  本函数用于与 spi_device_queue_trans 配合,实现异步传输的结果获取。  
 *        如果在队列中尚有未完成的事务,本函数会等待直至有事务完成或超时。  
 *        获得结果后,可检查 trans_desc->rx_buffer 中的数据或其他标志,并且可以重复利用或释放该事务结构。  
 */  
esp_err_t spi_device_get_trans_result(spi_device_handle_t handle, spi_transaction_t **trans_desc, TickType_t ticks_to_wait);  

/**
 * @brief 手动占用 SPI 总线以独占访问  
 *
 * @param handle 设备句柄  
 * @param ticks_to_wait 等待总线可用的时间  
 * @return ESP_OK 表示成功占用总线  
 *
 * @note 调用此函数后,其他设备的事务将被暂挂,直到调用 spi_device_release_bus 释放总线。  
 *       适用于需连续执行一组事务且中间不插入其他设备通信的场景。  
 *       使用完毕后务必调用 release_bus 释放,否则会阻塞低优先级任务的SPI通信。  
 */  
esp_err_t spi_device_acquire_bus(spi_device_handle_t handle, TickType_t ticks_to_wait);  

/**
 * @brief 释放通过 spi_device_acquire_bus 占用的 SPI 总线  
 *
 * @param handle 设备句柄  
 */  
esp_err_t spi_device_release_bus(spi_device_handle_t handle);  

三、SPI示例程序

在 ESP32S3 上通过SPI驱动 LCD 屏幕显示图片
main.c

#include <stdio.h>
#include "lcd.h"
#include "yingwu.h"

void app_main(void)
{
    bsp_i2c_init();
    pca9557_init();
    bsp_lcd_init(); // 液晶屏初始化
    lcd_draw_pictrue(0, 0, 320, 240, gImage_yingwu); // 显示3只鹦鹉图片
    while(1)
    {
        
    }
}

lcd.c

#include "lcd.h"

static const char *TAG = "BSP";

esp_err_t bsp_i2c_init(void)
{
    i2c_config_t i2c_conf = {
        .mode = I2C_MODE_MASTER,
        .sda_io_num = BSP_I2C_SDA,
        .sda_pullup_en = GPIO_PULLUP_ENABLE,
        .scl_io_num = BSP_I2C_SCL,
        .scl_pullup_en = GPIO_PULLUP_ENABLE,
        .master.clk_speed = BSP_I2C_FREQ_HZ};
    i2c_param_config(BSP_I2C_NUM, &i2c_conf);

    return i2c_driver_install(BSP_I2C_NUM, i2c_conf.mode, 0, 0, 0);
}

// 读取PCA9557寄存器的值
esp_err_t pca9557_register_read(uint8_t reg_addr, uint8_t *data, size_t len)
{
    return i2c_master_write_read_device(BSP_I2C_NUM, PCA9557_SENSOR_ADDR, &reg_addr, 1, data, len, 1000 / portTICK_PERIOD_MS);
}

// 给PCA9557的寄存器写值
esp_err_t pca9557_register_write_byte(uint8_t reg_addr, uint8_t data)
{
    uint8_t write_buf[2] = {reg_addr, data};

    return i2c_master_write_to_device(BSP_I2C_NUM, PCA9557_SENSOR_ADDR, write_buf, sizeof(write_buf), 1000 / portTICK_PERIOD_MS);
}

// 初始化PCA9557 IO扩展芯片
void pca9557_init(void)
{
    // 写入控制引脚默认值 DVP_PWDN=1  PA_EN = 0  LCD_CS = 1
    pca9557_register_write_byte(PCA9557_OUTPUT_PORT, 0x05);
    // 把PCA9557芯片的IO1 IO1 IO2设置为输出 其它引脚保持默认的输入
    pca9557_register_write_byte(PCA9557_CONFIGURATION_PORT, 0xf8);
}

// 设置PCA9557芯片的某个IO引脚输出高低电平
esp_err_t pca9557_set_output_state(uint8_t gpio_bit, uint8_t level)
{
    uint8_t data;
    esp_err_t res = ESP_FAIL;

    pca9557_register_read(PCA9557_OUTPUT_PORT, &data, 1);
    res = pca9557_register_write_byte(PCA9557_OUTPUT_PORT, SET_BITS(data, gpio_bit, level));

    return res;
}

// 控制 PCA9557_LCD_CS 引脚输出高低电平 参数0输出低电平 参数1输出高电平
void lcd_cs(uint8_t level)
{
    pca9557_set_output_state(LCD_CS_GPIO, level);
}

// 背光PWM初始化
esp_err_t bsp_display_brightness_init(void)
{
    // Setup LEDC peripheral for PWM backlight control
    const ledc_channel_config_t LCD_backlight_channel = {
        .gpio_num = BSP_LCD_BACKLIGHT,
        .speed_mode = LEDC_LOW_SPEED_MODE,
        .channel = LCD_LEDC_CH,
        .intr_type = LEDC_INTR_DISABLE,
        .timer_sel = 0,
        .duty = 0,
        .hpoint = 0,
        .flags.output_invert = true};
    const ledc_timer_config_t LCD_backlight_timer = {
        .speed_mode = LEDC_LOW_SPEED_MODE,
        .duty_resolution = LEDC_TIMER_10_BIT,
        .timer_num = 0,
        .freq_hz = 5000,
        .clk_cfg = LEDC_AUTO_CLK};

    ESP_ERROR_CHECK(ledc_timer_config(&LCD_backlight_timer));
    ESP_ERROR_CHECK(ledc_channel_config(&LCD_backlight_channel));

    return ESP_OK;
}

// 定义液晶屏句柄
static esp_lcd_panel_handle_t panel_handle = NULL;
esp_lcd_panel_io_handle_t io_handle = NULL;

// 设置液晶屏颜色
void lcd_set_color(uint16_t color)
{
    // 分配内存 这里分配了液晶屏一行数据需要的大小
    uint16_t *buffer = (uint16_t *)heap_caps_malloc(BSP_LCD_H_RES * sizeof(uint16_t), MALLOC_CAP_8BIT | MALLOC_CAP_SPIRAM);

    if (NULL == buffer)
    {
        ESP_LOGE(TAG, "Memory for bitmap is not enough");
    }
    else
    {
        for (size_t i = 0; i < BSP_LCD_H_RES; i++) // 给缓存中放入颜色数据
        {
            buffer[i] = color;
        }
        for (int y = 0; y < 240; y++) // 显示整屏颜色
        {
            esp_lcd_panel_draw_bitmap(panel_handle, 0, y, 320, y + 1, buffer);
        }
        free(buffer); // 释放内存
    }
}

// 背光亮度设置
esp_err_t bsp_display_brightness_set(int brightness_percent)
{
    if (brightness_percent > 100)
    {
        brightness_percent = 100;
    }
    else if (brightness_percent < 0)
    {
        brightness_percent = 0;
    }

    ESP_LOGI(TAG, "Setting LCD backlight: %d%%", brightness_percent);
    // LEDC resolution set to 10bits, thus: 100% = 1023
    uint32_t duty_cycle = (1023 * brightness_percent) / 100;
    ESP_ERROR_CHECK(ledc_set_duty(LEDC_LOW_SPEED_MODE, LCD_LEDC_CH, duty_cycle));
    ESP_ERROR_CHECK(ledc_update_duty(LEDC_LOW_SPEED_MODE, LCD_LEDC_CH));

    return ESP_OK;
}

// 关闭背光
esp_err_t bsp_display_backlight_off(void)
{
    return bsp_display_brightness_set(0);
}

// 打开背光 最亮
esp_err_t bsp_display_backlight_on(void)
{
    return bsp_display_brightness_set(100);
}

// 液晶屏初始化
esp_err_t bsp_display_new(void)
{
    esp_err_t ret = ESP_OK;
    // 背光初始化
    ESP_RETURN_ON_ERROR(bsp_display_brightness_init(), TAG, "Brightness init failed");
    // 初始化SPI总线
    ESP_LOGD(TAG, "Initialize SPI bus");
    const spi_bus_config_t buscfg = {
        .sclk_io_num = BSP_LCD_SPI_CLK,
        .mosi_io_num = BSP_LCD_SPI_MOSI,
        .miso_io_num = GPIO_NUM_NC,
        .quadwp_io_num = GPIO_NUM_NC,
        .quadhd_io_num = GPIO_NUM_NC,
        .max_transfer_sz = BSP_LCD_H_RES * BSP_LCD_V_RES * sizeof(uint16_t),
    };
    ESP_RETURN_ON_ERROR(spi_bus_initialize(BSP_LCD_SPI_NUM, &buscfg, SPI_DMA_CH_AUTO), TAG, "SPI init failed");
    // 液晶屏控制IO初始化
    ESP_LOGD(TAG, "Install panel IO");
    const esp_lcd_panel_io_spi_config_t io_config = {
        .dc_gpio_num = BSP_LCD_DC,
        .cs_gpio_num = BSP_LCD_SPI_CS,
        .pclk_hz = BSP_LCD_PIXEL_CLOCK_HZ,
        .lcd_cmd_bits = LCD_CMD_BITS,
        .lcd_param_bits = LCD_PARAM_BITS,
        .spi_mode = 2,
        .trans_queue_depth = 10,
    };
    ESP_GOTO_ON_ERROR(esp_lcd_new_panel_io_spi((esp_lcd_spi_bus_handle_t)BSP_LCD_SPI_NUM, &io_config, &io_handle), err, TAG, "New panel IO failed");
    // 初始化液晶屏驱动芯片ST7789
    ESP_LOGD(TAG, "Install LCD driver");
    const esp_lcd_panel_dev_config_t panel_config = {
        .reset_gpio_num = BSP_LCD_RST,
        .rgb_ele_order = LCD_RGB_ELEMENT_ORDER_RGB,
        .bits_per_pixel = BSP_LCD_BITS_PER_PIXEL,
    };
    ESP_GOTO_ON_ERROR(esp_lcd_new_panel_st7789(io_handle, &panel_config, &panel_handle), err, TAG, "New panel failed");

    esp_lcd_panel_reset(panel_handle);               // 液晶屏复位
    lcd_cs(0);                                       // 拉低CS引脚
    esp_lcd_panel_init(panel_handle);                // 初始化配置寄存器
    esp_lcd_panel_invert_color(panel_handle, true);  // 颜色反转
    esp_lcd_panel_swap_xy(panel_handle, true);       // 显示翻转
    esp_lcd_panel_mirror(panel_handle, true, false); // 镜像

    return ret;

err:
    if (panel_handle)
    {
        esp_lcd_panel_del(panel_handle);
    }
    if (io_handle)
    {
        esp_lcd_panel_io_del(io_handle);
    }
    spi_bus_free(BSP_LCD_SPI_NUM);
    return ret;
}

// LCD显示初始化
esp_err_t bsp_lcd_init(void)
{
    esp_err_t ret = ESP_OK;

    ret = bsp_display_new();                             // 液晶屏驱动初始化
    lcd_set_color(0x0000);                               // 设置整屏背景黑色
    ret = esp_lcd_panel_disp_on_off(panel_handle, true); // 打开液晶屏显示
    ret = bsp_display_backlight_on();                    // 打开背光显示

    return ret;
}

// 显示图片
void lcd_draw_pictrue(int x_start, int y_start, int x_end, int y_end, const unsigned char *gImage)
{
    // 分配内存 分配了需要的字节大小 且指定在外部SPIRAM中分配
    size_t pixels_byte_size = (x_end - x_start) * (y_end - y_start) * 2;
    uint16_t *pixels = (uint16_t *)heap_caps_malloc(pixels_byte_size, MALLOC_CAP_8BIT | MALLOC_CAP_SPIRAM);
    if (NULL == pixels)
    {
        ESP_LOGE(TAG, "Memory for bitmap is not enough");
        return;
    }
    memcpy(pixels, gImage, pixels_byte_size);                                                    // 把图片数据拷贝到内存
    esp_lcd_panel_draw_bitmap(panel_handle, x_start, y_start, x_end, y_end, (uint16_t *)pixels); // 显示整张图片数据
    heap_caps_free(pixels);                                                                      // 释放内存
}

lcd.h

#ifndef LCD_H
#define LCD_H

#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_lcd_types.h"
#include "esp_lcd_panel_io.h"
#include "esp_lcd_panel_vendor.h"
#include "esp_lcd_panel_ops.h"
#include "driver/ledc.h"
#include "driver/spi_master.h"
#include "esp_err.h"
#include "esp_log.h"
#include "esp_check.h"
#include "driver/i2c.h"
#include <string.h>

#define SET_BITS(_m, _s, _v) ((_v) ? (_m) | ((_s)) : (_m) & ~((_s)))

#define BSP_I2C_SDA (GPIO_NUM_1) // SDA引脚
#define BSP_I2C_SCL (GPIO_NUM_2) // SCL引脚
#define BSP_I2C_NUM (0)          // I2C外设
#define BSP_I2C_FREQ_HZ 100000   // 100kHz

#define PCA9557_INPUT_PORT 0x00
#define PCA9557_OUTPUT_PORT 0x01
#define PCA9557_POLARITY_INVERSION_PORT 0x02
#define PCA9557_CONFIGURATION_PORT 0x03
#define PCA9557_SENSOR_ADDR 0x19 /*!< Slave address of the MPU9250 sensor */
#define LCD_CS_GPIO BIT(0)       // PCA9557_GPIO_NUM_1
#define PA_EN_GPIO BIT(1)        // PCA9557_GPIO_NUM_2
#define DVP_PWDN_GPIO BIT(2)     // PCA9557_GPIO_NUM_3



#define BSP_LCD_PIXEL_CLOCK_HZ (80 * 1000 * 1000)
#define BSP_LCD_SPI_NUM (SPI3_HOST)
#define LCD_CMD_BITS (8)
#define LCD_PARAM_BITS (8)
#define BSP_LCD_BITS_PER_PIXEL (16)
#define LCD_LEDC_CH LEDC_CHANNEL_0

#define BSP_LCD_H_RES (320)
#define BSP_LCD_V_RES (240)

#define BSP_LCD_SPI_MOSI (GPIO_NUM_40)
#define BSP_LCD_SPI_CLK (GPIO_NUM_41)
#define BSP_LCD_SPI_CS (GPIO_NUM_NC)
#define BSP_LCD_DC (GPIO_NUM_39)
#define BSP_LCD_RST (GPIO_NUM_NC)
#define BSP_LCD_BACKLIGHT (GPIO_NUM_42)

// 函数声明
esp_err_t bsp_i2c_init(void);
esp_err_t pca9557_register_read(uint8_t reg_addr, uint8_t *data, size_t len);
esp_err_t pca9557_register_write_byte(uint8_t reg_addr, uint8_t data);
void pca9557_init(void);
esp_err_t bsp_lcd_init(void);
esp_err_t bsp_display_brightness_init(void);
esp_err_t bsp_display_new(void);
void lcd_draw_pictrue(int x_start, int y_start, int x_end, int y_end, const unsigned char *gImage);

#endif // !LCD_H

总结

本文围绕 ESP32 的 SPI 外设,从硬件资源、通信机制到软件接口进行了全面的介绍。在实际应用中,开发者可根据需求选择合适的传输方式:对于少量数据的即时通信,可直接使用阻塞发送;对于大量数据或需要并发处理的场景,可采用 DMA 加中断队列的异步方式以降低 CPU 占用。在后续的项目中,开发者可以依据本文提供的知识框架,快速上手 SPI 编程并针对性能瓶颈进行优化,从而充分发挥 ESP32 SPI 外设的强大能力。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值