S2-05 ESP-IDF开发 : SPI

SPI

SPI 是由摩托罗拉(Motorola)公司开发的全双工同步串行总线,是微处理控制单元(MCU)和外围设备之间进行通信的同步串行端口。主要应用在EEPROM、Flash、实时时钟(RTC)、数模转换器(ADC)、网络控制器、MCU、数字信号处理器(DSP)以及数字信号解码器之间。SPI 系统可直接与各个厂家生产的多种标准外围器件直接接口,一般使用4 条线:串行时钟线SCK、主机输入/从机输出数据线MISO、主机输出/从机输入数据线MOSI 和低电平有效的从机选择线SSEL。

SPI 的优势和缺点

SPI的优势:
  1. 全双工,高速率传输 ,比如比I2C 传输速率更高
  2. 推免输出接口,推挽输出接口能够减少走线分叉
  3. 信号完成性强,通信协议灵活
  4. 消息帧可任意调节
  5. 功耗低,电路简单 ,不需要上拉电阻,功耗可以更低
  6. 软件配置简单
SPI的缺点:
  1. 单一主设备号通常只能支持一个主设备号
  2. 传输距离短,一般只适合板内信号传输
  3. 缺少硬件检查协议,没有硬件级别的错误检查协议
  4. 无法内部寻址,多从设备时需要额外的片选信号线

SPI信号线介绍

SPI总线包括4条信号逻辑线,定义如下
MISO: Master inout slave output 主机输入,从机输出(数据来自从机)
MOSI: Master output slave input 主机输出,从机输出(数据来自主机)
SCLK: serial Clock 串行时钟信号,由主机产生发送给从机
CS: 片选信号,由主机发送,以控制与那个从机通信,通常时低电平为有效信号
S2-05 ESP-IDF开发 : SPI_第1张图片

SPI 接线方式

spi接线方式分别由二种分别是常规的独立从机配置和菊花链配置

1. 常规的独立从机配置

常规的独立从机配置,每个从机都需要一条单独的CS线,当主机要更特定的从机通信时,将相应的CS信号线拉低,并保持其他CS信号线为高,同时因为从机的MISO脚,再同一条信号上,因此要求没有被选择的从机的MISO引脚要配置为高组态输出
S2-05 ESP-IDF开发 : SPI_第2张图片

常规独立主从接线法是指 SPI 总线上每个从设备都被主设备独立控制,每个从设备都有一个独立的 CS 信号,用于选择需要通信的从设备。其优势包括:

  • 灵活性高:每个从设备可以独立控制,不受其他从设备的影响,可以根据需要选择通信对象。
  • 易于调试:每个从设备之间相互隔离,可以方便地进行单独调试和故障排除。
  • 速度快:由于没有菊花链中的额外延迟,可以实现更快的数据传输速度。
2. 菊花链配置

我们一般以信号线以串行的方式从一个设备依次传到下一个设备,直到数据到达目标设备的数据传输方式称为菊花链。
菊花链缺点:如果从机出现单点故障时,那么低于该设备优先级的从机就掉线了,距离主机越远的从机获得服务的优先级就越低。所以需要设置总线检测器,并安排好从机的优先级。如果某个从机超时应及时处理,放置单点故障造成整个链路的崩溃。菊花链充分的使用了SPI移位寄存器的功能,每个从机在下一个时钟周期,将输入数据复制到输出。
S2-05 ESP-IDF开发 : SPI_第3张图片

菊花链接线法是指每个从设备的输出都连接到下一个从设备的输入,形成一条串行链。在菊花链中,只有一个 SS 信号控制整个链路的通信,具有以下优势:

  • 引脚占用少:使用菊花链连接方式时,每个设备只需要使用一个信号线就可以完成数据传输,节省了系统中的引脚资源。
  • 成本低:相对于常规独立主从接线法来说,菊花链的 PCB 布线更简单,也更容易集成和实现。

菊花链的缺点是当链路中任何一个从设备故障或者通信失败时,整个链路都无法正常工作,因此需要注意菊花链的稳定性和可靠性。

SPI配置模式

SPI总线通信中的时钟相位和极性是指Master设备在传输数据时生成的SCLK时钟信号的特性。其主要作用是提供同步时钟信号,以确保Master和Slave设备之间数据传输的精确性和可靠性。

  • 时钟极性(CPOL):CPOL定义了时钟的空闲状态,即当SCLK处于空闲状态时,是高电平还是低电平。可以设置为0或1,用于匹配Slave设备的SCK极性。
  • 时钟相位(CPHA):CPHA定义了时钟沿的位置,即数据采样和传输的时机。可以设置为0或1,用于匹配Slave设备的数据采样时间。

具体来说,时钟相位和极性可分为四种情况:

  1. CPOL=0,CPHA=0:第一帧位数据是在SCK拉低的时候写入,在上升沿读出;
  2. CPOL=0,CPHA=1:第一帧数据在上升沿写入,下降沿读出;
  3. CPOL=1,CPHA=0:第一帧数据在SCK拉高的时候写入,下降沿读出
  4. CPOL=1,CPHA=1:第一帧数据在下降沿写入,上升沿读出
    S2-05 ESP-IDF开发 : SPI_第4张图片

这些理解即可,不用死记硬背,数据文档中都有

SPI 总线通讯协议

SPI 是一种通信通信的总线协议,数据线根据不同的配置,在时钟的上升沿(电平从低到高)或下降沿(从高到低)进行采样spi通信的时序如下1.主机先将对应从机的CS信号拉低,通知从机开始建立连接,数据接收端检测到时钟的边沿信号后,就立即开始读取数据线上的信号

SPI是全双工的,主机在发送数据的同时也在接收数据,主机可以通过查询的方式,来判断从机是否由数据需要发送,如果由主机会继续发送数据。来获取从机想要的发送的数据。之后从机只需要丢掉这些无效数据 (Dummy Data) 就可以了。
S2-05 ESP-IDF开发 : SPI_第5张图片

