当前位置: 首页 > news >正文

ESP32学习笔记_Peripherals(5)——SPI主机通信

摘要
SPI(串行外设接口)是一种高速全双工同步串行通信总线,采用主从架构,由主设备控制一个或多个从设备;SPI使用四条信号线(SCLK、MOSI、MISO、CS)实现通信,传输速度快(可达10MHz以上),具有接线简单、支持多从设备等优点。ESP32-S3芯片集成四个SPI控制器,其中SPI2和SPI3作为通用SPI总线使用;SPI通信过程包括主机初始化、选择从机、数据传输和结束通信四个阶段;ESP32的SPI传输事务由命令、地址、Dummy、写入和读取五个阶段组成,可通过配置结构体spi_bus_config_tspi_device_interface_config_tspi_transaction_t灵活设置参数

文章目录

      • Workflow 工作流程
      • 传输事务属性
        • spi_bus_config_t
        • spi_device_interface_config_t
          • SPI 时钟频率
          • SPI 模式配置
          • 多个同类型设备共用 SPI 设备配置
        • spi_transaction_t
      • SPI 驱动程序
        • 初始化 SPI 总线
        • 初始化 SPI 设备
        • 中断传输事务 Interrupt Transactions
        • 轮询传输事务 Polling Transactions
        • 卸载驱动
        • SPI 总线锁
      • 数据传输
        • 数据参数
        • 非 uint8_t 的整数传输
        • 传输线模式配置
        • 时钟设置
      • 传输阶段
        • 命令阶段和地址阶段
        • 写入和读取阶段
      • 传输速度限制因素
      • 传输事务持续时间
      • 缓存缺失
      • Example


参考资料
SPI Master - ESP-IDF 编程指南
GPIO & RTC GPIO - ESP-IDF 编程指南
时钟树 - ESP-IDF 编程指南
Espressif ESP32-S3 技术参考手册
Espressif ESP32-S3 系列芯片 技术规格书
BOSCH Sensortec BMI270 Datasheet
BMI270_SensorAPI - Github



SPI(Serial Peripheral Interface,串行外设接口)是一种高速、全双工、同步的串行通信总线;SPI 采用主从架构,由一个主设备控制一个或多个从设备

由于 SPI 推挽输出的特性,使得 SPI 总线只能有一个主设备,而使用开漏连接 I2C 总线可以有多个主设备
I2C学习笔记_开漏连接 - CSDN

SPI 一般使用四条信号线

  • SCLK - 串行时钟
  • MOSI - 主出从入
  • MISO - 主入从出
  • CS/SS - 片选信号

其中主设备产生时钟信号,数据在时钟边沿同步传输

SPI 具有传输速度快(可达到 10MHz 以上,而 I2C 一般为 400k)、接线简单、支持多从设备等优点,广泛应用于微控制器与存储器(如 Flash)、传感器、显示屏等外设之间的通信

在使用前,需要在 main 文件夹的 CMakeLists.txt 中加入以下配置

PRIV_REQUIRES driver# example
idf_component_register(SRCS "my_spi_test.c"PRIV_REQUIRES spi_flashINCLUDE_DIRS ""PRIV_REQUIRES driver)# or
idf_component_register(SRCS "my_spi_test.c"PRIV_REQUIRES spi_flash driverINCLUDE_DIRS "")

SPI 通信过程

  1. 主机初始化:主机配置 SPI 总线(SCLK、MOSI、MISO、CS 等引脚)
  2. 选择从机:主机拉低对应的 CS 引脚,选中目标从机
  3. 数据传输:
    1. 主机通过 MOSI 线发送数据
    2. 如果需要从机返回数据,则主机通过 MISO 线读取
    3. 数据传输由主机产生的时钟信号同步
  4. 结束通信:主机拉高 CS 引脚,结束本次与从机的通信

整个过程是全双工或半双工,主机控制时序和数据流向,从机被动响应

ESP32-S3 芯片集成了四个 SPI 控制器

  • SPI0:主要供内部使用以访问外部 flash/PSRAM
  • SPI1:主要供内部使用以访问外部 flash/PSRAM
  • SPI2(通用 SPI (GP-SPI)控制器):可作为主机或从机,支持多种 SPI 模式,适合连接外部 SPI 设备
  • SPI3(通用 SPI 控制器):可作为主机或从机,支持多种 SPI 模式,适合连接外部 SPI 设备
    其中,SPI2 和 SPI3 作为通用 SPI 总线使用,SPIO 和 SPI1 通常用于片上或片外存储器的访问

Espressif ESP32-S3 技术参考手册 P1047

Workflow 工作流程

ESP32 的 SPI 总线传输事务由五个阶段构成,任意阶段均可根据实际需求跳过

  • 如果将 command_bitsaddress_bits 设置为 0,即不会进入对应阶段
  • 如果将 spi_transaction_t::rx_buffer 设置为 NULL,且未设置 SPI_TRANS_USE_RXDATA,读取阶段将被跳过
  • 如果将 spi_transaction_t::tx_buffer 设置为 NULL,且未设置 SPI_TRANS_USE_TXDATA,写入阶段将被跳过
阶段描述
命令阶段 Command主机向总线发送命令字段,长度为 0-16 位
地址阶段 Address主机向总线发送地址字段,长度为 0-32 位
Dummy 阶段可自行配置,用于适配时序要求
写入阶段 Write主机向设备传输数据,这些数据在紧随命令阶段和地址阶段之后,从电平的角度来看,数据与命令没有区别
读取阶段 Read主机读取设备数据

SPI 主机驱动程序负责管理主机与设备间的通信,具有以下特性

  • 支持多线程环境使用
  • 读写数据过程中 DMA 透明传输
  • 同一信号总线上不同设备的数据可自动时分复用

传输事务属性

传输事务属性由总线配置结构体 spi_bus_config_t设备配置结构体 spi_device_interface_config_t传输事务配置结构体 spi_transaction_t 共同决定

  • spi_bus_config_t 配置总线参数,包括 IO、中断 CPU、优先级等
  • spi_device_interface_config_t 设备接口配置,包括 SPI 工作模式、时钟源、时钟速度等
  • spi_transaction_t 传输事务配置,包括命令数据、地址数据等
spi_bus_config_t

SPI 总线配置结构体,可以使用这个结构体来指定总线的 GPIO 引脚

总线配置结构体 spi_bus_config_t 的参数

