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

系列文章目录

持续更新中…


文章目录

  • 系列文章目录
  • 前言
  • 一、SPI概述
    • 1.主要功能
    • 2.SPI控制器架构
    • 3.SPI通信模式
    • 4.SPI数据帧与事务
    • 5.DMA与传输性能
    • 6.中断与驱动事件
  • 二、SPI类型定义及相关API
  • 三、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 = 1pca9557_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 controlconst 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% = 1023uint32_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");// 初始化液晶屏驱动芯片ST7789ESP_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 外设的强大能力。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/bicheng/94367.shtml
繁体地址,请注明出处:http://hk.pswp.cn/bicheng/94367.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

遥感机器学习入门实战教程|Sklearn案例⑧:评估指标(metrics)全解析

很多同学问&#xff1a;“模型好不好&#xff0c;怎么量化&#xff1f;” 本篇系统梳理 sklearn.metrics 中常用且“够用”的多分类指标&#xff0c;并给出一段可直接运行的示例代码&#xff0c;覆盖&#xff1a;准确率、宏/微/加权 F1、Kappa、MCC、混淆矩阵&#xff08;计数/…

【Bluedroid】深入解析A2DP SBC编码器初始化(a2dp_sbc_encoder_init)

SBC(Subband Coding)作为蓝牙 A2DP 协议的标准编解码器,其编码器的初始化与参数配置直接影响音频传输的音质、效率与兼容性。本文基于Andoird A2DP 协议栈源码,系统剖析 SBC 编码器的初始化流程,包括核心参数(比特池、采样率、声道模式等)的解析、计算与动态调整逻辑,以…

linux shell测试函数

在 C 语言中&#xff0c;int main(int argc, char *argv[])是程序的入口函数&#xff0c;而​​在 main函数中调用专门的测试逻辑&#xff08;如测试函数&#xff09;​​的程序结构&#xff0c;通常被称为​​测试程序&#xff08;Test Program&#xff09;​​或​​测试驱动…

【Java SE】抽象类、接口与Object类

文章目录一、 抽象类&#xff08;Abstract Class&#xff09;1.1 什么是抽象类&#xff1f;1.2 抽象类的语法1.2.1 定义抽象类1.2.2 继承抽象类1.3 抽象类的特性1.3.1 不能直接实例化1.3.2 抽象方法的限制1.3.3 抽象类可以包含构造方法1.3.4 抽象类不一定包含抽象方法1.3.5 抽象…

Autodl 创建新虚拟环境 python3.9

问题&#xff1a;本人在autodl上保存的环境因为很长时间没有开机&#xff0c;autodl竟然给我删除了。后来看了官网的介绍我才发现&#xff0c;原来15天不开机&#xff0c;autodl就会自动释放实例。 因此&#xff0c;我就自己重新选了一个虚拟环境&#xff0c;从头开始配置。 GP…

应急响应靶机-WindowsServer2022挖矿事件

依旧手痒开局&#xff0c;知攻善防实验室的原创靶机 https://mp.weixin.qq.com/s/URrNHvQSnFKOyefHKXKjQQ 相关账户密码&#xff1a; Administrator/zgsf123 注意&#xff1a;做个原始快照&#xff08;方便日后复习&#xff09;&#xff0c;安装VMware tool&#xff08;安装后图…

PCB电路设计学习3 电路原理图设计 元件PCB封装设计与添加

目录PCB电路设计学习3五、电路原理图设计5.1 32个发光二极管电路5.2 单片机外围电路5.3 供电与程序下载电路5.4 连接各部分网络&#xff0c;绘制边框和说明六、元件PCB封装设计与添加6.1 名词解释6.2 绘制PCB附学习参考网址欢迎大家有问题评论交流 (* ^ ω ^)PCB电路设计学习3 …

redis---常用数据类型及内部编码

Redis 中每种常用数据类型都对应多种内部编码&#xff0c;这些编码会根据数据特征&#xff08;如大小、数量&#xff09;自动切换&#xff0c;以平衡存储效率和操作性能。1.字符串&#xff08;String&#xff09;用途&#xff1a;存储文本、数字或二进制数据&#xff0c;是最基…

crypto.randomUUID is not a function

在本地运行时 crypto.randomUUID 好使&#xff0c;build 后放到服务器上用域名访问就不好使。原因&#xff1a;浏览器策略&#xff0c;浏览器在非https、localhost的环境中访问时&#xff0c;crypto.randomUUID 是不可用的开发时使用的是localhost正常访问 生产临时使用的是htt…