协议变体

然而,在实际应用中,很少出现同时发送和接受的情况,一般都是Master先发送命令或是地址,然后Slave发送数据。下图是SPI读写Flash存储器的时序。具体图中如Instruction等,就是操作Flash存储器所需要的形式。简单来说,实现对Flash存储器的通信,需要发送指令码、地址码、模式码、Dummy周期、数据码五个阶段,Flash厂商的芯片手册中有指令集描述每条指令所需要的通信格式。
S2-05 ESP-IDF开发 : SPI_第6张图片

因此对于SPI协议,出现了许多适应性的变体。

1. 三线 SPI

三线SPI仅有CS、SCLK以及IO口三条线,也就是将MOSI和MISO两线并成一条半双工数据线,所以三线SPI也可称为半双工SPI协议。其Interface如下:
S2-05 ESP-IDF开发 : SPI_第7张图片

2. Dual SPI

SPI 被创建之后,虽然传输速率有了很大的提升,但是还是无法满足贪婪人类的欲望,但是时钟频率又不能无限提升,所以就额外增加线路传输数据,如果多一根线的话,每次就能传2个bit的数据。所以Dual SPI就是将SPI的MOSI和MISO都改成双向IO口。
S2-05 ESP-IDF开发 : SPI_第8张图片
S2-05 ESP-IDF开发 : SPI_第9张图片

3. Quad SPI

在Dual SPI基础上,再增加两个IO口,就能实现每次4bit数据的传输,即 SPI 改。
S2-05 ESP-IDF开发 : SPI_第10张图片S2-05 ESP-IDF开发 : SPI_第11张图片

4. SDR 与 DDR 模式

上面给出的时序都是SDR模式(Single Data Rate),每个时钟周期只有1bit的数据。在此基础上,如果每个时钟沿都进行数据输出,就能在一个时钟周期内实现2bit数据的传输,也就称之为DDR(Double Data Rate)。
S2-05 ESP-IDF开发 : SPI_第12张图片
Fdatabase%2F785%3A1&pos_id=img-MIdTp8Lx-1705804518389)

5. Octal SPI

这是对SPI的一次魔改,目前理解就认为Quad SPI等只是在SPi基础上增加位宽而已,所以也还有一种八倍SPI (Octal SPI)。但这些变体过于变态,个人认为已经不能称之为串口通信了!

Octal SPI是SPI(Serial Peripheral Interface)的一种扩展标准,也叫做“8线串行接口”或者“8位SPI”。与传统的SPI接口相比,Octal SPI接口采用八个数据线进行并行数据传输,可以实现更高的数据传输速率和带宽。

Octal SPI接口具有以下特点:

  • 高速传输:Octal SPI接口采用8位并行传输,每个对应的数据线上都可以传输数据,最高速度可达到800Mbps,在高速通信场景下具有较大的优势。
  • 大带宽:由于Octal SPI接口可以同时传输8个数据位,因此在保证速度的情况下,它可以实现更大的带宽,可以满足一些大数据传输的需求。
  • 低功耗:Octal SPI接口可以使用节能模式,在传输数据时关闭一些不必要的电路部分,以降低功耗,这在移动设备等移动终端产品中非常有用。
  • 可靠性强:由于Octal SPI接口采用了并行传输,相比传统的SPI接口,其抗干扰能力更强,传输过程中数据不容易出错,可以提高传输的可靠性。
  • 兼容性良好:Octal SPI接口采用了SPI接口的标准协议,与现有的SPI接口兼容。因此,现有的SPI应用程序可以很容易地进行移植和使用。

Octal SPI接口在一些高速、大带宽和低功耗的应用场合下非常有用,比如移动终端产品、音频和视频处理器、图像传感器、存储芯片等。但同时也需要注意,Octal SPI接口需要更多的信号线和复杂的硬件电路支持,设计难度较大,成本也相应增加。

ESP32 的 SPI

ESP32-S3 集成了 4 个 SPI 外设。

  • SPI0,供 ESP32-S3 和加密 DMA (EDMA) 访问封装内或封装外 flash/PSRAM
  • SPI1,供 CPU 访问封装内或封装外 flash/PSRAM
  • SPI2,通用 SPI 控制器,具有单独的 DMA 通道
  • SPI3,通用 SPI 控制器,和部分外设共用一个 DMA 通道

SPI0 和 SPI1 特性:

  • 支持单线、双线、四线、八线 SPI 模式
  • 八线 SPI 模式支持单倍数据速率 (SDR) 和双倍数据速率 (DDR)
  • 时钟频率可配置,八线 SPI SDR/DDR 模式下最高可达 120 MHz
  • 数据传输以字节为单位

SPI2 特性:

  • 支持主机或从机模式
  • 具有单独的 DMA 通道
  • 支持单线、双线、四线、八线 SPI 模式
  • 时钟极性 (CPOL) 和相位 (CPHA) 可配置
  • 时钟频率可配置
  • 数据传输以字节为单位
  • 读写数据位序可配置:最高有效位 (MSB) 优先,或最低有效位 (LSB) 优先
  • 主机模式
    • 支持双线全双工通信,时钟频率最高可达 80 MHz
    • 八线 SPI 全双工模式仅支持单倍数据速率 (SDR)
    • 支持单线、双线、四线和八线半双工通信,时钟频率最高可达 80 MHz
    • 八线 SPI 半双工模式支持单倍数据速率(最高 80 MHz)和双倍数据速率(最高 40 MHz)
    • 支持 Moto6800/I8080/并行 RGB 接口 8 位 LCD 驱动
    • 具有六个 SPI_CS 管脚,可与六个独立 SPI 从机相连
    • CS 建立和保持时间可配置
  • 从机模式
    • 支持双线全双工通信,时钟频率最高可达 60 MHz
    • 支持单线、双线和四线半双工通信,时钟频率最高可达 60 MHz
    • 八线 SPI 全双工和半双工模式仅支持单倍数据速率 (SDR)