参数名类型描述
mosi_io_numint主机输出从机输入(MOSI,即 spi_d)信号的 GPIO 引脚号,不使用时设为 -1
data0_io_numint双线/四线/八线模式下 spi data0 信号的 GPIO 引脚号,不使用时设为 -1
miso_io_numint主机输入从机输出(MISO,即 spi_q)信号的 GPIO 引脚号,不使用时设为 -1
data1_io_numint双线/四线/八线模式下 spi data1 信号的 GPIO 引脚号,不使用时设为 -1
sclk_io_numintSPI 时钟信号的 GPIO 引脚号,不使用时设为 -1
quadwp_io_numint写保护(WP) 信号的 GPIO 引脚号,不使用时设为 -1
data2_io_numint四线/八线模式下 spi data2 信号的 GPIO 引脚号,不使用时设为 -1
quadhd_io_numint保持(HD)信号的 GPIO 引脚号,不使用时设为 -1
data3_io_numint四线/八线模式下 spi data3 信号的 GPIO 引脚号,不使用时设为 -1
data4_io_numint八线模式下 spi data4 信号的 GPIO 引脚号,不使用时设为 -1
data5_io_numint八线模式下 spi data5 信号的 GPIO 引脚号,不使用时设为 -1
data6_io_numint八线模式下 spi data6 信号的 GPIO 引脚号,不使用时设为 -1
data7_io_numint八线模式下 spi data7 信号的 GPIO 引脚号,不使用时设为 -1
iocfg[9]int按上述顺序排列的 GPIO 配置数组格式
data_io_default_levelbool无事务时数据 IO 输出的默认电平
max_transfer_szint最大传输大小(字节):启用 DMA 时默认为 4092,禁用 DMA 时默认为 SOC_SPI_MAXIMUM_BUFFER_SIZE
flagsuint32_t驱动程序要检查的总线能力,SPICOMMON_BUSFLAG_标志的或值
isr_cpu_idesp_intr_cpu_affinity_t注册 SPI 中断服务程序的 CPU 核心
intr_flagsint用于设置总线优先级和 IRAM 属性的中断标志,参见 esp_intr_alloc.h

注意:从机驱动程序不使用 quadwp/quadhd 线,可以安全地将这些字段保持未初始化状态

通常情况下,驱动程序会使用 GPIO 矩阵来路由信号,但有一个例外:当所有信号都可以通过 IO_MUX 路由或设置为-1 时,系统会使用 IO_MUX
只要有一个信号是通过 GPIO 矩阵路由的,那么所有的信号都将通过路由(保证了信号同步和时序一致性,但也会带来额外的延迟,影响高频性能)

IO_MUX(IO 多路复用器) 是 ESP32 芯片内部的硬件直连路由系统,它提供了 GPIO 引脚与外设功能之间的专用硬件连接通道;当使用 IO_MUX 时,信号直接通过硬件路由,没有额外的延迟,性能最优,但 IO_MUX 的缺点是灵活性较低,每个外设只能使用预定义的特定 GPIO 引脚,不能任意选择引脚
GPIO 矩阵(GPIO Matrix) 是 ESP32 的可编程信号路由系统,它允许任意 GPIO 引脚连接到任意外设功能,提供了极高的引脚配置灵活性;通过软件配置,可以将 SPI、I2C、UART 等外设信号路由到几乎任何可用的 GPIO 引脚上;在 ESP32 上,使用 GPIO 矩阵会带来大约 25 ns 的输入延迟,可能会导致在超过 40 MHz 速度时出现读取错误

SPI 主机驱动程序(ESP32)- ESP-IDF 编程指南(官方文档未给出 S3 型号的值)
对于 ESP32-S3当 SPI 主机被设置为 80 MHz 或更低的频率时,通过 GPIO 矩阵路由 SPI 管脚的行为将与通过 IOMUX 路由相同(GPIO 矩阵和 IO_MUX 路由方式在功能和性能上没有区别,可以使用 GPIO 矩阵实现灵活引脚分配;只有在追求更高 SPI 速率时,才建议优先使用 IO_MUX 专用引脚以获得最佳性能)
SPI 主机驱动程序(ESP32S3)- ESP-IDF 编程指南

ESP32-S3 SPI 总线的 IO_MUX 管脚如下表所示

管脚名称GPIO 编号 (SPI2)
CS010
SCLK12
MISO13
MOSI11
QUADWP14
QUADHD9
如果有多设备挂载在总线上,可以用设置 GPIO 电平的方式断言 (Assertion)或去断言 (De-assertion)一条 CS 线
gpio_set_level();

只有连接到总线的第一台设备可以使用 CS0 管脚

从机驱动程序不使用 quadwp/quadhd 线,因此 spi_bus_config_t 中引用这些线的字段将被忽略,可以保持未初始化状态

spi_device_interface_config_t

SPI 设备接口配置结构体 spi_device_interface_config_t 的参数

参数名类型描述
command_bitsuint8_t命令阶段的默认位数(0-16),当不使用 SPI_TRANS_VARIABLE_CMD 时使用,否则忽略
address_bitsuint8_t地址阶段的默认位数(0-64),当不使用 SPI_TRANS_VARIABLE_ADDR 时使用,否则忽略
dummy_bitsuint8_t在地址阶段和数据阶段之间插入的虚拟位数
modeuint8_tSPI 模式,表示(CPOL, CPHA)配置对:0:(0,0)、1:(0,1)、2:(1,0)、3:(1,1)
clock_sourcespi_clock_source_t选择 SPI 时钟源,默认为 SPI_CLK_SRC_DEFAULT
duty_cycle_posuint16_t正时钟的占空比,以 1/256 递增(128=50%/50%占空比);设为 0 等同于设为 128
cs_ena_pretransuint16_t传输前 CS 激活的 SPI 位周期数(0-16);仅适用于半双工传输
cs_ena_posttransuint8_t传输后 CS 保持激活的 SPI 位周期数(0-16)
clock_speed_hzintSPI 时钟速度(Hz);从 clock_source 派生
input_delay_nsint从机的最大数据有效时间:SCLK 和 MISO 有效之间的时间,包括从机到主机的可能时钟延迟,高频(>8MHz)建议设置有效值
sample_pointspi_sampling_point_tSPI 主机接收位的采样点调优
spics_io_numint此设备的 CS GPIO 引脚号,不使用时设为-1
flagsuint32_tSPI_DEVICE_标志的按位或组合
queue_sizeint事务队列大小:设置同时可以"在空中"(已排队但未完成)的事务数量
pre_cbtransaction_cb_t传输开始前调用的回调函数;在中断上下文中调用,为最佳性能应放在 IRAM 中
post_cbtransaction_cb_t传输完成后调用的回调函数;在中断上下文中调用,为最佳性能应放在 IRAM 中

spi_device_interface_config_t 结构体中的 input_delay_ns 用于设置从 SCLK 发射沿到 MISO 数据有效的最大输入延迟(单位为纳秒)
这个参数的设置有助于主机驱动正确计算最大可用 SPI 时钟频率,尤其是在全双工模式下,可以参考芯片手册的 AC 特性,或者通过示波器测量实际延迟

典型值如:ESP32 使用 IO_MUX 时为 50ns,使用 GPIO_MATRIX 时为 75ns
如果设置为 0,驱动仍可正常工作,但建议填写准确值以获得更好的时序补偿和频率计算