【思考】什么是服务器?什么是服务?什么是部署?

文章目录1 什么是服务器&#xff1f;什么是服务&#xff1f;端口是什么意思&#xff1f;2 什么是部署&#xff1f;1 什么是服务器&#xff1f;什么是服务&#xff1f;端口是什么意思&#xff1f; 服务器本质是一台运行着程序的电脑&#xff0c;它可以运行着很多程序&#xff0c…

自动驾驶导航信号使用方式调研

1 总结 本文调研在给定导航信号后&#xff0c;如何在端到端架构下&#xff0c;利用导航信息引导轨迹生成。 目前主流的方案可以分为2种。一种是将导航作为“前置引导”深度融入轨迹生成过程&#xff08;导航前置型&#xff09;&#xff1b;另一种则是将导航作为“后置评价”标准…

玳瑁的嵌入式日记D21-08020(数据结构)

双向链表double link listtypedef struct dou_node { DATATYPE data; struct dou_node *prev; struct dou_node *next; }DouLinkNode;双向链表&#xff1a;节点 数据 NEXT PREV . 手撕代码(增加删除) 增加&#xff0c;删除的操作&#xff0c; 需要 tmp 停止待操作节点的前一…

Uipath查找元素 查找子元素 获取属性活动组合使用示例

Uipath 查找元素 查找子元素 获取属性组合使用示例使用场景案例介绍项目流程图附加浏览器查找元素查找子元素遍历循环获取属性点击元素使用场景 在实际场景中&#xff0c;有时需RPA自动点击某组范围元素或获取某组范围元素的值&#xff0c;如需获取指定的父元素&#xff0c;再…

【MongoDB与MySQL对比】

MongoDB 与 MySQL 全方位对比分析在现代软件开发中&#xff0c;数据库的选择直接影响系统性能、扩展性和开发效率。MongoDB 和 MySQL 作为两种主流数据库&#xff0c;分别代表了 NoSQL 和关系型数据库的典型&#xff0c;各自在不同场景中发挥着重要作用。本文将抛开代码示例&am…

Spring AI开发指导-对话模型

对话模型接口描述Spring AI基于Spring Cloud的架构体系&#xff0c;定义了一系列可扩展的API接口&#xff0c;支持对接不同类型的AI大模型的核心功能&#xff0c;这些API接口支持同步编程模式或者异步编程模式&#xff1a;接口ModelModel是同步编程模式接口&#xff0c;其参数支…

Win11 下卸载 Oracle11g

目录 1、停止服务 2、启动 Universal install 应用 3、执行 deinstall.bat 脚本 4、删除注册表相关数据 5、删除环境变量中的oracle相关路径 6、删除安装文件 7、删除C盘中的相关Oracle文件 8、删除 Oracle 数据存放目录 9、检查 10、重装oracle可能还会碰到的问题 &…

深入剖析Spring Boot应用启动全流程

目录 前言 启动流程概览 一、第一阶段&#xff1a;初始化SpringApplication 二、第二阶段&#xff1a;运行SpringApplication 三、第三阶段&#xff1a;环境准备 四、第四阶段&#xff1a;创建应用上下文 五、第五阶段&#xff1a;准备应用上下文 六、第六阶段&#xf…

Matplotlib 可视化大师系列(三):plt.bar() 与 plt.barh() - 清晰对比的柱状图

目录Matplotlib 可视化大师系列博客总览Matplotlib 可视化大师系列&#xff08;三&#xff09;&#xff1a;plt.bar() 与 plt.barh() - 清晰对比的柱状图一、 柱状图是什么&#xff1f;何时使用&#xff1f;二、 函数原型与核心参数plt.bar(x, height, ...) - 垂直柱状图plt.ba…

基于 FastAPI 和 OpenFeature 使用 Feature Flag 控制业务功能

模拟业务场景&#xff1a;多租户系统跨域转账&#xff0c;需要控制某租户下某用户是否可以在某域转账 open_feature_util.py import typing from abc import abstractmethod, ABCMeta from typing import Sequencefrom openfeature.evaluation_context import EvaluationContex…

Stm32通过ESP8266 WiFi连接阿里云平台

本文将介绍stm32如何通过WiFi来连接阿里云&#xff0c;上传数据和接收指令。要先与阿里云建立TCP连接&#xff0c;然后再通过MQTT协议交互。 大体流程&#xff1a;1、在阿里云网页上创建产品和设备&#xff1b;2、stm32通过WiFi连接云平台&#xff1b;3、MQTT连接阿里云&#…