SPI3 特性:

  • 支持主机或从机模式
  • 与 ADC 和 DAC 外设共用一个 DMA 通道
  • 支持单线、双线、四线、八线 SPI 模式
  • 八线 SPI 模式仅支持单倍数据速率 (SDR)
  • 时钟极性 (CPOL) 和相位 (CPHA) 可配置
  • 时钟频率可配置
  • 数据传输以字节为单位
  • 读写数据位序可配置:最高有效位 (MSB) 优先,或最低有效位 (LSB) 优先
  • 主机模式
    • 支持双线全双工通信,时钟频率最高可达 80 MHz
    • 支持单线、双线和四线半双工通信,时钟频率最高可达 80 MHz
    • 支持 1 位 LCD 驱动
    • 具有三个 SPI_CS 管脚,可与三个独立 SPI 从机相连
    • CS 建立和保持时间可配置
  • 从机模式
    • 支持双线全双工通信,时钟频率最高可达 60 MHz
    • 支持单线、双线和四线半双工通信,时钟频率最高可达 60 MHz

下表给出了与 SPI 主驱动器相关的术语:

属于 定义
Host(主机) ESP32-S3 内部的 SPI 控制器外设,通过总线启动 SPI 传输,并充当 SPI 主机。
Device(设备) SPI 从设备。SPI 总线可以连接到一个或多个设备。每个设备共享 MOSI、MISO 和 SCLK 信号,但仅当主机断言设备的单独 CS 线时才在总线上处于活动状态。
Bus(总线) 连接到一个主机的所有设备共用的信号总线。通常,总线包括以下线路:MISO、MOSI、SCLK、一条或多条 CS 线路,以及可选的 QUADWP 和 QUADHD。所以设备连接到相同的线路,除了每个设备都有自己的 CS 线路。如果以菊花链方式连接,多个设备也可以共享一条 CS 线。
MOSI Master Out,Slave In,从主机到设备的数据传输。也是八进制/OPI 模式下的 data0 信号。
MISO Master In,Slave Out,从设备到主机的数据传输。也是八进制/OPI 模式下的 data1 信号。
SCLK 串行时钟。主机生成的时钟信号,使数据位传输保持同步。
CS 片选信号,允许主机选择连接到总线的单个设备以发送或接收数据。
QUADWP 写入模式信号。用于 4 位 (qio/qout) 通讯。也适用于 Octal/OPI 模式下的 data2 信号。
QUADHD 读取模式信号。用于 4 位 (qio/qout) 通讯。也适用于 Octal/OPI 模式下的 data3 信号。
DATA4 八进制/OPI 模式下的 Data4 信号。
DATA5 八进制/OPI 模式下的 Data5 信号。
DATA6 八进制/OPI 模式下的 Data6 信号。
DATA7 八进制/OPI 模式下的 Data7 信号。
Assertion(片选) 激活 CS 线连接的设备,当片选信号被拉低(也就是置为“0”)时,表示该设备被选中,可以与主控器进行数据传输。
De-Assertion(反选) 将 CS 线连接的设备设置为非活动状态,当片选信号被拉高(也就是置为“1”)时,表示该设备不被选中,不能与主控器进行数据传输。
Transaction(事务) 是指一组在特定的时间段内完成的SPI数据传输操作。在一个Transaction中,通常包含了一次对某个从设备(Slave)的读写操作,并且这次读写操作完成之后,从设备与主控器之间的数据传输也就结束了。
Launch edge(传输沿) 在Master端在时钟线(SCLK)上发送一次特定信号来启动一个数据传输周期的边缘。Master在该边缘上发送启动信号,Slave通过接收到启动信号开始执行数据传输过程。
Latch edge(接收沿) 在Slave端采样数据的边缘。在SPI通信中,数据在SCLK信号上跟随Master从高位到低位或从低位到高位移位,而数据的采样则是在每个SCLK的下降沿或上升沿进行的。Latch Edge就是指Slave在这个边缘上对数据进行采样,然后将采样到的数据传递给Master端。

设备特性

SPI 主驱动程序管理主机与设备的通信。该驱动程序支持以下功能:

  • 多线程环境
  • 读取和写入数据时对 DMA 透传
  • 来自同一信号总线上不同设备的数据的自动时分复用

注意:
SPI 主驱动器具有多个设备连接到单个总线的概念(共享单个 ESP32-S3 SPI 外设)。只要每个设备仅由一个任务访问,驱动程序就是线程安全的。但是,如果多个任务尝试访问同一个 SPI 设备,则驱动程序不是线程安全的。在这种情况下,建议:

  • 构建一个独立任务访问 SPI 设备,其他需要使用 SPI 设备的都通过任务通讯方式操作。
  • 使用为共享设备添加互斥锁 xSemaphoreCreateMutex

SPI事务

SPI 总线事务由五个阶段组成,可在下表中找到。这些阶段中的任何一个都可以跳过。

阶段 标题 描述
Command 命令阶段 该阶段用于传输指令信息,包括读写操作、寄存器选择和数据格式等。在该阶段,Master端向Slave端发送命令码(0~16位),以指定本次数据传输的类型和目的。
Address 地址阶段 该阶段用于传输地址信息,通常用于访问存储器或者寄存器等设备,以确定具体的存取位置。在该阶段,Master端向Slave端发送地址信息(0~32位),以便Slave端能够正确地识别并访问相应的存储单元。
Write 写入阶段 该阶段用于向设备写入数据,Master端向Slave端发送需要存储的数据信息。数据在SCLK信号上跟随Master从高位到低位或从低位到高位移位,同时Slave端接收和存储数据。
Dummy 空闲阶段 该阶段也称为延时周期(该阶段是可配置的),用于在Write和Read之间产生一段空闲时间,以便Slave端完成相应的处理操作。在该阶段,Master端不进行数据传输,只是通过周期性的时钟信号来保持通信连接。
Read 读出阶段 该阶段用于从设备读取数据,Slave端向Master端发送存储的数据信息。数据在SCLK信号上跟随Master从高位到低位或从低位到高位移位,同时Master端接收和处理数据。

