ESP32-IDF 02-4 外设-SPI
Posted Ciaran-byte
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ESP32-IDF 02-4 外设-SPI相关的知识,希望对你有一定的参考价值。
SPI
1. 硬件描述
1.1 SPI资源描述
esp32一共有4个spi外设。
- SPI0和SPI1是专有SPI,其中SPI0是私有的,用于系统主flash的,不对用户开放。SPI1只能用于主机模式,引脚与SPI0共用,用于操作系统主flash。SPI0与SPI1共用同一个总线判决器。
- SPI2和SPI3是通用SPI,给用户使用,用于驱动外部设备。SPI2也叫做HSPI,SPI3也叫做VSPI。SPI2和SPI3的引脚可以任意安排。并且每个SPI可以使用三个片选线。意味着每个SPI外设可以同时驱动三组设备。
1.2 SPI类型描述
esp32的SPI支持三线SPI、四线标准SPI、Dual SPI和Quad SPI等工作模式。
1.2.1 四线标准SPI
四线标准SPI由SCK、MOSI、MISO、CS四根线组成。四线标准SPI是全双工的通讯
总线名称 | 总线功能 |
---|---|
SCK | 时钟线,决定着通讯的速度 |
MOSI | 主输出从输入。主机输入,从机输入 |
MISO | 主输入从输出。主机输入,从机输出 |
CS | 片选线。当片选线被拉低的时候,总线有效,可以开始通讯 |
1.2.2 三线SPI
三线SPI就是把MISO和MOSI总线进行了合并。同一时间只能进行单方向的读或者写。是半双工的通讯。
1.2.3 Dual SPI
Dual SPI是四线半双工的SPI通讯。Dual SPI就是让MISO和MOSI同时进行发送或者接收的工作。因此通讯速度会得到极大的提高。这个时候MISO和MOSI总线名称就变成了IO0和IO1
1.2.3 Quad SPI
Quad SPI是六线半双工的SPI通讯。除了SCK和CS总线以外,增加了IO0、IO1、IO2、IO3四条总线,这四条总线能同时进行并行的读写,比Dual SPI通讯速度相比,又得到了极大的提高。有时候IO2和IO3引脚与WP和HD引脚共用。WD是写保护,HD是状态保持。如下图是Flash芯片
2. SPI配置过程
2.1 SPI通讯过程
esp32的SPI通讯包括以下的流程
Phase | Description |
---|---|
Command | 在这个阶段,主机发送命令(0-16bit) |
Address | 在这个阶段,主机发送地址(0-64bit) |
Write | 在这个阶段,主机发送数据 |
Dummy | 在这个阶段,主机发送空字符,用于接收数据 |
Read | 在这个阶段,从机向主机发送数据 |
其中这些阶段所有的都是可以省略的。一般是主机发送Command字节,然后后面跟着Address字节。然后会发送写入数据字节。如果在读取数据之前,希望可以延迟一段时间,让这段时间时钟不作为,可以增加Dummy字节。然后进行数据的读取。
对于SPI通讯的机制来说,每次主机发送1个字节,从机都会返回一个字节。但是从机返回的字节不一定都是有意义的字节,而主机发送过去的字节同样也不一定是有意义的字节。比如下面这组数据
主机向从机发送指令字节0x9F,从机返回一个没有意义的字节0x00。然后主机没有向从机发送Address字节,主机也没有发送写入命令字节。在读取数据字节之前,也不需要进行延时,因此也没有dummy字节。从机根据指令字节0x9F,返回了三个数据字节,0xEF,0X70,0X18。(实际上就是主机向flash发送指令,读取flash ID的过程)
2.2 详细配置流程
2.2.1 配置总线初始化结构体
该步骤是配置结构体spi_device_handle_t,主要是配置总线分配到哪个引脚上。
如果不需要使用相关总线,就定义为-1
spi_bus_config_t buscfg; //总线配置结构体
buscfg.miso_io_num = GPIO_NUM_12; //gpio12->miso
buscfg.mosi_io_num = GPIO_NUM_13; //gpio13->mosi
buscfg.sclk_io_num = GPIO_NUM_14; //gpio14-> sclk
buscfg.quadhd_io_num = -1; // HD引脚不设置,这个引脚配置Quad SPI的时候才有用
buscfg.quadwp_io_num = -1; // WP引脚不设置,这个引脚配置Quad SPI的时候才有用
buscfg.max_transfer_sz = SOC_SPI_MAXIMUM_BUFFER_SIZE;
//设置传输数据的最大值。非DMA最大64bytes,DMA最大4096bytes
//buscfg.intr_flags = 0; //这个用于设置SPI通讯中相关的中断函数的中断优先级,0是默认。
//这组中断函数包括SPI通讯前中断和SPI通讯后中断两个函数。
buscfg.flags = SPICOMMON_BUSFLAG_MASTER;
//这个用于设置初始化的时候要检测哪些选项。比如这里设置的是spi初始化为主机模式是否成功。
//检测结果通过spi_bus_initialize函数的
//返回值进行返回。如果初始化为主机模式成功,就会返回esp_ok
2.2.2 总线初始化
配置好总线初始化结构体以后,就要对总线进行初始化了
通过下面的函数进行配置
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即是选择使用SPI1还是SPI2还是SPI3
- bus_config就是我们前面使用的结构体
- dma_chan是是否使用DMA进行SPI通讯的数据传输。不使用DMA传输,SPI一次能够传输的数据长度只有64byte,太少了,建议打开DMA
e = spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH1);
if (e != ESP_OK)
{
printf("bus initialize failed!\\n");
}
else
{
printf("bus initialize successed!\\n");
}
2.2.3 设备初始化结构体
完成总线初始化以后,就要进行设备的初始化了。
spi_device_interface_config_t interface_config; //设备配置结构体
//interface_config.address_bits = 32;
//1.如果设置为0,在通讯的时候就不会发送地址位。
//2.如果设置了非零值,就会在spi通讯的地址发送阶段发送指定长度的address数据。
//如果设置了非零值并且在后面数据发送结构体中没有定义addr的值,会默认发送指定长度0值
//3.我们后面发送数据会使用到spi_transaction_t结构体,这个结构体会使用spi_device_interface_config_t中定义好address、command和dummy的长度
//如果想使用非固定长度,就要使用spi_transaction_ext_t结构体了。这个结构体包括了四个部分,包含了一个spi_transaction_t和address、command、dummy的长度。
//我们要做的就是在spi_transaction_ext_t.base.flags中设置SPI_TRANS_VARIABLE_ADDR/CMD/DUMMY
//然后定义好这三部分数据的长度,然后用spi_transaction_ext_t.base的指针代替spi_transaction_t的指针即可
interface_config.command_bits = 8;
//与address_bits是一样的
//interface_config.dummy_bits = 3*8;
//这里的配置方法与address_bits是一样的。但是要着重说一下这个配置的意义,后面会再说一遍
//1.dummy_bits是用来用来补偿输入延迟。
//在read phase开始阶段之前被插入进去。在dummy_bits的时钟下,并不进行数据读取的工作
//相当于这段时间发送的clock都是虚拟的时钟,并没有功能。在输入延迟最大允许时间不够的时候,可以通过这种方法进行配置,从而
//能够使得系统工作在更高的时钟频率下。
//3.如果主机设备只进行write操作,可以在flags中设置SPI_DEVICE_NO_DUMMY,关闭dummy bits的发送。只有写操作的话,即使使用了gpio交换矩阵,时钟周期也可以工作在80MHZ
//interface_config.input_delay_ns = 0;
//时钟发出信号到miso进行输入直接会有延迟,这个参数就是配置这个允许的最大延迟时间。
//如果主机接收到从机时钟,但是超过这个时间没有收到miso发来的输入信号,就会返回通讯失败。
//这个时间即使设置为0,也能正常工作,但是最好通过手册或逻辑分析仪进行估算。能够实现更好的通讯。
//超过8M的通讯都应该认真设置这个数字
interface_config.clock_speed_hz = 1*1000 * 1000 ;
//配置通讯的时钟频率。
//这个频率受到io_mux和input_delay_ns限制。
//如果是io直连的,时钟上限是80MHZ,如果是gpio交换矩阵连接进来的,时钟上限是40MHZ。
//如果是全双工,时钟上限是26MHZ。并且还要考虑输入延时。在相同输入延时的条件下,使用gpio交换矩阵会比使用io mux最大允许的时钟频率小。可以通过
//spi_get_freq_limit()来计算能够允许的最大时钟频率是多少
//有关SPI通讯时钟极限和配置的问题,后面会详细说一下。
interface_config.mode = 0; //设置SPI通讯的相位特性和采样边沿。包括了mode0-3四种。要看从设备能够使用哪种模式
interface_config.spics_io_num = GPIO_NUM_15; //配置片选线
interface_config.duty_cycle_pos = 0;
//设置时钟的占空比,比例是 pos*1/256,默认为0,也就是50%占空比
//interface_config.cs_ena_pretrans; //在传输之前,片选线应该保持激活状态多少个时钟,只有全双工的时候才需要配置
//interface_config.cs_ena_posttrans; //在传输之后,片选线应该保持激活状态多少个时钟,只有全双工的时候才需要配置
interface_config.queue_size = 6; //传输队列的长度,表示可以在通讯的时候挂起多少个spi通讯。在中断通讯模式的时候会把当前spi通讯进程挂起到队列中
//interface_config.flags; //配置与从机有关的一些参数,比如MSB还是LSB,使不使用三线SPI
//interface_config.pre_cb;
//配置通讯前中断。比如不在这里配置cs片选线,把片选线作为自行控制的线,把片选线拉低放在通讯前中断中
//interface_config.post_cb;
//配置通讯后中断。比如不在这里配置cs片选线,把片选线作为自行控制的线,把片选线拉高放在通讯前中断中
2.2.4 设备初始化
使用函数
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即是选择使用SPI1还是SPI2还是SPI3
- dev_config 设备初始化结构体的指针
- handle是获取驱动设备的句柄,后面用于指定通过哪个设备发送数据、设备使用哪个中断函数、向中断函数内传递数据都有用途。
spi_device_handle_t spi2_handle;
e = spi_bus_add_device(SPI2_HOST, &interface_config, &spi2_handle);
if (e != ESP_OK)
{
printf("device config error\\n");
}
else
{
printf("device config success\\n");
}
2.2.5 数据包收发结构体配置
数据包发送结构体有两种,一种是用于固定address、cmd、dummy长度的,一种是用于可变长度的。
2.2.5.1 固定长度结构体
固定长度结构体使用的是spi_transaction_t,定义的固定长度来自 spi_device_interface_config_t 结构体
这个结构体简单说一下,我们是先配置cmd、address、dummy的数据,发送的时候会按照这个顺序发送。然后再根据指定的length长度,发送数据。
本例子是发送指令0x9F以后,从机会返回flash ID,长3个byte。所以数据长度定义为3*8
因为spi发送数据的时候,本来就是发送1个bit,然后接受一个bit,比如本例子数据长度是3*8 bit,也就是会从tx_buffer中取三个数据,通过MOSI发送出去,同时通过MISO接受三个数据到rx_buffer中。
关于数据的发送和接受位置,需要说一下,与tx_buffer/tx_data和rx_buffer/rx_data这两种内存。如果在flags中指定了SPI_TRANS_USE_RXDATA/SPI_TRANS_USE_TXDATA,发送和接受的位置会选择使用结构体内部的空间tx_data和rx_data,就不需要定义外部的buffer地址了。如果不定义这两个东西,默认就是使用rx_buffer和tx_buffer,需要指向外部指针。
不过,接收数据的时候,对发送的数据内容没有要求,也可以把发送区域定义为空。
uint8_t data[3]; //定义要发送的数据位置
data[0] = 0xff;
data[1] = 0xff;
data[2]= 0xff;
spi_transaction_t transaction_config; //定义数据结构体
memset(&transaction_config, 0, sizeof(transaction_config)); //为数据结构体分配内存
transaction_config.cmd = 0x9F; //因为是固定内存地址,使用的是nterface_config的配置,也就是8bit cmd,0bit address
transaction_config.length = 3 * 8; //要发送或者接收的数据的长度,不算前面的cmd/address/dummy的长度
transaction_config.tx_buffer =data; //发送没有指定内部空间,使用的是外部区域,因此要自己指定
transaction_config.rx_buffer = NULL; //接收定义了SPI_TRANS_USE_RXDATA,使用的是内部空间。
transaction_config.flags = SPI_TRANS_USE_RXDATA;
2.2.5.2 非固定长度结构体
非固定长度结构体使用的是spi_transaction_ext_t,使用的address/cmd/dummy长度是这个结构体的,这个结构体定义如下
typedef struct {
struct spi_transaction_t base; ///< Transaction data, so that pointer to spi_transaction_t can be converted into spi_transaction_ext_t
uint8_t command_bits; ///< The command length in this transaction, in bits.
uint8_t address_bits; ///< The address length in this transaction, in bits.
uint8_t dummy_bits; ///< The dummy length in this transaction, in bits.
} spi_transaction_ext_t ;
可以看到这个结构体包含了struct spi_transaction_t和command/address/dummy的长度。
在使用上,与struct spi_transaction_t基本相似,除了要定义结构体内command/address/dummy的长度以外,还要在base.flag中进行额外的配置,如本案例中SPI_TRANS_VARIABLE_CMD表示command长度是可变的。
uint8_t data[3]; //定义要发送过去的数据
data[0] = 0xff;
data[1] = 0xff;
data[2] = 0xff;
spi_transaction_ext_t ext; //定义非固定长度结构体
memset(&ext, 0, sizeof(ext)); //分配内存
ext.command_bits = 8; //command长度是可变的,本次发送command长度为8bits
ext.base.cmd = 0x9f;
ext.base.length = 3 * 8;
ext.base.tx_buffer = data;
ext.base.rx_buffer = NULL;
ext.base.flags = SPI_TRANS_VARIABLE_CMD | SPI_TRANS_VARIABLE_ADDR|SPI_TRANS_USE_RXDATA;
2.2.6 数据包的发送
发送可以以中断方式发送,也可以以轮询方式发送。
中断方式就是执行完命令后,可以去处理其他的进程,当前进行加入队列中,cpu可以去处理其他事情。这种处理方法比较花时间,但是cpu可以做其他事情。
轮询方式就是当前cpu一直处理,直到处理完为止,cpu不能去做其他事情。这种处理方法比较省时间,但是cpu比较繁忙
2.2.6.1 以中断方式发送
发送函数
esp_err_t spi_device_transmit(spi_device_handle_t handle, spi_transaction_t *trans_desc);
但是经过我的测试,如果把片选线在结构体spi_device_interface_config_t配置了的话,用这种方法数据发送不出去。如果片选线没有在这个结构体内配置,而是通过通讯前中断,用gpio_set_level函数拉低片选线,然后再通讯后中断中用同样函数拉高片选线的方式是可以发送数据的。
2.2.6.2 以轮询方式发送
发送函数
esp_err_t spi_device_polling_transmit(spi_device_handle_t handle, spi_transaction_t *trans_desc);
与上一个相反,这个函数只有内部配置好片选线的时候用才能把数据发送出去。如果用外部自定义的片选线,这个函数数据发送不出去
所以,如果用了前面的配置参数,就必须要用这里的这个函数
//使用固定长度结构体发送
e = spi_device_polling_transmit(spi2_handle, &transaction_config);
//使用非固定长度结构体发送
e = spi_device_polling_transmit(spi2_handle, &ext.base);
这套示例程序,实际上就是读取flash芯片id的程序,综合程序放到后面的实验中。
3. 高速时钟SPI通讯配置
如果我们想用spi以更高的频率通讯,就要注意以下的问题
3.1 IO MUX
SPI都有专用的IO MUX,但是也可以通过GPIO交换矩阵进行连接
GPIO矩阵让引脚具有灵活性,也带来了问题。
- 让MISO输入延迟更久了,如果需要SPI有更高的速度,请使用IO MUX
- 如果是IO MUX, SPI速度可以达到80MHZ,而GPIO 交换矩阵只能达到40MHZ
SPI专用IO MUX
Pin Name | SPI2 | SPI3 |
---|---|---|
CS0 | 15 | 5 |
SCLK | 14 | 18 |
MISO | 12 | 19 |
MOSI | 13 | 23 |
QUADWP | 2 | 22 |
QUADHD | 4 | 21 |
3.2 MISO延时允许时间
当esp32从MISO引脚输入的时候,应该考虑延时的问题。从图中看出,时钟与MISO之间存在延时,如果MISO延时过于严重,会使得通讯失败。
为了对MISO输入延时进行容错,esp32在结构体配置中有一个选项input_delay_ns,就是运行的MISO最大延时,如果我们的通讯频率大于8M,请认真填写这个数字,可以使用逻辑分析仪进行分析。
3.3 dummy 字节补偿延时
有时候input_delay_ns这个参数可能没有办法补偿MISO时钟延时,这个时候就可以引入虚拟时钟的概念。当发出读取命令的时候,后面的几个时钟作为dummy bit,在dummy bit 发送时间,esp32不进行读取,从而补偿了读的延时。
4.基于SPI读写外部flash
关于SPI的使用案例,这里以SPI读写flash芯片为例。flash使用的是W25Q128FV
4.1 flash芯片
4.1.1 flash芯片概述
- 16M存储空间
- 每个sector 4k,每个block 16个sector。 一共有 4096个sector和256个block,共16M
- 最大支持频率104MHZ(标准SPI)
- 支持标准SPI,DSPI,QSPI等协议
- 引脚包括 CLK/CS/DI/DO/WP/HOLD/VCC/GND
4.1.2 flash芯片常用指令
该表中的第一列为指令名,第二列为指令编码,第三至第 N 列的具体内容根据指令的
不同而有不同的含义。其中带括号的字节参数,方向为 FLASH 向主机传输,即命令响应,
不带括号的则为主机向 FLASH 传输。表中“A0~A23” 指 FLASH 芯片内部存储器组织的
地址; “M0~M7” 为厂商号( MANUFACTURER ID); “ID0-ID15”为 FLASH 芯片的
ID;“dummy”指该处可为任意数据;“D0~D7” 为 FLASH 内部存储矩阵的内容。
4.2 代码示例
4.2.1 硬件设计
W25Q128FV与ESP32引脚连线如下
Flash引脚 | ESP32引脚 |
---|---|
VCC | 3V3 |
GND | GND |
DO | GPIO_NUM_12 |
DI | GPIO_NUM_13 |
SCLK | GPIO_NUM_14 |
CS | GPIO_NUM_15 |
WP | 3V3 |
HD | 3V3 |
4.2.2 软件设计
4.2.2.1 相关头文件
#include "driver/spi_master.h"
#include "driver/spi_common.h"
#include "hal/gpio_types.h"
#include <cstring>
4.2.2.2 相关宏定义
//定义HSPI相关IO MUX引脚
#define BSP_HSPI SPI2_HOST
#define BSP_HSPI_MISO GPIO_NUM_12
#define BSP_HSPI_MOSI GPIO_NUM_13
#define BSP_HSPI_SCLK GPIO_NUM_14
#define BSP_HSPI_CS GPIO_NUM_15
#define BSP_HSPI_WP GPIO_NUM_2
#define BSP_HSPI_HD GPIO_NUM_4
//定义VSPI相关IO MUX引脚
#define BSP_VSPI SPI3_HOST
#define BSP_VSPI_MISO GPIO_NUM_19
#define BSP_VSPI_MOSI GPIO_NUM_23
#define BSP_VSPI_SCLK GPIO_NUM_18
#define BSP_VSPI_CS GPIO_NUM_5
#define BSP_VSPI_WP GPIO_NUM_22
#define BSP_VSPI_HD GPIO_NUM_21
//定义Flash实验所需要的引脚
#define Flash_SPI BSP_HSPI
#define Flash_SPI_MISO BSP_HSPI_MISO
#define Flash_SPI_MOSI BSP_HSPI_MOSI
#define Flash_SPI_SCLK BSP_HSPI_SCLK
#define Flash_SPI_CS BSP_HSPI_CS
#define Flash_SPI_WP -1
#define Flash_SPI_HD -1
#define Flash_SPI_DMA SPI_DMA_CH1
//定义设备参数
#define Flash_CLK_SPEED 6 * 1000 * 1000 //6M的时钟
#define Flash_Address_Bits 3*8 //地址位长度
#define Flash_Command_Bits 1*8 //命令位长度
#define Flash_Dummy_Bits 0*8 //dummy位长度
#define SPI_Flash_PageSize 256 //页写入最大值
4.2.2.3 flash指令定义
//定义命令指令
#define W25X_JedecDeviceID 0x9F //获取flashID的指令
#define W25X_WriteEnable 0x06 //写入使能
#define W25X_WriteDisable 0x04 //禁止写入
#define W25X_ReadStatusReg 0x05 //读取状态寄存器
#define W25X_SectorErase 0x20 //扇区擦除
#define W25X_BlockErase 0xD8 //块擦除
#define W25X_ChipErase 0xC7 //芯片擦除
#define W25X_PageProgram 0x02 //页写入
#define W25X_ReadData 0x03 //数据读取
#define Dummy_Byte 0xFF //空指令,用于填充发送缓冲区
#define WIP_Flag 0x01 //flash忙碌标志位
#define WIP_SET 1
4.2.2.2 初始化SPI
/**
* @breif 初始化spi,包括初始化总线结构体和设备结构体
* @param[out] handle: 获取配置SPI的操作句柄
* @retval esp_err_t
**/
esp_err_t bsp_spi_flash_init(spi_device_handle_t* handle)
{
//00 定义错误标志
esp_err_t e;
//01 配置总线初始化结构体
static spi_bus_config_t bus_cfg; //总线配置结构体
bus_cfg.miso_io_num = Flash_SPI_MISO; //miso
bus_cfg.mosi_io_num = Flash_SPI_MOSI; //mosi
bus_cfg.sclk_io_num = Flash_SPI_SCLK; //sclk
bus_cfg.quadhd_io_num = Flash_SPI_HD; // HD
bus_cfg.quadwp_io_num = Flash_SPI_WP; // WP
bus_cfg.max_transfer_sz = 4092; //非DMA最大64bytes,DMA最大4092bytes
//bus_cfg.intr_flags = 0; //这个用于设置中断优先级的,0是默认
bus_cfg.flags = SPICOMMON_BUSFLAG_MASTER;
//这个用于设置初始化的时候要检测哪些选项。比如这里设置的是spi初始化为主机模式是否成功。检测结果通过spi_bus_initialize函数的
//返回值进行返回。如果初始化为主机模式成功,就会返回esp_ok
//02 初始化总线配置结构体
e = spi_bus_initialize(Flash_SPI, &bus_cfg, Flash_SPI_DMA);
if (e != ESP_OK)
{
printf("bus initialize failed!\\n");
return e;
}
//03 配置设备结构体
static spi_device_interface_config_t interface_cfg; //设备配置结构体
interface_cfg.address_bits = Flash_Address_Bits; //配置地址位长度
//(1)如果设置为0,在通讯的时候就不会发送地址位。
//(2)如果设置了非零值,就会在spi通讯的地址发送阶段发送指定长度的address数据。
//如果设置了非零值并且在后面数据发送结构体中没有定义addr的值,会默认发送指定长度0值
//(3)我们后面发送数据会使用到spi_transaction_t结构体,这个结构体会使用spi_device_interface_config_t中定义好address、command和dummy的长度
//如果想使用非固定长度,就要使用spi_transaction_ext_t结构体了。这个结构体包括了四个部分,包含了一个spi_transaction_t和address、command、dummy的长度。
//我们要做的就是在spi_transaction_ext_t.base.flags中设置SPI_TRANS_VARIABLE_ADDR/CMD/DUMMY
//然后定义好这三部分数据的长度,然后用spi_transaction_ext_t.base的指针代替spi_transaction_t的指针即可
interface_cfg.command_bits = Flash_Command_Bits; //配置命令位长度
//与address_bits是一样的
interface_cfg.dummy_bits = Flash_Dummy_Bits; //配置dummy长度
//这里的配置方法与address_bits是一样的。但是要着重说一下这个配置的意义,后面会再说一遍
//(1)dummy_bits是用来用来补偿输入延迟。
//(2)在read phase开始阶段之前被插入进去。在dummy_bits的时钟下,并不进行数据读取的工作
//相当于这段时间发送的clock都是虚拟的时钟,并没有功能。在输入延迟最大允许时间不够的时候,可以通过这种方法进行配置,从而
//能够使得系统工作在更高的时钟频率下。
//(3)如果主机设备只进行write操作,可以在flags中设置SPI_DEVICE_NO_DUMMY,关闭dummy bits的发送。只有写操作的话,即使使用了gpio交换矩阵,时钟周期也可以工作在80MHZ
//interface_cfg.input_delay_ns = 0; //配置输入延时的允许范围
//时钟发出信号到miso进行输入直接会有延迟,这个参数就是配置这个允许的最大延迟时间。
//如果主机接收到从机时钟,但是超过这个时间没有收到miso发来的输入信号,就会返回通讯失败。
//这个时间即使设置为0,也能正常工作,但是最好通过手册或逻辑分析仪进行估算。能够实现更好的通讯。
//超过8M的通讯都应该认真设置这个数字
interface_cfg.clock_speed_hz = Flash_CLK_SPEED; //配置时钟频率
//配置通讯的时钟频率。
//这个频率受到io_mux和input_delay_ns限制。
//如果是io直连的,时钟上限是80MHZ,如果是gpio交换矩阵连接进来的,时钟上限是40MHZ。
//如果是全双工,时钟上限是26MHZ。并且还要考虑输入延时。在相同输入延时的条件下,使用gpio交换矩阵会比使用io mux最大允许的时钟频率小。可以通过
//spi_get_freq_limit()来计算能够允许的最大时钟频率是多少
//有关SPI通讯时钟极限和配置的问题,后面会详细说一下。
interface_cfg.mode = 0; //设置SPI通讯的相位特性和采样边沿。包括了mode0-3四种。要看从设备能够使用哪种模式
interface_cfg.spics_io_num = Flash_SPI_CS; //配置片选线
interface_cfg.duty_cycle_pos = 0; //配置占空比
//设置时钟的占空比,比例是 pos*1/256,默认为0,也就是50%占空比
//interface_cfg.cs_ena_pretrans; //在传输之前,片选线应该保持激活状态多少个时钟,只有全双工的时候才需要配置
//interface_cfg.cs_ena_posttrans; //在传输之后,片选线应该保持激活状态多少个时钟,只有全双工的时候才需要配置
interface_cfg.queue_size = 6; //传输队列的长度,表示可以在通讯的时候挂起多少个spi通讯。在中断通讯模式的时候会把当前spi通讯进程挂起到队列中
//interface_cfg.flags; //配置与从机有关的一些参数,比如MSB还是LSB,使不使用三线SPI
//interface_cfg.pre_cb;
//配置通讯前中断。比如不在这里配置cs片选线,把片选线作为自行控制的线,把片选线拉低放在通讯前中断中
//interface_cfg.post_cb;
//配置通讯后中断。比如不在这里配置cs片选线,把片选线作为自行控制的线,把片选线拉高放在通讯前中断中
//04 设备初始化
e = spi_bus_add_device(Flash_SPI, &interface_cfg, handle);
if (以上是关于ESP32-IDF 02-4 外设-SPI的主要内容,如果未能解决你的问题,请参考以下文章