回调函数(如 pre_cb、post_cb)不是必须设置的,如果应用不需要在传输前后执行特定操作,可以将这些回调函数设置为 NULL,驱动会自动跳过回调处理

SPI 时钟频率

GPSPI 外设的时钟源可以通过设置 spi_device_interface_config_t::clock_source 选择

可用的时钟源参阅 spi_clock_source_t
时钟树 - ESP-IDF 编程指南

默认情况下,驱动程序将把时钟源设置为 SPI_CLK_SRC_DEFAULT;这往往代表 GPSPI(通用 SPI 控制器) 可选时钟源中的最高频率,在不同的芯片上数值会有所不同

设备的实际时钟频率可能不完全等于所设置的数字,驱动会将其重新计算为与硬件兼容的最接近的数字,并且不超过时钟源的时钟频率
调用函数 spi_device_get_actual_freq() 可以了解驱动计算的实际频率

设备的时钟频率可在传输过程中实时更改,可以通过设置 spi_transaction_t::override_freq_hz 实现,为当前设备的此次及以后的传输使用新的时钟频率
如果某次期望设置的时钟频率无法实现,驱动将打印警告并继续使用之前的时钟频率进行传输

ret = spi_bus_remove_device(spi_handle1);
if (ret != ESP_OK) {ESP_LOGE(TAG, "Failed to remove SPI device 1: %s", esp_err_to_name(ret));return ret;
}ESP_LOGI(TAG, "Deleted SPI devices, reconfiguring for high speed...");
ESP_LOGI(TAG, "Changing SPI CLK speed to 10MHz");spi_device_interface_config_t dev_cfg1 = {.clock_speed_hz = BMI270_CLK_SPEED_HZ_10MHz,  // 10MHz for burst communication.mode           = 0,                          // SPI mode 0 (CPOL=0, CPHA=0) - BMI270 standard.spics_io_num   = -1,                         // Manual CS control - do not use hardware CS.queue_size     = 1,
};ret = spi_bus_add_device(BMI270_HOST1, &dev_cfg1, &spi_handle1);
if (ret != ESP_OK) {ESP_LOGE(TAG, "Failed to add SPI device 1: %s", esp_err_to_name(ret));return ret;
}
SPI 模式配置

SPI 模式(Mode)用来描述 SPI 总线时钟的极性(CPOL)和相位(CPHA)这两个参数的组合
CPOL(时钟极性):决定空闲时 SCK(时钟线)的电平

  • 0 空闲时 SCK 为低电平
  • 1 空闲时 SCK 为高电平
    CPHA(时钟相位):决定数据采样的时钟沿
  • 0 第一个时钟沿(片选有效后,SCK 从空闲电平开始的第一个和第二个边沿)采样数据
  • 1 第二个时钟沿采样数据

四种 SPI 模式的对应关系

模式(CPOL, CPHA)说明
0(0, 0)SCK 空闲为低,数据在 SCK 下降沿变化,在 SCK 上升沿采样
1(0, 1)SCK 空闲为低,数据在 SCK 上升沿变化,在 SCK 下降沿采样
2(1, 0)SCK 空闲为高,数据在 SCK 上升沿变化,在 SCK 下降沿采样
3(1, 1)SCK 空闲为高,数据在 SCK 下降沿变化,在 SCK 上升沿采样

Espressif ESP32-S3 技术参考手册 P1083

多个同类型设备共用 SPI 设备配置

对于同一类型的设备,可以使用同一个设备配置,但不设置 CS 引脚,而是在传输时手动拉低相应的 CS 引脚
如下图所示:所有设备的 MOSI、MISO、SCLK 共用,而每个设备(D1~Dn)的 CS 单独控制

 +---------------+|    SPI Host   |+-------+-------+|
CS1 -----+-------- D1|
CS2 -----+-------- D2:|
CSn -----+-------- Dn

配置 GPIO 工作方式用于手动拉高/低 CS Pin

GPIO & RTC GPIO - ESP-IDF 编程指南

gpio_config_t cs_conf = {.pin_bit_mask = (1ULL << CS_PIN_1),.mode         = GPIO_MODE_OUTPUT, // Set gpio output mode.pull_up_en   = GPIO_PULLUP_ENABLE,.pull_down_en = GPIO_PULLDOWN_DISABLE,.intr_type    = GPIO_INTR_DISABLE};esp_err_t ret = gpio_config(&cs_conf);
if (ret != ESP_OK) {ESP_LOGE(TAG, "Failed to configure CS pin GPIO%d: %s", i, CS_PIN_1, esp_err_to_name(ret));return ret;
}// Set CS high initially (inactive)
gpio_set_level(CS_PIN_1, 1);
ESP_LOGI(TAG, "Bus1 CS pin %d (GPIO%d) configured", i, CS_PIN_1);/* --- */// Pull down CS pin
gpio_set_level(CS_PIN_1, 0);
spi_transaction_t

SPI 事务结构体 spi_transaction_t 的参数

参数名类型描述
flagsuint32_tSPI_TRANS_ 标志的按位或组合
cmduint16_t命令数据,长度由 spi_device_interface_config_t 中的 command_bits 设置;例如:写入 0x0123 且 command_bits=12 时发送命令 0x12, 0x3_
addruint64_t地址数据,长度由 spi_device_interface_config_t 中的 address_bits 设置;例如:写入 0x123400address_bits=24 时发送地址 0x12, 0x34, 0x00
lengthsize_t总数据长度,以位为单位
rxlengthsize_t接收的总数据长度,在全双工模式下不应大于 length(0 时默认为 length 的值)
override_freq_hzuint32_t为当前设备覆盖的新频率速度值,保持 0 跳过更新,每次需要约 30us
uservoid*用户自定义变量,可用于存储事务 ID 等
tx_bufferconst void*发送缓冲区指针,无 MOSI 阶段时设为 NULL
tx_data[4]uint8_t如果设置了 SPI_TRANS_USE_TXDATA,直接从此变量发送数据
rx_buffervoid*接收缓冲区指针,无 MISO 阶段时设为 NULL;使用 DMA 时按 4 字节单位写入
rx_data[4]uint8_t如果设置了 SPI_TRANS_USE_RXDATA,数据直接接收到此变量

一个 SPI 主机可以发送全双工传输事务,此时读取和写入阶段同步进行,传输事务总长度取决于以下结构体成员长度总和

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

spi_transaction_t::rxlength 决定了接收到的数据包长度

在半双工传输事务中,读取和写入阶段独立进行(一次一个方向),写入和读取阶段的长度由 spi_transaction_t::lengthspi_transaction_t::rxlength 分别决定

并非每个 SPI 设备都要求命令和地址,因此命令阶段和地址阶段为可选项,反映在设备的配置中,如果 spi_device_interface_config_t::command_bitsspi_device_interface_config_t::address_bits 被设置为零,则不会唤起命令或地址阶段

并非每个传输事务都需要写入和读取数据,因此读取和写入阶段也是可选项

  • 如果将 spi_transaction_t::rx_buffer 设置为 NULL,且未设置 SPI_TRANS_USE_RXDATA,读取阶段将被跳过
  • 如果将 spi_transaction_t::tx_buffer 设置为 NULL,且未设置 SPI_TRANS_USE_TXDATA,写入阶段将被跳过

SPI 驱动程序

初始化 SPI 总线

通过调用函数 spi_bus_initialize() 初始化 SPI 总线,在结构体 spi_bus_config_t 中设置所需的 I/O 管脚,不需要的信号设置为 -1

spi_bus_initialize(SPI2_HOST, &bus_config, dma_chan);
参数名类型描述
host_idspi_host_device_t控制该总线的 SPI 外设标识符(如 SPI2_HOST、HSPI_HOST 等)
bus_configconst spi_bus_config_t*指向 SPI 总线配置结构体的指针,指定 GPIO 引脚和总线参数
dma_chanspi_dma_chan_tDMA 通道选择,影响传输大小限制和性能
DMA 设置说明
SPI_DMA_DISABLED禁用 DMA,限制传输大小,如果只有 SPI 闪存使用此总线,则设置为 SPI_DMA_DISABLED
SPI_DMA_CH_AUTO自动分配 DMA 通道
初始化 SPI 设备

调用函数 spi_bus_add_device() 注册连接到总线的设备,用参数 spi_device_interface_config_t 配置设备时序要求,获得设备句柄

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)
  • handle 用于存储设备句柄的变量指针