事务的属性由总线配置结构 spi_bus_config_t,设备配置结构 spi_device_interface_config_t 和事务配置结构决定spi_transaction_t

SPI 主机可以发送全双工事务,在此期间读取和写入阶段同时发生。总事务长度由以下成员的总和决定:

  • spi_device_interface_config_t::command_bits
  • spi_device_interface_config_t::address_bits
  • spi_transaction_t::length

而成员 spi_transaction_t::rxlength 只确定接收到缓冲区的数据长度。

在半双工事务中,读取和写入阶段不是同时的(一次一个方向)。写入和读取阶段的长度分别由结构的length和成员决定。rxlengthspi_transaction_t

命令和地址阶段是可选的,因为并非每个 SPI 设备都需要命令和/或地址。这反映在设备的配置中:如果 command_bitsaddress_bits 设置为零,则不会发生任何命令或地址阶段。

读取和写入阶段也可以是可选的,因为并非每个事务都需要写入和读取数据。如果 rx_buffer为 NULL 且未设置SPI_TRANS_USE_RXDATA ,则跳过读取阶段。如果 tx_buffe r为 NULL 且未设置 SPI_TRANS_USE_TXDATA,则跳过写入阶段。

驱动程序支持两种类型的事务:中断事务和轮询事务。程序员可以选择为每个设备使用不同的事务类型。

ESP32中 SPI 主机的构建流程

初始化SPI总线
挂载设备
收取信息
发送信息
移除设备
释放总线
1. 初始化 SPI 总线

在使用 SPI 总线之前,首先需要对 SPI 总线进行配置,使用 spi_bus_initialize() 函数,传入一个还有 SPI 总线配置的 spi_bus_config_t 结构体对总线进行初始化,该结构体原型如下(忽略4线8线,只站在主机层面看):

typedef struct {
    int miso_io_num;        /*!< MISO signal IO number, -1 if not used */
    int mosi_io_num;        /*!< MOSI signal IO number, -1 if not used */
    int sclk_io_num;        /*!< SCLK signal IO number, -1 if not used */
    int quadwp_io_num;      /*!< SPI WP signal, -1 if not used */
    int quadhd_io_num;      /*!< SPI HD signal, -1 if not used */
    int max_transfer_sz;    /*!< Maximum transfer size in bytes, defaults to 4096 */
    int flags;              /*!< Additional bus configuration flags to set */
    int intr_flags;         /*!< Flags for the interrupt allocating function */
} spi_bus_config_t;
成员 描述
miso_io_num 表示 MISO(Master In Slave Out)信号所连接的 GPIO 端口号。如果不使用此信号,则应设为 -1。
mosi_io_num 表示 MOSI(Master Out Slave In)信号所连接的 GPIO 端口号。如果不使用此信号,则应设为 -1。
sclk_io_num 表示 SCLK(Serial Clock)信号所连接的 GPIO 端口号。如果不使用此信号,则应设为 -1。
quadwp_io_num 表示 WP(Write Protect)信号所连接的 GPIO 端口号。如果不使用此信号,则应设为 -1。
quadhd_io_num 表示 HD(Hold)信号所连接的 GPIO 端口号。如果不使用此信号,则应设为 -1。
max_transfer_sz 表示最大的传输长度,以字节为单位,默认值为 4096 字节,如果为0表示启动DMA进行数据传送。
flags 附加总线配置标志。(见下面解释)
intr_flags 用于分配中断的标志。(见下面解释)
flags 总线配置标志可选项:
  • SPICOMMON_BUSFLAG_SLAVE :将总线配置为从机模式。
  • SPICOMMON_BUSFLAG_MASTER :将总线配置为主机模式。
  • SPICOMMON_BUSFLAG_IOMUX_PINS :使用 IO mux 配置 SPI 总线的引脚。
  • SPICOMMON_BUSFLAG_GPIO_PINS :将 SPI 总线的信号引脚强制路由到 GPIO 矩阵中。
  • SPICOMMON_BUSFLAG_SCLK :检查 SCLK(串行时钟)信号是否存在。
  • SPICOMMON_BUSFLAG_MISO :检查 MISO(主设备输出,从设备输入)信号是否存在。
  • SPICOMMON_BUSFLAG_MOSI :检查 MOSI(主设备输入,从设备输出)信号是否存在。
  • SPICOMMON_BUSFLAG_DUAL :检查 MOSI 和 MISO 信号是否已经初始化,以便支持双线模式。
  • SPICOMMON_BUSFLAG_WPHD :检查 WP(写保护)和 HD(暂停)信号是否存在。
  • SPICOMMON_BUSFLAG_QUAD :检查 MOSI、MISO、WP 和 HD 引脚是否已经配置为输出,以便支持四线模式。
  • SPICOMMON_BUSFLAG_IO4_IO7 :检查 IO4~IO7 引脚是否存在。
  • SPICOMMON_BUSFLAG_OCTAL :检查 MOSI、MISO、WP、HD、IO4~IO7 引脚是否同时存在,以便支持八线模式。
  • SPICOMMON_BUSFLAG_NATIVE_PINS :与 SPICOMMON_BUSFLAG_IOMUX_PINS 功能相同,表示使用本机(native)引脚进行 SPI 总线的配置。
  • 具体可参照 spi_common.h 中的 SPICOMMON_BUSFLAG_ 配置项*

