SPI 是由摩托罗拉(Motorola)公司开发的全双工同步串行总线,是微处理控制单元(MCU)和外围设备之间进行通信的同步串行端口。主要应用在EEPROM、Flash、实时时钟(RTC)、数模转换器(ADC)、网络控制器、MCU、数字信号处理器(DSP)以及数字信号解码器之间。SPI 系统可直接与各个厂家生产的多种标准外围器件直接接口,一般使用4 条线:串行时钟线SCK、主机输入/从机输出数据线MISO、主机输出/从机输入数据线MOSI 和低电平有效的从机选择线SSEL。
SPI总线包括4条信号逻辑线,定义如下
MISO: Master inout slave output 主机输入,从机输出(数据来自从机)
MOSI: Master output slave input 主机输出,从机输出(数据来自主机)
SCLK: serial Clock 串行时钟信号,由主机产生发送给从机
CS: 片选信号,由主机发送,以控制与那个从机通信,通常时低电平为有效信号
spi接线方式分别由二种分别是常规的独立从机配置和菊花链配置
常规的独立从机配置,每个从机都需要一条单独的CS线,当主机要更特定的从机通信时,将相应的CS信号线拉低,并保持其他CS信号线为高,同时因为从机的MISO脚,再同一条信号上,因此要求没有被选择的从机的MISO引脚要配置为高组态输出
常规独立主从接线法是指 SPI 总线上每个从设备都被主设备独立控制,每个从设备都有一个独立的 CS 信号,用于选择需要通信的从设备。其优势包括:
我们一般以信号线以串行的方式从一个设备依次传到下一个设备,直到数据到达目标设备的数据传输方式称为菊花链。
菊花链缺点:如果从机出现单点故障时,那么低于该设备优先级的从机就掉线了,距离主机越远的从机获得服务的优先级就越低。所以需要设置总线检测器,并安排好从机的优先级。如果某个从机超时应及时处理,放置单点故障造成整个链路的崩溃。菊花链充分的使用了SPI移位寄存器的功能,每个从机在下一个时钟周期,将输入数据复制到输出。
菊花链接线法是指每个从设备的输出都连接到下一个从设备的输入,形成一条串行链。在菊花链中,只有一个 SS 信号控制整个链路的通信,具有以下优势:
菊花链的缺点是当链路中任何一个从设备故障或者通信失败时,整个链路都无法正常工作,因此需要注意菊花链的稳定性和可靠性。
SPI总线通信中的时钟相位和极性是指Master设备在传输数据时生成的SCLK时钟信号的特性。其主要作用是提供同步时钟信号,以确保Master和Slave设备之间数据传输的精确性和可靠性。
具体来说,时钟相位和极性可分为四种情况:
这些理解即可,不用死记硬背,数据文档中都有
SPI 是一种通信通信的总线协议,数据线根据不同的配置,在时钟的上升沿(电平从低到高)或下降沿(从高到低)进行采样spi通信的时序如下1.主机先将对应从机的CS信号拉低,通知从机开始建立连接,数据接收端检测到时钟的边沿信号后,就立即开始读取数据线上的信号
SPI是全双工的,主机在发送数据的同时也在接收数据,主机可以通过查询的方式,来判断从机是否由数据需要发送,如果由主机会继续发送数据。来获取从机想要的发送的数据。之后从机只需要丢掉这些无效数据 (Dummy Data) 就可以了。
然而,在实际应用中,很少出现同时发送和接受的情况,一般都是Master先发送命令或是地址,然后Slave发送数据。下图是SPI读写Flash存储器的时序。具体图中如Instruction等,就是操作Flash存储器所需要的形式。简单来说,实现对Flash存储器的通信,需要发送指令码、地址码、模式码、Dummy周期、数据码五个阶段,Flash厂商的芯片手册中有指令集描述每条指令所需要的通信格式。
因此对于SPI协议,出现了许多适应性的变体。
三线SPI仅有CS、SCLK以及IO口三条线,也就是将MOSI和MISO两线并成一条半双工数据线,所以三线SPI也可称为半双工SPI协议。其Interface如下:
SPI 被创建之后,虽然传输速率有了很大的提升,但是还是无法满足贪婪人类的欲望,但是时钟频率又不能无限提升,所以就额外增加线路传输数据,如果多一根线的话,每次就能传2个bit的数据。所以Dual SPI就是将SPI的MOSI和MISO都改成双向IO口。
在Dual SPI基础上,再增加两个IO口,就能实现每次4bit数据的传输,即 SPI 改。
上面给出的时序都是SDR模式(Single Data Rate),每个时钟周期只有1bit的数据。在此基础上,如果每个时钟沿都进行数据输出,就能在一个时钟周期内实现2bit数据的传输,也就称之为DDR(Double Data Rate)。
Fdatabase%2F785%3A1&pos_id=img-MIdTp8Lx-1705804518389)
这是对SPI的一次魔改,目前理解就认为Quad SPI等只是在SPi基础上增加位宽而已,所以也还有一种八倍SPI (Octal SPI)。但这些变体过于变态,个人认为已经不能称之为串口通信了!
Octal SPI是SPI(Serial Peripheral Interface)的一种扩展标准,也叫做“8线串行接口”或者“8位SPI”。与传统的SPI接口相比,Octal SPI接口采用八个数据线进行并行数据传输,可以实现更高的数据传输速率和带宽。
Octal SPI接口具有以下特点:
Octal SPI接口在一些高速、大带宽和低功耗的应用场合下非常有用,比如移动终端产品、音频和视频处理器、图像传感器、存储芯片等。但同时也需要注意,Octal SPI接口需要更多的信号线和复杂的硬件电路支持,设计难度较大,成本也相应增加。
ESP32-S3 集成了 4 个 SPI 外设。
SPI0 和 SPI1 特性:
SPI2 特性:
SPI3 特性:
下表给出了与 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 主驱动程序管理主机与设备的通信。该驱动程序支持以下功能:
注意:
SPI 主驱动器具有多个设备连接到单个总线的概念(共享单个 ESP32-S3 SPI 外设)。只要每个设备仅由一个任务访问,驱动程序就是线程安全的。但是,如果多个任务尝试访问同一个 SPI 设备,则驱动程序不是线程安全的。在这种情况下,建议:
- 构建一个独立任务访问 SPI 设备,其他需要使用 SPI 设备的都通过任务通讯方式操作。
- 使用为共享设备添加互斥锁 xSemaphoreCreateMutex。
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_transaction_t::rxlength 只确定接收到缓冲区的数据长度。
在半双工事务中,读取和写入阶段不是同时的(一次一个方向)。写入和读取阶段的长度分别由结构的length和成员决定。rxlengthspi_transaction_t
命令和地址阶段是可选的,因为并非每个 SPI 设备都需要命令和/或地址。这反映在设备的配置中:如果 command_bits 或 address_bits 设置为零,则不会发生任何命令或地址阶段。
读取和写入阶段也可以是可选的,因为并非每个事务都需要写入和读取数据。如果 rx_buffer为 NULL 且未设置SPI_TRANS_USE_RXDATA ,则跳过读取阶段。如果 tx_buffe r为 NULL 且未设置 SPI_TRANS_USE_TXDATA,则跳过写入阶段。
驱动程序支持两种类型的事务:中断事务和轮询事务。程序员可以选择为每个设备使用不同的事务类型。
在使用 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 总线配置标志可选项: |
intr_flags 中断分配标志可选项有:
在这个配置项中,主要是将 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 设备的句柄可用于配置和操作该设备。 |
上文书讲到,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 表示成功,其他表示失败。 |
设备挂载好之后,便可以使用 spi_device_polling_transmit() 或 spi_device_queue_trans() 向SPI设备发送需要的数据。
在 ESP32 中进行 SPI 数据传输,使用的都是 事务,事务是指从开始到结束的一个完整的数据传输过程。
一个完整的事务包含以下几个步骤:
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 总线传输的协议,具体作用如下:
cmd 字段
cmd 字段用于指定命令字,通过命令字可以控制从设备的工作状态和使能信号等。通常情况下,命令字是从设备手册中定义的固定值。在执行 SPI 总线传输时,如果需要发送命令字,则可以将其写入到 cmd 字段所对应的缓冲区中,然后通过 spi_device_transmit() 函数一并发送出去。
例如,在读取某些 SPI 设备的寄存器值时,需要先向从设备发送一个命令字表示要读取哪个寄存器地址,然后从设备会返回指定地址的寄存器值。这时,可以将命令字写入 cmd 字段所对应的缓冲区中,在 spi_device_transmit() 函数中发送出去,然后在接收数据时,SPI 从设备会将返回的寄存器值写入到 rx_buffer 所对应的缓冲区中。
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 数组时,通常需要按照以下顺序填充各个字段:
成员 spi_transaction_t::rxlength 只确定接收到缓冲区的数据长度。
命令和地址阶段是可选的,因为并非每个 SPI 设备都需要 命令 和 地址 。这反映在设备的配置中:如果command_bits 和 address_bits设置为零,则不会发生任何命令或地址阶段。
SPI 在进行数据传输的时候,MOSI 发送 MISO 接收,如果是在全双工模式下,主机每送出一位数据,同时就可以接收到一位数据,当一个字节发送完毕后,也就意味着主机可以从从机中获得一个字节的数据。
ESP32 的实物传输允许采用 中断 或 轮询 方式发送事务,两种传输方式对应的函数分别是 spi_device_transmit 和 spi_device_polling_transmit。
另外,需要注意的是,在使用 spi_device_transmit 函数时,需要先配置一个中断处理程序(即使用 esp_intr_alloc() 函数分配一个 ESP32 中断向量),否则传输过程中的中断将无法被正确处理,导致数据传输失败。而在使用 spi_device_polling_transmit 函数时,则不需要分配中断向量。
两种方式的不同点在于:
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() 函数可以提高系统的响应速度和资源利用率,但需要注意预留合适的堆空间以存储消息,并及时处理传输错误。
esp_err_t spi_bus_remove_device(spi_device_handle_t handle);
移除设备之后,记得释放与该设备相关所开辟的一切内存。
esp_err_t spi_bus_free(spi_host_device_t host_id);
释放总线之后,记得释放与该总线相关所开辟的一切内存。
虽然主机模式和从机模式中的设备都属于 同一个SPI 设备,但从机明显要显得卑微一些。
从机中无需初始化总线,直接初始化设备信息即可,因为总线只有主机可以操控,但在初始化从机设备的时候,仍需要将总线信息传入给 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,所以在执行闪存操作期间可能会导致回调崩溃。 |
在 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_trans 和 spi_slave_get_trans_result 进行数据传输。
esp_err_t spi_slave_free(spi_host_device_t host);
代码共享位置:http://192.168.172.17:3000/Mars.CN/ESP-IDF-S2-SPI.git