esp32一共有4个spi外设。
esp32的SPI支持三线SPI、四线标准SPI、Dual SPI和Quad SPI等工作模式。
四线标准SPI由SCK、MOSI、MISO、CS四根线组成。四线标准SPI是全双工的通讯
总线名称 | 总线功能 |
---|---|
SCK | 时钟线,决定着通讯的速度 |
MOSI | 主输出从输入。主机输入,从机输入 |
MISO | 主输入从输出。主机输入,从机输出 |
CS | 片选线。当片选线被拉低的时候,总线有效,可以开始通讯 |
三线SPI就是把MISO和MOSI总线进行了合并。同一时间只能进行单方向的读或者写。是半双工的通讯。
Dual SPI是四线半双工的SPI通讯。Dual SPI就是让MISO和MOSI同时进行发送或者接收的工作。因此通讯速度会得到极大的提高。这个时候MISO和MOSI总线名称就变成了IO0和IO1
Quad SPI是六线半双工的SPI通讯。除了SCK和CS总线以外,增加了IO0、IO1、IO2、IO3四条总线,这四条总线能同时进行并行的读写,比Dual SPI通讯速度相比,又得到了极大的提高。有时候IO2和IO3引脚与WP和HD引脚共用。WD是写保护,HD是状态保持。如下图是Flash芯片
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的过程)
该步骤是配置结构体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
配置好总线初始化结构体以后,就要对总线进行初始化了
通过下面的函数进行配置
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);
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");
}
完成总线初始化以后,就要进行设备的初始化了。
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片选线,把片选线作为自行控制的线,把片选线拉高放在通讯前中断中
使用函数
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);
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");
}
数据包发送结构体有两种,一种是用于固定address、cmd、dummy长度的,一种是用于可变长度的。
固定长度结构体使用的是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;
非固定长度结构体使用的是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;
发送可以以中断方式发送,也可以以轮询方式发送。
中断方式就是执行完命令后,可以去处理其他的进程,当前进行加入队列中,cpu可以去处理其他事情。这种处理方法比较花时间,但是cpu可以做其他事情。
轮询方式就是当前cpu一直处理,直到处理完为止,cpu不能去做其他事情。这种处理方法比较省时间,但是cpu比较繁忙
发送函数
esp_err_t spi_device_transmit(spi_device_handle_t handle, spi_transaction_t *trans_desc);
但是经过我的测试,如果把片选线在结构体spi_device_interface_config_t配置了的话,用这种方法数据发送不出去。如果片选线没有在这个结构体内配置,而是通过通讯前中断,用gpio_set_level函数拉低片选线,然后再通讯后中断中用同样函数拉高片选线的方式是可以发送数据的。
发送函数
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的程序,综合程序放到后面的实验中。
如果我们想用spi以更高的频率通讯,就要注意以下的问题
SPI都有专用的IO MUX,但是也可以通过GPIO交换矩阵进行连接
GPIO矩阵让引脚具有灵活性,也带来了问题。
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 |
当esp32从MISO引脚输入的时候,应该考虑延时的问题。从图中看出,时钟与MISO之间存在延时,如果MISO延时过于严重,会使得通讯失败。
为了对MISO输入延时进行容错,esp32在结构体配置中有一个选项input_delay_ns,就是运行的MISO最大延时,如果我们的通讯频率大于8M,请认真填写这个数字,可以使用逻辑分析仪进行分析。
有时候input_delay_ns这个参数可能没有办法补偿MISO时钟延时,这个时候就可以引入虚拟时钟的概念。当发出读取命令的时候,后面的几个时钟作为dummy bit,在dummy bit 发送时间,esp32不进行读取,从而补偿了读的延时。
关于SPI的使用案例,这里以SPI读写flash芯片为例。flash使用的是W25Q128FV
该表中的第一列为指令名,第二列为指令编码,第三至第 N 列的具体内容根据指令的
不同而有不同的含义。其中带括号的字节参数,方向为 FLASH 向主机传输,即命令响应,
不带括号的则为主机向 FLASH 传输。表中“A0~A23” 指 FLASH 芯片内部存储器组织的
地址; “M0~M7” 为厂商号( MANUFACTURER ID); “ID0-ID15”为 FLASH 芯片的
ID;“dummy”指该处可为任意数据;“D0~D7” 为 FLASH 内部存储矩阵的内容。
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 |
#include "driver/spi_master.h"
#include "driver/spi_common.h"
#include "hal/gpio_types.h"
#include
//定义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 //页写入最大值
//定义命令指令
#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
/**
* @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 (e != ESP_OK)
{
printf("device config error\n");
return e;
}
return ESP_OK;
}
flash芯片会有字节的ID,包括1个byte的厂商号,和两个字节的Flash型号。发送0x9F指令后,flash会返回三个字节的设备ID。
/**
* @breif 获取falshID
* @param[in] handle: 提供SPI的操作句柄
* @retval flashID
* @code
spi_device_handle_t spi2_handle;
esp_err_t e;
void setup() {
bsp_spi_flash_init(&spi2_handle);
while (1)
{
uint32_t id=bsp_spi_flash_ReadID(spi2_handle);
printf("0x%X\n", id);
delay(1000);
}
}
* @endcode
**/
uint32_t bsp_spi_flash_ReadID(spi_device_handle_t handle)
{
//00 定义错误标志
esp_err_t e;
//01 接收数据的时候发送的空指令
uint8_t data[3];
data[0] = Dummy_Byte;
data[1] = Dummy_Byte;
data[2] = Dummy_Byte;
//02 定义用于返回的数据
uint32_t Temp;
//03 定义数据发送接收结构体
spi_transaction_ext_t ext; //因为读取设备ID的指令结构与前面定义的默认的不一样,所以需要更改位长
memset(&ext, 0, sizeof(ext)); //初始化结构体
ext.command_bits = 8; //指令位长度为8
ext.address_bits = 0; //地址位长度为0
ext.base.cmd = W25X_JedecDeviceID; //设备ID
ext.base.length = 3 * 8; //要发送数据的长度
ext.base.tx_buffer = data; //要发送数据的内容
ext.base.rx_buffer = NULL; //接收数据buffer使用结构体内部带的
ext.base.flags = SPI_TRANS_VARIABLE_CMD | SPI_TRANS_VARIABLE_ADDR | SPI_TRANS_USE_RXDATA;
//04 数据收发
e=spi_device_polling_transmit(handle, &ext.base);
if (e != ESP_OK)
{
printf("get ID error!\n");
return 0;
}
//05 返回获取的数据ID
uint8_t temp0 = ext.base.rx_data[0];
uint8_t temp1 = ext.base.rx_data[1];
uint8_t temp2 = ext.base.rx_data[2];
Temp = (temp0 << 16) | (temp1 << 8) | temp2;
return Temp;
}
flash在每次执行页写入、扇区擦除、块擦除、芯片擦除之前,都要执行一次写使能。写使能的指令是0x06
/**
* @breif flash写使能。在执行页写入和擦除命令之前,都必须执行一次页写入
* @param[in] handle: 提供SPI的操作句柄
* @retval 无
**/
void bsp_spi_flash_WriteEnable(spi_device_handle_t handle)
{
esp_err_t e; //错误标志位
// 定义数据发送接收结构体
spi_transaction_ext_t ext; //写使能的长度与默认的不同,需要修改
memset(&ext, 0, sizeof(ext)); //初始化结构体
ext.command_bits = 8; //指令位长度为8
ext.address_bits = 0; //地址位长度为0
ext.base.cmd = W25X_WriteEnable; //写使能
ext.base.length = 0; //要发送数据的长度,这里不需要发送数据
ext.base.flags = SPI_TRANS_VARIABLE_CMD | SPI_TRANS_VARIABLE_ADDR;
//发送指令
e = spi_device_polling_transmit(handle, &ext.base);
if (e != ESP_OK)
{
printf("write enable failed!\n");
}
}
由于flash并不能每次总线通讯结束就完成写入、擦除等操作,因此flash内部定义了状态寄存器,我们需要不断读取这个寄存器,知道操作完成。
状态寄存器的第0位能够判断出当前falsh是否处于忙碌状态,向flash发送0x05就会返回这个寄存器的数组。当第0位为1的时候,表示falsh处于忙碌状态
/**
* @breif 等待flash完成当前操作
* @param[in] handle: 提供SPI的操作句柄
* @retval 无
**/
void bsp_spi_flash_WaitForWriteEnd(spi_device_handle_t handle)
{
// 定义数据发送接收结构体
spi_transaction_ext_t ext; //写使能的长度与默认的不同,需要修改
memset(&ext, 0, sizeof(ext)); //初始化结构体
ext.command_bits = 8; //指令位长度为8
ext.address_bits = 0; //地址位长度为0
ext.base.cmd = W25X_ReadStatusReg; //读取状态寄存器
ext.base.length = 1 * 8; //要发送数据的长度,这里不需要发送数据
ext.base.rx_buffer = NULL; //不使用外部数据
ext.base.tx_buffer = NULL; //不使用外部数据
ext.base.tx_data[0] = Dummy_Byte; //发送数据
ext.base.flags = SPI_TRANS_VARIABLE_CMD | SPI_TRANS_VARIABLE_ADDR | SPI_TRANS_USE_RXDATA | SPI_TRANS_USE_TXDATA;
do
{
//发送指令
spi_device_polling_transmit(handle, &ext.base);
}
while (ext.base.rx_data[0] & WIP_Flag == WIP_SET);
}
flash写入只能把数据位0变成1,因此,如果是已经写入了的空间,还想再次写入,就比如先进行擦除操作。flash数据擦除类型包括扇区擦除、块擦除和芯片擦除三个级别。执行擦除操作必须进行4k对齐。擦除指令是0x20,然后后面跟着三个字节的地址即可。注意flash擦除之前要先执行写使能。
以扇区擦除为例
/**
* @breif 扇区擦除
* @param[in] handle: 提供SPI的操作句柄
* @param[in] SectorAddr: 要擦除的起始扇区地址
* @retval 无
**/
void bsp_spi_flash_SectorErase(spi_device_handle_t handle,uint32_t SectorAddr)
{
bsp_spi_flash_WriteEnable(handle);
bsp_spi_flash_WaitForWriteEnd(handle);
// 定义数据发送接收结构体
spi_transaction_t t; //配置位与默认一致,不需要修改
memset(&t, 0, sizeof(t)); //初始化结构体
t.cmd = W25X_SectorErase; //擦除指令
t.addr = SectorAddr; //擦除地址
t.length = 0; //不需要额外数据了
//发送指令
spi_device_polling_transmit(handle, &t);
//等待擦除完毕
bsp_spi_flash_WaitForWriteEnd(handle);
}
flash每个页包含256个字节。每次写入的时候不能超过256个字节,否则会覆盖前面的数据。而且,写入的初始字节应该进行对齐。写入的指令是0x02,后面跟着要写入的地址即可。
注意flash页写入之前要先执行写使能。如果这段地址被写入过了,还要先执行擦除。
/**
* @breif 页写入
* @param[in] handle: 提供SPI的操作句柄
* @param[in] pBuffer:要写入的数据地址
* @param[in] WriteAddr:要写入的地址
* @param[in] NumByteToWrite: 要写入的长度
* @retval 无
**/
void bsp_spi_flash_PageWrite(spi_device_handle_t handle, uint8_t* pBuffer,uint32_t WriteAddr,uint16_t NumByteToWrite)
{
bsp_spi_flash_WriteEnable(handle);
// 定义数据发送接收结构体
spi_transaction_t t; //配置位与默认一致,不需要修改
memset(&t, 0, sizeof(t)); //初始化结构体
t.cmd = W25X_PageProgram; //页写入
t.addr = WriteAddr; //擦除地址
t.length = 8*NumByteToWrite; //写入长度
t.tx_buffer = pBuffer; //写入的数据
t.rx_buffer = NULL; //不需要读取数据
if (NumByteToWrite > SPI_Flash_PageSize)
{
printf("length is too long!\n");
return ;
}
//发送指令
spi_device_polling_transmit(handle, &t);
//等待擦除完毕
bsp_spi_flash_WaitForWriteEnd(handle);
}
/**
* @breif 不定量数据写入
* @param[in] handle: 提供SPI的操作句柄
* @param[in] pBuffer:要写入的数据地址
* @param[in] WriteAddr:要写入的地址
* @param[in] NumByteToWrite: 要写入的长度
* @retval 无
**/
void bsp_spi_flash_BufferWrite(spi_device_handle_t handle, uint8_t* pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite)
{
uint8_t NumOfPage = 0, NumOfSingle = 0, Addr = 0, count = 0, temp = 0;
//进行取余运算,查看是否进行了页对齐
Addr = WriteAddr % SPI_Flash_PageSize;
//差count个数据值可以进行页对齐
count = SPI_Flash_PageSize - Addr;
//计算要写多少个完整的页
NumOfPage = NumByteToWrite / SPI_Flash_PageSize;
//计算剩余多少字节不满1页
NumOfSingle = NumByteToWrite % SPI_Flash_PageSize;
//如果Addr=0,也就是进行了页对齐
if (Addr == 0)
{
//如果写不满1页
if (NumOfPage == 0)
{
bsp_spi_flash_PageWrite(handle, pBuffer, WriteAddr, NumByteToWrite);
}
else
{
//如果超过1页,先把满的写了
while (NumOfPage--)
{
bsp_spi_flash_PageWrite(handle, pBuffer, WriteAddr, SPI_Flash_PageSize);
WriteAddr += SPI_Flash_PageSize;
pBuffer += SPI_Flash_PageSize;
}
//不满的1页再写
bsp_spi_flash_PageWrite(handle, pBuffer, WriteAddr, NumOfSingle);
}
}
//如果没有进行页对齐
else
{
if (NumOfPage == 0)
{
//如果当前页剩下的count个位置比NumOfSingle小,1页写不完
if (NumOfSingle > count)
{
//先把这页剩下的写了
temp = NumOfSingle - count;
bsp_spi_flash_PageWrite(handle, pBuffer, WriteAddr, count);
WriteAddr += count;
pBuffer += count;
//再把多了的写了
bsp_spi_flash_PageWrite(handle, pBuffer, WriteAddr, temp);
}
else
{
//如果剩下的空间足够大,就直接写
bsp_spi_flash_PageWrite(handle, pBuffer, WriteAddr, NumByteToWrite);
}
}
//如果不止1页
else
{
//先把对不齐的字节写了
NumByteToWrite -= count;
NumOfPage = NumByteToWrite / SPI_Flash_PageSize;
NumOfSingle = NumByteToWrite % SPI_Flash_PageSize;
bsp_spi_flash_PageWrite(handle, pBuffer, WriteAddr,count);
//重复地址对齐的情况
WriteAddr += count;
pBuffer += count;
while (NumOfPage--)
{
bsp_spi_flash_PageWrite(handle, pBuffer, WriteAddr, SPI_Flash_PageSize);
pBuffer += SPI_Flash_PageSize;
WriteAddr += SPI_Flash_PageSize;
}
if (NumOfSingle != 0)
{
bsp_spi_flash_PageWrite(handle, pBuffer, WriteAddr, NumOfSingle);
}
}
}
}
flash数据读取的时候,数据量是任意的,不必小于页大小。读取指令是0x03
/**
* @breif 数据读取
* @param[in] handle: 提供SPI的操作句柄
* @param[out] pBuffer:要读取的数据buffer地址
* @param[in] WriteAddr:要写入的地址
* @param[in] NumByteToWrite: 要写入的长度
* @retval 无
**/
void bsp_spi_flash_BufferRead(spi_device_handle_t handle, uint8_t* pBuffer, uint32_t WriteAddr, uint16_t NumByteToWrite)
{
bsp_spi_flash_WriteEnable(handle);
// 定义数据发送接收结构体
spi_transaction_t t; //配置位与默认一致,不需要修改
memset(&t, 0, sizeof(t)); //初始化结构体
t.cmd = W25X_ReadData; //读取数据
t.addr = WriteAddr; //擦除地址
t.length = 8 * NumByteToWrite; //读取长度
t.tx_buffer = NULL; //不需要写入数据
t.rx_buffer = pBuffer; //读取数据
//发送指令
spi_device_polling_transmit(handle, &t);
//等待擦除完毕
bsp_spi_flash_WaitForWriteEnd(handle);
}
#include "bsp_spi_flash.h"
spi_device_handle_t spi2_handle;
esp_err_t e;
void setup() {
uint8_t pBuffer[600] = { 0x10,0x20,0x30,0x40,0x50,0x90,0x11,0x00,0x00,0x00 };
delay(1000);
bsp_spi_flash_init(&spi2_handle);
uint32_t id=bsp_spi_flash_ReadID(spi2_handle);
bsp_spi_flash_SectorErase(spi2_handle, 0);
bsp_spi_flash_BufferWrite(spi2_handle, pBuffer, 1, 600);
uint8_t rBuffer[600];
bsp_spi_flash_BufferRead(spi2_handle, rBuffer, 1, 600);
for (int i = 0; i < 600; i++)
{
if (i % 10 == 0)
{
printf("\n");
}
printf("0x%x ", rBuffer[i]);
}
}
void loop() {
}
【1】SPI Master Drive
【2】ESP32 单片机学习笔记 - 02 - 软件IIC&硬件SPI
【3】ESP32设备SPI主设备驱动