intr_flags 中断分配标志可选项有:

  • ESP_INTR_FLAG_IRAM:分配 IRAM 中的中断处理程序。
  • ESP_INTR_FLAG_SHARED:在多个处理器上共享中断。
  • ESP_INTR_FLAG_LEVEL1:在级别 1 中分配中断处理程序。
  • 具体可参照 esp_intr_alloc.h 文件中的 ESP_INTR_FLAG_ 相关配置项*

在这个配置项中,主要是将 MOIS 和 MISO 以及 SCLK 三个引脚设置好,在他的可以忽略(如果需要使用自定义缓冲区,则可以设置max_transfer_sz 参数,否则设置为0,使用 DMA 传输数据,至于什么是 DMA,后文中会有讲到)。

设置完成之后,还需要使用 spi_bus_initialize() 函数安装 SPI 总线,该函数原型如下:

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);
参数 描述
host_id SPI 使用总线编号,ESP32-S3 中可选项为 SPI1_HOST~ SPI3_HOST, 分别对应 SPI1、SPI2 和 SPI3
bus_config SPI总线配置结构体指针,包含了一些SPI总线的配置信息,例如时钟分频等等。
dma_chan DMA通道选择,允许使用 DMA 通道来进行数据传输。如果选择了 DMA 通道,则可以使用内存中任意大小的缓冲区进行传输。若指定为 SPI_DMA_DISABLED,则传输大小会受到限制。如果只有 SPI Flash 需要使用该总线,则应设置为 SPI_DMA_DISABLED;如果需要让驱动自动分配 DMA 通道,则应将其设置为 SPI_DMA_CH_AUTO
返回值 返回 ESP_OK 表示初始哈成功; ESP_ERR_INVALID_ARG 表示配置标无效; ESP_ERR_INVALID_STATE 表示主机ID已经被占用;ESP_ERR_NOT_FOUND 表示没有可用的 DMA 通道;ESP_ERR_NO_MEM 表示内存不足
注意:在使用完SPI总线后,应通过调用 spi_bus_free() 函数释放SPI总线资源。如果需要在所创建的SPI总线上使用SPI设备进行数据传输,则还需要使用 spi_bus_add_device() 函数为每个SPI设备创建一个句柄。 SPI 设备的句柄可用于配置和操作该设备。
2. 向总线中添加设备

上文书讲到,SPI 是一个总线,SPI 总线上可以挂在多个设备,所以总线初始化完毕之后还不能立即使用,还必须挂在设备才可进行通讯,而在上文中配置 SPI 总线的时候,我们只完成了 SCLK,MISO,MOSI,三根线的配置,而 SPI 双工通讯中至少需要配置 4 根线,另一根就是片选 CS 线,而这条线是和总线配置无关的,它属于设备, 在第二部设备配置中配置。
挂在 SPI 设备 首先对设备进行配置,用到设备配置结构体 spi_device_interface_config_t ,以及设备添加函数 spi_bus_add_device

配置参数使用结构体 spi_device_interface_config_t,原型如下:

typedef struct {
    uint8_t command_bits;
    uint8_t address_bits;
    uint8_t dummy_bits;
    uint8_t mode;
    uint16_t duty_cycle_pos; 
    uint16_t cs_ena_pretrans;
    uint8_t cs_ena_posttrans; 
    int clock_speed_hz; 
    int input_delay_ns; 
    int spics_io_num;
    uint32_t flags;
    int queue_size;
    transaction_cb_t pre_cb;   
    transaction_cb_t post_cb;  
} spi_device_interface_config_t;
成员 描述
command_bits SPI 传输过程中要传输命令字节的位数,取值范围为 0 到 16。
address_bits SPI 传输过程中要传输地址的位数,取值范围为 0 到 64。
dummy_bits SPI 传输过程中要插入的空闲位数,即地址位和数据位之间的空闲位数。
mode SPI 时序模式,表示 CPOL 和 CPHA 的组合
duty_cycle_pos SPI 时钟占空比的正脉冲占用时间比例,以 1/256 为单位。例如,值为 128 表示 50% 的占空比。将该值设置为 0(未设置)等同于将其设置为 128。
cs_ena_pretrans SPI 传输前 CS 信号的激活时间,以 SPI 时钟循环次数(bit-cycles)为单位,取值范围为 0 到 16。仅适用于半双工传输。
cs_ena_posttrans SPI 传输后 CS 信号持续激活的时间,以 SPI 时钟循环次数为单位,取值范围为 0 到 16。
clock_speed_hz SPI 总线时钟的频率,以 Hz 为单位,必须是 80MHz 的除数。可用的除数预定义为 SPI_MASTER_FREQ_* 。
input_delay_ns 从设备有效数据到达之前 CS 信号的保持时间,包括从从设备到主设备的时钟延迟。该参数用于提供额外的延迟,以确保从设备上的数据已经准备好了。如果不知道需要多少延迟,可以将其设置为 0。对于高频率的传输(超过 8MHz),建议设置正确的值,以获得更好的时序性能。
spics_io_num 控制从设备 CS 信号的 GPIO 引脚号,如果不需要使用 CS 信号,则设置为 -1。
flags SPI 从设备的标志位,可以通过按位或进行设定。可用的标志位预定义为 SPI_DEVICE_* 。
queue_size 传输队列的大小。该参数指定可以同时处于“传输中”状态的传输事务数量。可以通过 spi_device_queue_trans 函数入队列,并通过 spi_device_get_trans_result 函数获取结果。
pre_cb 和 post_cb SPI 传输开始前和传输结束之后的回调函数指针,用于完成一些额外的操作。由于回调函数在中断上下文中执行,因此建议使用 IRAM 存储函数体,以获得更好的性能。
一般情况下,我们只需要配置设备时钟,传输方式(mode),CS线三个重要参数即可,传输消息队列根据内存情况具体分配