spi_bus_remove_device() 卸载设备

主机驱动程序支持两种类型的传输事务:中断传输事务和轮询传输事务
可以选择在不同设备上使用不同的传输事务类型,设备不能同时使用两种传输事务类型,但两种传输事务可以交替运行

在一个或多个 spi_transaction_t 结构体中填充所需的传输事务参数,使用轮询传输事务或中断传输事务发送这些结构体
[[SPI#Example]]

中断传输事务 Interrupt Transactions

传输由中断服务程序(ISR)处理

  • 应用可以将多个传输事务加入队列,驱动在 ISR 中自动逐一发送数据
  • 中断传输事务将阻塞传输事务程序,该任务会等待 SPI 传输完成,CPU 可以在传输过程中切换到其他任务,提高多任务并发能力,节约 CPU 时间

调用函数 spi_device_queue_trans() 将传输事务添加到队列中,使用函数 spi_device_get_trans_result() 查询结果,或将所有请求输入 spi_device_transmit() 实现同步处理

esp_err_t spi_device_queue_trans(spi_device_handle_t handle, spi_transaction_t *trans_desc, TickType_t ticks_to_wait)
  • trans_desc 要执行的事务描述
    这个函数会把传输事务加入队列或启动传输,但不会等待传输完成就立即返回,可以连续发起多个传输
esp_err_t spi_device_get_trans_result(spi_device_handle_t handle, spi_transaction_t **trans_desc, TickType_t ticks_to_wait)
  • trans_desc 指向一个变量的指针(注意是二级指针),该变量能够包含正在执行的传输的描述指针,直到描述通过 spi_device_get_trans_result 返回之前,不应修改该描述
  • 查询结果的状态(如是否成功)由函数的返回值决定
esp_err_t spi_device_transmit(spi_device_handle_t handle, spi_transaction_t *trans_desc)

这个函数等同于调用 spi_device_queue_trans() 后跟 spi_device_get_trans_result()每次只能处理一个传输,必须等这次传输结束后才能进行下一次
当仍有从 spi_device_queue_trans()polling_start/transmit 单独启动但未最终完成的传输时,不要使用此函数

相当于先把传输事务加入队列然后等待并获取结果
不能同时混用同步(spi_device_transmit)和异步(spi_device_queue_trans/polling_start)的事务处理,必须等前面的异步事务全部完成后,才能安全调用 spi_device_transmit()

当多个任务访问同一 SPI 设备时,此函数不是线程安全的(如果多个任务同时对同一个 SPI 设备调用该函数,可能会导致数据冲突或不可预期的行为,为每个 SPI 设备加互斥锁,或保证同一时刻只有一个任务访问该设备)
通常,同一个 SPI 设备不能同时进行轮询(polling)和中断(interrupt)事务,如果已经用轮询方式发起了传输,就不能再用中断方式发起新的传输,反之亦然;必须等前一种方式的所有传输都完成后,才能切换到另一种方式
为更好地控制函数的调用顺序,只在单个任务中向同一设备发送混合传输事务

轮询传输事务 Polling Transactions

CPU 持续轮询 SPI 主机的状态位,直到传输完成

  • 节省了用于队列处理和上下文切换的时间,减少了传输事务持续时间
  • 在传输期间,CPU 会被占用,处于忙碌状态(程序不断轮询 SPI 主机的状态位,直到传输事务完成),不能执行其他任务
  • 可以通过 spi_device_acquire_bus()spi_device_release_bus() 打包一系列轮询传输,进一步减少开销

spi_device_acquire_bus() 获取总线
若需连续发送专门的 SPI 传输事务以提高效率,可采用获取总线的方式;获取总线后,与其他设备间的传输事务(包括轮询传输事务或中断传输事务)将处于待处理状态,直到总线被释放

执行中断传输事务时,发起传输的任务会被阻塞,等待 ISR 和传输完成,但 CPU 可以运行其他任务;轮询传输事务则会让 CPU 持续忙于等待 SPI 完成,减少了上下文切换和队列处理的开销,但会占用 CPU 资源,无法运行其他任务

ISR 会干扰飞行中的轮询传输事务,以适应中断传输事务,在调用 spi_device_polling_start() 前,确保所有发送到 ISR 的中断传输事务已经完成,可以通过持续调用 spi_device_get_trans_result(),直至全部传输事务返回来保证 ISR 中的传输事务完成

调用函数 spi_device_polling_transmit() 发送一个轮询传输,等待其完成,并返回结果

esp_err_t spi_device_polling_transmit(spi_device_handle_t handle, spi_transaction_t *trans_desc)

若有插入内容的需要,也可以使用 spi_device_polling_start()spi_device_polling_end() 发送传输事务

spi_device_polling_start(handle, &trans_desc, portMAX_DELAY);
// 在这里插入自定义处理,比如处理部分数据或准备下一个传输
spi_device_polling_end(handle, portMAX_DELAY);

传输事务完成后,spi_device_polling_end() 程序需要至少 1 µs 的时间来解除阻塞其他任务,可以调用函数 spi_device_acquire_bus()spi_device_release_bus()打包一系列轮询传输事务以避免开销

如果要向设备发送背对背传输事务,也要在发送传输事务前调用函数 spi_device_acquire_bus(),并在发送传输事务后调用函数 spi_device_release_bus()

SPI 背对背传输事务(back-to-back transactions) 是指在 SPI 主机驱动中,通过调用 spi_device_acquire_bus() 获取总线控制权后,可以连续向同一设备发送多个传输事务,而无需在每次传输之间释放总线,这种方式可以减少总线的争用,提高多次连续传输的效率
完成连续传输后,需要调用 spi_device_release_bus() 释放总线控制权
适用于需要对同一 SPI 设备进行多次连续操作且希望保证操作的原子性(所有对该 SPI 设备的操作不会被其他设备或任务打断,其他 SPI 设备的传输请求会被挂起,直到释放总线为止,从而避免了总线争用,保证了这些操作的连续性和完整性)和效率的场景

卸载驱动

从总线上移除特定设备,以设备句柄为参数调用函数 spi_bus_remove_device()

esp_err_t spi_bus_remove_device(spi_device_handle_t handle)

要复位(或重置)总线,先确保总线上所有设备都被移除,然后调用函数 spi_bus_free()

esp_err_t spi_bus_free(spi_host_device_t host_id)
SPI 总线锁

为了多路复用来自 SPI 主机、SPI flash 等不同驱动的设备,为了确保同一时刻只有一个设备能独占总线,避免资源冲突和数据错误,每个 SPI 总线上都配有 SPI 总线锁,驱动程序可以通过对锁实施仲裁,将设备连接到总线上

每个总线锁都已初始化并注册了后台服务 (BG,Background Service),设备在进行数据传输前,需等待后台服务被禁用,确保总线在安全、可控的状态下被访问

对 SPI0/1 总线
只有 SPI flash 驱动程序可以连接到 SPI1 总线,SPI 主机驱动暂不支持 SPI1 总线
SPI1 总线(有时也称为 SPI0/1 总线)是 ESP 芯片内部用于连接外部 flash(和部分芯片的 PSRAM)的专用总线
指令/数据 cache(即 CPU 取指和数据访问的高速缓存)与 SPI1 总线共享硬件资源;也就是说,CPU 运行的代码和数据,很多时候是通过 cache 从外部 flash 读取的,而 flash 就挂在 SPI1 总线上

对 ESP32-S3:
SPI0:供 ESP32-S3 的 GDMA 控制器与 Cache 访问封装内或封装外 flash/PSRAM
SPI1:供 CPU 访问封装内或封装外 flash/PSRAM
Espressif ESP32-S3 系列芯片 技术规格书 P48

当 SPI1 总线被用于 flash 读写或其他操作时(比如 SPI flash 驱动进行擦写),必须禁用 cache,如果在 SPI1 总线上进行操作时 cache 还在工作,可能会导致数据冲突或系统异常(比如 cache 访问 flash 时,flash 正在被写入或擦除,数据就不一致了)
cache 处于禁用状态时,让出当前任务的执行权毫无意义,因为其他任务(包括 ISR)也无法运行,代码都在 flash 上,cache 被禁用无法取指,因此,该情况下 SPI1 总线上的任何设备都无法使用 ISR

对其他总线
对于其他总线,驱动程序可以将 ISR 注册为后台服务,若设备任务要求独占总线(如在轮询传输模式通过 spi_device_acquire_bus() 占用总线),则总线锁将阻塞该任务,同时禁用 ISR,随即解除对该任务的阻塞,任务释放锁后,如果 ISR 中还有待处理的事务,则锁将尝试重新启用 ISR

数据传输

数据参数
  • 如果使用的是 spi_transaction_t 结构体中的 tx_buffer 字段(即没有设置 SPI_TRANS_USE_TXDATA,或者数据长度大于 32 位),可以直接修改 tx_buffer 指向的内存内容来改变 SPI 的传输数据内容,驱动会从 tx_buffer 指向的内存区域读取数据进行传输
  • 如果启用了 DMA,tx_buffer 指向的内存需要满足 DMA 要求(如 32 位对齐、长度为 4 字节的倍数等),否则会影响传输效率
  • 如果设置了 SPI_TRANS_USE_TXDATA,则应只操作 tx_data 字段,不要同时修改 tx_buffer,因为两者在结构体中的内存重叠,修改其中一个会影响另一个,可能导致数据混乱或不可预期的行为
非 uint8_t 的整数传输

SPI 主机逐字节地将数据读入和写入内存,默认情况下,数据优先以最高有效位 (MSB) 发送,极少数情况下会优先使用最低有效位 (LSB),如果需要发送一个小于 8 位的值,这些位应以 MSB 优先的方式写入内存

例,如果需要发送 0b00010,则应将其写成 uint8_t 变量,读取长度设置为 5 位,此时,设备仍然会收到 8 位数据,并另有 3 个“随机”位(可能是 0,也可能是内存中原有的值,即收到的数据位 0b00010xxx),所以读取过程必须准确

ESP32-S3 属于小端芯片(uint16_t 和 uint32_t 变量的最低有效位存储在最小的地址),因此如果 uint16_t 存储在内存中,则首先发送位 7:0,其次是位 15:8

在某些情况下,要传输的数据大小与 uint8_t 数组不同,可使用以下宏将数据转换为可由 SPI 驱动直接发送的格式

  • 需传输的数据,使用 SPI_SWAP_DATA_TX
  • 接收到的数据,使用 SPI_SWAP_DATA_RX
SPI_SWAP_DATA_TX(DATA, LEN)uint16_t data = SPI_SWAP_DATA_TX(0x145, 9);
// 发送 9 位数据

将长度不超过 32 位的无符号整数转换为 SPI 驱动程序可直接发送的格式

  • DATA 要发送的数据,可以是 uint8_tuint16_tuint32_t
  • LEN 要发送的数据长度,由于 SPI 外设从 MSB 开始发送,将数据移至 MSB
传输线模式配置

ESP32-S3 支持的线路模式

模式命令位宽地址位宽数据位宽传输事务标志信号总线 IO 设置标志信号
普通 SPI 模式11100
双线输出模式112SPI_TRANS_MODE_DIOSPICOMMON_BUSFLAG_DUAL
双线 I/O 模式122SPI_TRANS_MODE_DIO SPI_TRANS_MULTILINE_ADDRSPICOMMON_BUSFLAG_DUAL
四线输出模式114SPI_TRANS_MODE_QIOSPICOMMON_BUSFLAG_QUAD
四线 I/O 模式144SPI_TRANS_MODE_QIO SPI_TRANS_MULTILINE_ADDRSPICOMMON_BUSFLAG_QUAD
八线输出模式118SPI_TRANS_MODE_OCTSPICOMMON_BUSFLAG_OCTAL
OPI 模式888SPI_TRANS_MODE_OCT SPI_TRANS_MULTILINE_ADDR SPI_TRANS_MULTILINE_CMDSPICOMMON_BUSFLAG_OCTAL
在结构体 spi_transaction_t 中设置 flags 来使用这些模式,如 传输事务标志信号 一栏所示
检查相应的 IO 管脚是否被设置,请在 spi_bus_config_t 中设置 flags,如 总线 IO 设置标志信号 一栏所示

写入或读取阶段的理论最大传输速度可根据下表计算

写入/读取阶段的线宽速度 (Byte/s)
1-LineSPI 频率 / 8
2-LineSPI 频率 / 4
4-LineSPI 频率 / 2
8-LineSPI 频率

其他阶段(命令阶段、地址阶段、Dummy 阶段)的传输速度计算与此类似

普通 SPI 模式(标准模式)
所有阶段都使用单线传输(MOSI/MISO),每个时钟周期传输 1 位数据
最基本的 SPI 通信方式,兼容性最好
传输速度最慢,但稳定可靠

双线模式
通常为 MOSI 和 MISO 同时作为数据线传输

  • 双线输出模式:只有数据阶段使用 2 条线,命令和地址仍是单线
  • 双线 I/O 模式:地址和数据阶段都使用 2 条线,传输效率更高
    命令阶段仍然使用单线传输
    每个时钟周期可以传输 2 位数据,相比标准模式,数据传输速度提升约 2 倍

四线模式
除了 MOSI 和 MISO,还会用到原本用于 Flash 芯片的 WP(写保护)和 HOLD(保持)引脚,将它们复用为数据线(通常称为 SIO2、SIO3)

  • 四线输出模式:只有数据阶段使用 4 条线
  • 四线 I/O 模式:地址和数据阶段都使用 4 条线
    命令阶段仍然使用单线传输
    每个时钟周期传输 4 位数据,数据传输速度相比标准模式提升约 4 倍

八线模式
在四线的基础上再增加 4 根数据线(SIO4~SIO7),共 8 根数据线

  • 八线输出模式:只有数据阶段使用 8 条线
  • OPI 模式(八线 I/O):命令、地址、数据阶段全部使用 8 条线
    每个时钟周期传输 8 位数据,传输速度最快,但需要更多 GPIO 引脚和兼容的设备
时钟设置

时钟占空比应接近 50%,否则会影响数据识别和接收

ESP32-S3 SPI 传输的时钟速率设置有以下限制
主机模式下
SPI2/SPI3 作为主机时,时钟频率最高可达 80 MHz,无论是全双工还是半双工模式,单线、双线、四线、八线(SDR)都支持最高 80 MHz
八线半双工模式下,DDR 最高 40 MHz

DDR(Double Data Rate,双倍数据速率,适用于和 Flash 和 PSRAM 的通信,需要在 menuconfig 中打开) 指在一个时钟周期的上升沿和下降沿都进行数据采样或传输,这样在同样的时钟频率下,数据吞吐量是 SDR(Single Data Rate,单倍数据速率) 的两倍
例如,80 MHz 的 DDR 实际等效于 160 Mbps 的数据速率,但 ESP32-S3 八线 SPI DDR 模式下最高只支持 40 MHz 时钟
ESP-IDF 编程指南 - SPI Flash 和片外 SPI RAM 配置

从机模式下
SPI2/SPI3 作为从机时,时钟频率最高可达 60 MHz,支持单线、双线、四线半双工和双线全双工通信,八线模式仅支持 SDR
SPI 从机的工作频率最高为 60 MHz

当 SPI 主机被设置为 80 MHz 或更低的频率时,通过 GPIO 矩阵路由 SPI 管脚的行为将与通过 IOMUX 路由相同

Espressif ESP32-S3 系列芯片 技术规格书 P48

传输阶段

命令阶段和地址阶段

在命令阶段和地址阶段,spi_transaction_t::cmdspi_transaction_t::addr 被发送到总线,该过程中无数据读取
命令阶段和地址阶段的默认长度通过调用 spi_bus_add_device()spi_device_interface_config_t 中设置

如果 spi_transaction_t::flags 中的标志信号 SPI_TRANS_VARIABLE_CMDSPI_TRANS_VARIABLE_ADDR 未设置,驱动会自动采用设备初始化时设定的默认长度

更改命令阶段和地址阶段的长度

  1. 声明结构体 spi_transaction_ext_t,它是 spi_transaction_t 的扩展
  2. spi_transaction_ext_t::base 中设置标志信号 SPI_TRANS_VARIABLE_CMD 和/或 SPI_TRANS_VARIABLE_ADDR
  3. 分别在 spi_transaction_ext_t::command_bitsspi_transaction_ext_t::address_bits 字段中,设置本次事务所需的命令和地址长度
成员名称类型描述
basestruct spi_transaction_t基础事务数据,允许将指向 spi_transaction_t 的指针转换为 spi_transaction_ext_t 指针,使用此扩展结构时,需在 base 的 flag 中设置 SPI_TRANS_VARIABLE_CMD_ADR
command_bitsuint8_t当前事务中命令(command)的长度,以位(bits)为单位
address_bitsuint8_t当前事务中地址(address)的长度,以位(bits)为单位
dummy_bitsuint8_t当前事务中虚拟(dummy)周期的长度,以位(bits)为单位
在实际传输时,用 spi_transaction_ext_t::base 的指针代替 spi_transaction_t 的指针即可

如果需要命令阶段和地址阶段的线数与数据阶段保持一致,需要在结构体 spi_transaction_t 中将 SPI_TRANS_MULTILINE_CMDSPI_TRANS_MULTILINE_ADDR 设置进该结构体的 flags 成员变量,这样,命令和/或地址阶段会采用与数据阶段相同的线宽(如双线、四线、八线),而不是默认的单线模式

写入和读取阶段

一般而言,需要传输到设备或由设备读取的数据将由 spi_transaction_t::rx_bufferspi_transaction_t::tx_buffer 指向的内存块中读取或写入,如果传输时启用了 DMA,则缓冲区应:

  • 申请支持 DMA 的内存
  • 32 位对齐(从 32 位边界开始,长度为 4 字节的倍数)

若未满足以上要求,传输事务效率将受到临时缓冲区分配和复制的影响

如果使用多条数据线传输,需在结构体 spi_device_interface_config_t::flags 设置 SPI_DEVICE_HALFDUPLEX 标志信号;结构体 spi_transaction_t 中的 flags 应按照[[#传输线模式配置]]中的描述设置

半双工模式下,不支持“同时具有读取和写入阶段”的传输事务,如果需要在一次事务中同时进行读写(即全双工),使用全双工模式

传输速度限制因素

  • 传输事务间隔时间
  • SPI 时钟频率
  • 缓存缺失的 SPI 函数,包括回调
    影响大传输事务传输速度的主要参数是时钟频率,而多个小传输事务的传输速度主要由传输事务间隔时长决定

传输事务持续时间

传输事务持续时间包括

  • 设置 SPI 外设寄存器
  • 将数据复制到 FIFO 或设置 DMA 链接
  • SPI 传输事务时间

中断传输事务允许附加额外的开销,以适应 FreeRTOS 队列的成本以及任务与 ISR 切换所需的时间

FreeRTOS 队列的成本
中断传输事务通常会将多个传输请求放入队列,由驱动在中断服务程序(ISR)中依次处理,队列的管理(如入队、出队操作)会消耗一定的时间
任务与 ISR 切换的时间
当 SPI 传输完成时,系统需要在任务和 ISR 之间切换,这个上下文切换也会带来时间开销

对于 ISR,CPU 可以在传输事务进行过程中切换到其他任务,这能够节约 CPU 时间,会延长传输事务持续时间
轮询传输事务不会阻塞任务,程序不断轮询 SPI 主机的状态位,直到传输事务完成

DMA

  • 如果 DMA 启用,每个传输事务设置链接列表需要约 2 µs,当主机传输数据时,它会自动从链接列表中读取数据
  • 如果不启用 DMA,CPU 必须自己从 FIFO 中写入和读取每个字节,这一过程时长通常不到 2 µs,但写入和读取的传输事务长度都被限制在 64 字节

单个字节数据的典型传输事务持续时间如下:
使用 DMA 的中断传输事务:26 µs
使用 CPU 的中断传输事务:24 µs
使用 DMA 的轮询传输事务:11 µs
使用 CPU 的轮询传输事务:9 µs

https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32s3/api-reference/peripherals/spi_master.html#id18

以上数据测试时,CONFIG_SPI_MASTER_ISR_IN_IRAM 选项处于启用状态,SPI 传输事务相关的代码放置在 IRAM 中
若关闭此选项(如为了节省 IRAM),可能影响传输事务持续时间

IRAM(指令 RAM) 是 ESP 芯片内部 SRAM 的一部分,被分配用于存放需要从 RAM 运行的指令代码
与从 flash 执行代码相比,将关键代码放入 IRAM 可以避免因缓存未命中带来的延迟,从而提升执行速度
IRAM 资源有限,未被用作 IRAM 的部分会作为 DRAM(数据 RAM)使用,用于静态数据和堆分配;通常,时序要求高或需要在中断中运行的代码会被放入 IRAM,以保证性能和实时性

缓存缺失

SPI 主机驱动程序_缓存缺失 - ESP-IDF 编程指南
默认配置只将 ISR 置于 IRAM 中,其他 SPI 相关功能,包括驱动本身和回调都可能发生缓存缺失,需等待代码从 flash 中读取
为避免缓存缺失,可参考 CONFIG_SPI_MASTER_IN_IRAM(menuconfig 中启用),将整个 SPI 驱动置入 IRAM,并将整个回调及其 callee 函数(回调函数中调用的其它函数)一起置入 IRAM

缓存缺失 是指当程序需要执行的代码或访问的数据没有在 IRAM(内部 RAM)中,而是存储在 flash 中时,由于缓存被禁用,CPU 无法直接从 flash 读取所需内容,导致必须等待 flash 操作完成后才能继续执行
这种情况下,相关功能会因为无法及时获取代码或数据而发生延迟或崩溃

SPI 驱动是基于 FreeRTOS 的 API 实现的,在使用 CONFIG_SPI_MASTER_IN_IRAM 时,不得启用 CONFIG_FREERTOS_PLACE_FUNCTIONS_INTO_FLASH

单个中断传输事务传输 n 字节的总成本为 20+8n/Fspi[MHz][µs],故传输速度为 n/(20+8n/Fspi),8 MHz 时钟速度的传输速度见下表

Fspi —— SPI 时钟频率

频率 (MHz)传输事务间隔 (µs)传输事务长度 (bytes)传输时长 (µs)传输速度 (KBps)
82512638.5
825833242.4
8251641490.2
8256489719.1
825128153836.6
传输事务长度较短时将提高传输事务间隔成本,因此应尽可能将几个短传输事务压缩成一个传输事务,以提升传输速度

注意,ISR 在 flash 操作期间默认处于禁用状态,要在 flash 操作期间继续发送传输事务,需启用 CONFIG_SPI_MASTER_ISR_IN_IRAM,并在 spi_bus_config_t::intr_flags 中设置 ESP_INTR_FLAG_IRAM
此时,flash 操作前列队的传输事务将由 ISR 并行处理
此外,每个设备的回调和它们的 callee 函数都应该在 IRAM 中,避免回调因缓存丢失而崩溃

Example

BOSCH Sensortec BMI270 Datasheet BST-BMI270-DS000 P133

从 BMI270 的 0x00 寄存器中读取 ChipID,验证 SPI 读取
随后可以用 BMI270_SensorAPI - Github 中提供的 API 对 BMI270 进行初始化和数据读取

对于内存分配,也可以用 heap_caps_malloc() 分配更适合 DMA 的

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/gpio.h"
#include "sdkconfig.h"
#include "esp_log.h"#include "driver/spi_master.h"#define SPI_TEST_HOST      SPI3_HOST
#define SPI_CLK_TEST_PIN   GPIO_NUM_28
#define SPI_MISO_TEST_PIN  GPIO_NUM_29
#define SPI_MOSI_TEST_PIN  GPIO_NUM_34
#define SPI_CS_TEST_PIN    GPIO_NUM_40
#define SPI_CLOCK_SPEED_HZ 100000#define LOG_TAG            "SPI_TEST"
#define BMI270_REG_CHIP_ID 0x00void app_main(void)
{// Configure the SPI busspi_bus_config_t buscfg = {.miso_io_num     = SPI_MISO_TEST_PIN,.mosi_io_num     = SPI_MOSI_TEST_PIN,.sclk_io_num     = SPI_CLK_TEST_PIN,.quadwp_io_num   = -1,.quadhd_io_num   = -1,.max_transfer_sz = 4096,};ESP_ERROR_CHECK(spi_bus_initialize(SPI_TEST_HOST, &buscfg, SPI_DMA_CH_AUTO));ESP_LOGI(LOG_TAG, "SPI bus initialized");// Configure the SPI devicespi_device_interface_config_t devcfg = {.command_bits   = 8,                      // Use 8-bit command.address_bits   = 0,                      // Do not use address bits.clock_speed_hz = SPI_CLOCK_SPEED_HZ,     // 100 kHz.mode           = 0,                      // SPI mode 0.spics_io_num   = SPI_CS_TEST_PIN,        // Use CS.queue_size     = 1,                      // Queue size at least 1.flags          = SPI_DEVICE_HALFDUPLEX,  // Use half duplex.pre_cb         = NULL,                   // No callback.post_cb        = NULL,.input_delay_ns = 0,};spi_device_handle_t dev_handle = NULL;ESP_ERROR_CHECK(spi_bus_add_device(SPI_TEST_HOST, &devcfg, &dev_handle));if (dev_handle != NULL){ESP_LOGI(LOG_TAG, "SPI device initialized");ESP_LOGI(LOG_TAG, "SPI clock speed: %d Hz", SPI_CLOCK_SPEED_HZ);}else {ESP_LOGE(LOG_TAG, "Failed to initialize SPI device");}spi_transaction_t t = {.cmd      = 0x80,.rxlength = 16,                    // Receive 16 bits of data, bit 0~7 will be dummy byte(0x00).flags    = SPI_TRANS_USE_RXDATA,  // Use RXDATA to receive data};ESP_LOGI(LOG_TAG, "Sending SPI command length: %d bits", devcfg.command_bits);ESP_LOGI(LOG_TAG, "Sending SPI command: 0x%04X", t.cmd);vTaskDelay(pdMS_TO_TICKS(1000));// Use polling transmitesp_err_t err = spi_device_polling_transmit(dev_handle, &t);  // Polling Transactionsif (err == ESP_OK){ESP_LOGI(LOG_TAG, "BMI270 CHIP_ID: 0x%02X", t.rx_data[0]);  // Dummy byteESP_LOGI(LOG_TAG, "BMI270 CHIP_ID: 0x%02X", t.rx_data[1]);}else{ESP_LOGE(LOG_TAG, "SPI transmit failed: %s", esp_err_to_name(err));}// Use interrupt transmiterr = spi_device_transmit(dev_handle, &t);  // Interrupt Transactionsif (err == ESP_OK){ESP_LOGI(LOG_TAG, "BMI270 CHIP_ID: 0x%02X", t.rx_data[0]);  // Dummy byteESP_LOGI(LOG_TAG, "BMI270 CHIP_ID: 0x%02X", t.rx_data[1]);}else{ESP_LOGE(LOG_TAG, "SPI transmit failed: %s", esp_err_to_name(err));}while (true){vTaskDelay(pdMS_TO_TICKS(5000));ESP_LOGI(LOG_TAG, "Free heap: %lu bytes", esp_get_free_heap_size());}
}

以下是一个适用于 Bosch Sensortec 提供的 BMI270 API 的读取函数

官方提供的 API 包含通信协议等内容,并采用 C 语言实现
用户根据所用 MCU,自行编写适配的上层协议(如 SPI 通信驱动)进行集成

注意此处配置的是全双工模式,主机在发送完地址后持续发送 dummy byte

BMI2_INTF_RETURN_TYPE spi_read_bmi270(uint8_t reg_addr, uint8_t *reg_data, uint32_t len, void *intf_ptr)
{esp_err_t ret;uint8_t cs_pin                 = BMI270_CS_PIN; // Set CS pin for each device// For multiple devices mounted on the same SPI bus// multi-device communication can be achieved by manually selecting the CS (Chip Select) pin,// rather than configuring it in softwarespi_device_handle_t spi_handle = bmi270_spi_handle;if (spi_handle == NULL){ESP_LOGE(TAG, "Invalid SPI handle for sensor %d", current_sensor_id);return BMI2_E_COM_FAIL;}uint8_t read_cmd = BMI2_SPI_RD_MASK | reg_addr;uint8_t *spi_send_data = (uint8_t *)malloc((len + 1) * sizeof(uint8_t));// Warning:Do not use malloc to allocate memory frequently, otherwise it will cause memory fragmentation.if (spi_send_data == NULL){ESP_LOGE(TAG, "Failed to allocate memory for SPI send data");return BMI2_E_COM_FAIL;}memset(spi_send_data, 0, (len + 1) * sizeof(uint8_t));  // Initialize to zerospi_send_data[0] = read_cmd;uint8_t *spi_recv_data = (uint8_t *)malloc((len + 1) * sizeof(uint8_t));if (spi_recv_data == NULL){ESP_LOGE(TAG, "Failed to allocate memory for SPI receive data");free(spi_send_data);return BMI2_E_COM_FAIL;}gpio_set_level(cs_pin, 0);// Manually pull down the CS Pinets_delay_us(1);  // Small delay for CS setup time// ESP_LOGI(TAG, "Reading accelerometer and gyroscope data from sensor %d", sensor_id);spi_transaction_t cmd_trans = {.length    = (len + 1) * 8,  // 1 byte = 8 bits.tx_buffer = spi_send_data,.rxlength  = 0,  // When set to 0, it will use the size of .length.rx_buffer = spi_recv_data};ret = spi_device_transmit(spi_handle, &cmd_trans);if (ret != ESP_OK){ESP_LOGE(TAG, "Failed to send read command: %s", esp_err_to_name(ret));gpio_set_level(cs_pin, 1);  // Release CS on errorfree(spi_send_data);free(spi_recv_data);return BMI2_E_COM_FAIL;}// ESP_LOGI(TAG, "SPI transaction completed for sensor %d", sensor_id);// Manual CS control - pull CS high to end transactionets_delay_us(1);  // Small delay for CS hold timegpio_set_level(cs_pin, 1);for (int i = 1; i < (len + 1); i++){reg_data[i - 1] = spi_recv_data[i];}free(spi_send_data);free(spi_recv_data);return BMI2_OK;
}
http://www.xdnf.cn/news/1410103.html

相关文章:

  • 编写一个名为 tfgets 的 fgets 函数版本
  • FPGA入门指南:从零开始的可编程逻辑世界探索
  • deep seek的对话记录如何导出
  • 【大数据技术实战】流式计算 Flink~生产错误实战解析
  • Springcloud-----Nacos
  • 【Spring Cloud微服务】7.拆解分布式事务与CAP理论:从理论到实践,打造数据一致性堡垒
  • Java试题-选择题(25)
  • 【Java进阶】Java与SpringBoot线程池深度优化指南
  • 【计算机组成原理·信息】2数据②
  • SpringAI应用开发面试全流程:核心技术、工程架构与业务场景深度解析
  • 第2.5节:中文大模型(文心一言、通义千问、讯飞星火)
  • 【系统分析师】高分论文:论网络系统的安全设计
  • 【51单片机】【protues仿真】基于51单片机音乐喷泉系统
  • Mysql什么时候建临时表
  • MySQL直接启动命令mysqld详解:从参数说明到故障排查
  • 策略模式:灵活应对算法动态切换
  • 探索数据结构中的 “树”:揭开层次关系的奥秘
  • 3【鸿蒙/OpenHarmony/NDK】如何在鸿蒙应用中使用NDK?
  • Makefile语句解析:头文件目录自动发现与包含标志生成
  • 【读论文】自监督消除高光谱成像中的非独立噪声
  • AI 取代部分岗位后:哪些职业更易被替代?人类该如何提升 “不可替代性”?
  • 硬件-电感学习DAY6——电感磁芯损耗全解析
  • 多人协作开发指南二
  • GPU-Driven Rendering inAssassin’s Creed Mirage
  • Android开发简介
  • LangChain框架深度解析:定位、架构、设计逻辑与优化方向
  • 计算机视觉与深度学习 | 双目立体特征提取与匹配算法综述——理论基础、OpenCV实践与MATLAB实现指南
  • leetcode_240 搜索二维矩阵 II
  • leetcode-hot-100(堆)
  • 分享一个实用的B站工具箱(支持音视频下载等功能)