设备配置好之后,需要通过 spi_bus_add_device() 函数将这个设备添加到总线中,并返回一个指向该设备的设备句柄,该函数原型如下:

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);
参数 描述
host_id 总线编号,ESP32-S3 中可选项为 SPI1_HOST~ SPI3_HOST, 分别对应 SPI1、SPI2 和 SPI3
dev_config 指向 spi_device_interface_config_t 结构体的指针,包含了许多 SPI 设备参数,例如时钟频率、传输位序和极性等等。
handle 返回指向 spi_device_handle_t 类型的指针,用于保存每个添加到总线上的设备的句柄地址。如果在函数调用成功时设备句柄已被分配,则 handle 指向的内存地址将更新以包含该设备的句柄。
返回值 返回 ESP_OK 表示成功,其他表示失败。
3. SPI 传输数据

设备挂载好之后,便可以使用 spi_device_polling_transmit()spi_device_queue_trans() 向SPI设备发送需要的数据。
在 ESP32 中进行 SPI 数据传输,使用的都是 事务,事务是指从开始到结束的一个完整的数据传输过程。
一个完整的事务包含以下几个步骤:

  1. 片选信号选择设备:SPI 总线上支持连接多个不同的设备,因此在进行通信之前需要先通过片选信号来选择需要访问的设备。在 ESP32-S3 中,可以通过配置 GPIO 引脚来实现片选信号的控制。
  2. 预处理设置:在每次事务开始前,需要设置一些预处理参数,包括传输模式、传输速度、传输数据类型、传输数据长度等。
  3. 数据传输:在片选信号选择设备并设置好预处理参数后,就可以开始进行数据传输了。SPI 通信可以同时进行数据的收发,并且有两种模式:主机模式和从机模式。在主机模式下,主机负责时钟的产生和控制,并向从机发送数据;在从机模式下,从机只能被动接收数据,并不能主动发送。
  4. 后处理操作:在完成数据传输后,可以对传输结果进行必要的后处理操作,例如校验和的计算、CRC 校验、错误处理等。

ESP32 中的事务使用的是结构体 spi_transaction_t,该结构体原型如下:

struct spi_transaction_t {
    uint32_t flags;
    uint16_t cmd;
    uint64_t addr;
    size_t length;
    size_t rxlength;
    void *user;
    union {
        const void *tx_buffer;
        uint8_t tx_data[4];
    };
    union {
        void *rx_buffer;
        uint8_t rx_data[4];
    };
} ;
成员 描述
flags SPI 事务标志位,可以使用 SPI_TRANS_* 宏定义进行设置。
cmd 命令数据,用于 SPI 事务中发送命令时使用,其长度由 spi_device_interface_config_t 结构体中的 command_bits 成员指定。
addr 地址数据,用于 SPI 事务中发送地址时使用,其长度由 spi_device_interface_config_t 结构体中的 address_bits 成员指定。
length 本次发送的总数据长度,以位为单位,SPI最多一次可传输 64字节(65536位)数据,如果向传输的更多,建议使用 DMA。
rxlength 接收到的总数据长度,应该不大于 length,仅在 SPI 全双工模式下使用。
user 用户定义的变量,可以用于存储事务 ID 等信息。
tx_buffer 发送数据缓冲区指针,如果不需要发送数据,则设置为 NULL。
tx_data 为了方便快速发送数据,当使用宏定义 SPI_TRANS_USE_TXDATA 时,可以直接将要发送的四字节数据存储在此处。
rx_buffer 接收数据缓冲区指针,如果不需要接收数据,则设置为 NULL,在使用 DMA 时,每次读取 4 字节。
rx_data 当使用宏定义 SPI_TRANS_USE_RXDATA 时,接收到的数据将会直接存储在此处,每次存储 4 字节。

在 spi_transaction_t 结构体中,cmd 和 addr 字段用于控制 SPI 总线传输的协议,具体作用如下:

  1. cmd 字段
    cmd 字段用于指定命令字,通过命令字可以控制从设备的工作状态和使能信号等。通常情况下,命令字是从设备手册中定义的固定值。在执行 SPI 总线传输时,如果需要发送命令字,则可以将其写入到 cmd 字段所对应的缓冲区中,然后通过 spi_device_transmit() 函数一并发送出去。

    例如,在读取某些 SPI 设备的寄存器值时,需要先向从设备发送一个命令字表示要读取哪个寄存器地址,然后从设备会返回指定地址的寄存器值。这时,可以将命令字写入 cmd 字段所对应的缓冲区中,在 spi_device_transmit() 函数中发送出去,然后在接收数据时,SPI 从设备会将返回的寄存器值写入到 rx_buffer 所对应的缓冲区中。

  2. addr 字段
    addr 字段用于指定地址信息,通常用于读写 SPI 设备中的寄存器或存储器等。在执行 SPI 总线传输时,如果需要发送地址信息,则可以将其写入到 addr 字段所对应的缓冲区中,然后通过 spi_device_transmit() 函数一并发送出去。

    例如,在写入某些 SPI 设备的寄存器值时,需要先向从设备发送一个地址信息表示要写入哪个寄存器地址,然后再将要写入的数据发送给从设备。这时,可以将地址信息写入 addr 字段所对应的缓冲区中,在 spi_device_transmit() 函数中发送出去,然后将要写入的数据写入到 tx_buffer 所对应的缓冲区中,一并发送出去。

注意: 在使用 cmd 和 addr 字段之前,需要首先对它们进行相应的配置,具体方式可以参考 spi_device_interface_config_t 结构体中 command_bits、address_bits 和 dummy_bits 参数的详细说明。同时,不同的 SPI 从设备在命令字和地址信息方面的要求可能会有所不同,需要根据具体的设备手册进行配置。

根据 SPI 传输规范,在 SPI 通道中传输的都是二进制数据位,所以事务在发送的时候也是需要转换成 进行数据传输,也就是说,事务总长度的和 = spi_device_interface_config_t::command_bits + spi_device_interface_config_t::address_bits + spi_transaction_t::length + spi_device_interface_config_t::dummy_bits,并且,单位是位 而不是字节。
具体来说,在将一个事务转换为 uint8_t 数组时,通常需要按照以下顺序填充各个字段:

  1. 命令字(如果有的话),填充到数组的最前面。
  2. 地址信息(如果有的话),在命令字之后填充。
  3. 数据信息,根据 SPI 传输模式的不同可能会分为发送数据和接收数据两部分。发送数据一般是由用户通过 tx_buffer 字段进行填充的,而接收数据则通过 rx_buffer 字段返回。
  4. 如果使用了 Dummy Cycle(空闲周期),则在数据信息之后填充 dummy 数据。

在这里插入图片描述

成员 spi_transaction_t::rxlength 只确定接收到缓冲区的数据长度。

命令和地址阶段是可选的,因为并非每个 SPI 设备都需要 命令地址 。这反映在设备的配置中:如果command_bits 和 address_bits设置为零,则不会发生任何命令或地址阶段。

SPI 在进行数据传输的时候,MOSI 发送 MISO 接收,如果是在全双工模式下,主机每送出一位数据,同时就可以接收到一位数据,当一个字节发送完毕后,也就意味着主机可以从从机中获得一个字节的数据。

ESP32 的实物传输允许采用 中断轮询 方式发送事务,两种传输方式对应的函数分别是 spi_device_transmitspi_device_polling_transmit

  • spi_device_transmit 函数采用的是中断处理模式,即在开始传输之后,将传输的数据交给硬件 SPI 控制器,在传输完成之前不会阻塞程序的执行。此时,用户可以继续执行其他的操作,等到传输完成时,ESP32 的硬件 SPI 中断服务例程会被调用,从而使得数据传输完成。因此,该函数是非阻塞的,并且能够更好地支持多任务系统。但是,这种方式需要在传输过程中通过回调函数来处理传输完成事件,因此对于复杂的应用场景可能需要编写更加复杂的代码来处理相关事件。
  • spi_device_polling_transmit 函数则采用了轮询模式,在传输数据的过程中会一直等待数据传输完成之后再返回。因此,该函数是阻塞的,并且只适用于单任务系统。但是,由于其简单易用的特性,常用于较为简单的应用场景。

另外,需要注意的是,在使用 spi_device_transmit 函数时,需要先配置一个中断处理程序(即使用 esp_intr_alloc() 函数分配一个 ESP32 中断向量),否则传输过程中的中断将无法被正确处理,导致数据传输失败。而在使用 spi_device_polling_transmit 函数时,则不需要分配中断向量。

两种方式的不同点在于:

  1. 中断方式是异步的,而轮询方式是同步的。中断方式将 SPI 总线的数据传输过程分为多个子任务,每个子任务由硬件 SPI 控制器负责执行,并在执行完成后自动触发中断处理函数,而主程序可以继续执行其他操作。而在轮询方式下,主程序必须等待整个数据传输任务完成后才能继续执行其他操作。
  2. 中断方式需要配置中断向量并编写中断处理函数,对于复杂的应用场景可能需要更加熟悉底层硬件的工作原理。而轮询方式较为简单,适用于数据量较小、实时性要求较低的应用场景。
  3. 中断方式可以更好地支持多任务系统,多个任务之间可以并发执行,从而提高系统的整体运行效率。而轮询方式只能依次执行,性能相对较差。
  4. 中断方式可能会导致一定的延迟,因为在中断处理函数被调用之前都需要等待一定的时间。而轮询方式可以保证数据传输的实时性。

spi_device_transmit 函数原型如下:

esp_err_t spi_device_transmit(spi_device_handle_t handle, spi_transaction_t *trans_desc);

spi_device_polling_transmit 函数原型如下:

esp_err_t spi_device_polling_transmit(spi_device_handle_t handle, spi_transaction_t *trans_desc);

这两个函数使用方式相同,只要传输 设备句柄 及 事务句柄 即可。

霸占总线

有时我们在使用 SPI 总线的时候,希望能够以独占的方式发送 SPI 事务而不被其他优先级更高的任务打断,以便花费尽可能少的时间完成任务。为此,我们可以使用总线获取,这有助于暂停与其他设备的事务(轮询或中断),直到总线被释放。
所以在发送事务之前,我们可以通过调用 spi_device_acquire_bus() 独占总线, 使用完之后 在通过 spi_device_release_bus() 释放独占。

霸占设备

再试用 spi_device_polling_transmit 进行事务传输的时候,如果此时有其他优先级更高的任务进入,并且同时也要使用 spi_device_polling_transmit 向同一设备传输数据,则有可能导致数据传输混乱,为了解决这个问题,在任务传输之前,我们可以先通过 spi_device_polling_start 占用这个设备,事务传输完毕之后,在通过spi_device_polling_end 来结束这个设备的占用,在此期间其他任务向使用 spi_device_polling_transmit 传输数据都会返回错误结果。

异步传输数据

除了以上两种同步传输数据之外,ESP32 还具有一种异步传输数据的方式,可通过 spi_device_queue_trans 将事务先放入到消息队列中,然后等待 SPI 总线空闲的时候在发送,期间可以通过 spi_device_get_trans_result 查询传输的结果。

在使用该函数进行数据传输时,也需要先获取 SPI 总线的访问权限,并将待发送的数据和相关参数封装成消息,推送到 SPI 总线的消息队列中。在系统空闲时,SPI 设备驱动程序会自动从消息队列中获取待处理的消息,并执行相应的数据传输操作。在传输完成后,系统会自动调用用户注册的回调函数,通知数据接收方数据已经传输完成。

同步事务传输相比,spi_device_queue_trans() 函数可以提高系统的响应速度和资源利用率,但需要注意预留合适的堆空间以存储消息,并及时处理传输错误。

4. 移除设备
esp_err_t spi_bus_remove_device(spi_device_handle_t handle);

移除设备之后,记得释放与该设备相关所开辟的一切内存。

5. 释放总线
esp_err_t spi_bus_free(spi_host_device_t host_id);

释放总线之后,记得释放与该总线相关所开辟的一切内存。

ESP32中 SPI 从机的构建流程

虽然主机模式和从机模式中的设备都属于 同一个SPI 设备,但从机明显要显得卑微一些。

初始化从机设备
发送数据
接收数据
释放总线
1. 初始化从机设备

从机中无需初始化总线,直接初始化设备信息即可,因为总线只有主机可以操控,但在初始化从机设备的时候,仍需要将总线信息传入给 spi_slave_initialize,该函数是 SPI 从机初始化的唯一函数,其原型如下:

esp_err_t spi_slave_initialize(spi_host_device_t host, const spi_bus_config_t *bus_config, const spi_slave_interface_config_t *slave_config, spi_dma_chan_t dma_chan);
参数 描述
host 使用的总线编号
bus_config SPI总线配置结构体,从机中只需要关注 SCLK、MOSI、MISO三条线的配置
slave_config 从机设备配置结构体
dma_chan 是否使用 MDA 通道

该函数中需要传入 bus_config 结构体,在代码撰写中,这里只需要配置总线各引脚所使用的GPIO编号即可,slave_config 是指向设备的指针,其原型如下:

typedef struct {
    int spics_io_num;
    uint32_t flags;
    int queue_size;
    uint8_t mode;
    slave_transaction_cb_t post_setup_cb;
    slave_transaction_cb_t post_trans_cb;
} spi_slave_interface_config_t;

从结构上看,该结构体和 spi_device_interface_config_t 非常类似,只是少了很多东西(要不说它卑微呢)。

成员 描述
spics_io_num 所使用片选信号引脚编号
flags 表示 SPI_SLAVE_* 标志的按位或,用于设置从机设备的模式。
queue_size 表示事务队列的大小。这个参数设置了可以 ‘in the air’(使用 spi_slave_queue_trans 排队但还没有使用 spi_slave_get_trans_result 完成)的事务数量上限。
mode 表示 SPI 工作模式,对(CPOL, CPHA)两兄弟的配置。
post_setup_cb 一个回调函数指针,用于在 SPI 寄存器加载了新数据之后调用。 如果不在 IRAM 中,由于初始化时驱动程序使用的是 ESP_INTR_FLAG_IRAM,所以在执行闪存操作期间可能会导致回调崩溃。
post_trans_cb 一个回调函数指针,用于在事务完成后调用。与 post_setup_cb 类似, 如果不在 IRAM 中,由于初始化时驱动程序使用的是 ESP_INTR_FLAG_IRAM,所以在执行闪存操作期间可能会导致回调崩溃。
2. 数据传输

在 ESP32-IDF 中,一共提供了两种从机数据传输的方案,一种是使用 spi_slave_transmit 方式,这种方式在发送时会阻塞当前任务到发送结束,本质上与 spi_slave_queue_trans 后接 spi_slave_get_trans_result 相同。当仍有未使用 spi_slave_get_trans_result 完成的事务排队时,不要使用它。该函数原型如下:

esp_err_t spi_slave_transmit(spi_host_device_t host, spi_slave_transaction_t *trans_desc, TickType_t ticks_to_wait);
参数 描述
host 传输数据的 SPI 从机设备指针
trans_desc 指向 spi_slave_transaction_t 结构体的指针,用于描述本次 SPI 数据传输的相关信息,包括发送和接收缓存区地址、数据长度等。
ticks_to_wait 等待 SPI 数据传输完成的最大时间,类型为 TickType_t。如果设置为 portMAX_DELAY,则会一直等待,直到传输完成;如果设置为 0,则表示不等待传输完成,立即返回。如果超过等待时间后仍未完成传输,则函数返回超时错误。

该函数中第二个参数为需要传输的数据,类型是 spi_slave_transaction_t,从名称上看,和 spi_transaction_t 极为相似,但它还是一如既往的卑微,结构体原型如下:

struct spi_slave_transaction_t {
    size_t length; 
    size_t trans_len; 
    const void *tx_buffer;
    void *rx_buffer;
    void *user; 
};
成员 描述
length 表示整个 SPI 数据传输的总数据长度,单位为位(bit)。
trans_len 实际事务的数据长度,单位为位(bit)。这个字段的值可以小于 length,当它被设置为一个比 length 小的值时,实际上只有前几个 bits 被发送和接收。
tx_buffer 指向要发送数据的缓冲区的指针。如果不需要发送数据,则可以将其设置为 NULL。如果需要发送数据,则该指针应指向发送缓冲区的起始地址。
rx_buffer 指向存储接收数据的缓冲区的指针。如果不需要接收数据,则可以将其设置为 NULL。如果需要接收数据,则该指针应指向接收缓冲区的起始地址。
user 一个用户定义的变量,可以用于存储例如事务 ID 等和传输相关的信息。这个字段是一个指针类型,可以指向任何数据类型。

另外开始已使用 spi_slave_queue_transspi_slave_get_trans_result 进行数据传输。

3. 释放设备
esp_err_t spi_slave_free(spi_host_device_t host);

代码共享位置:http://192.168.172.17:3000/Mars.CN/ESP-IDF-S2-SPI.git

你可能感兴趣的:(ESP-IDF,入门篇,单片机,嵌入式硬件,ESP32,FreeRTOS,ESP-IDF)