一、SPI简介
SPI(Serial Peripheral Interface) 协议是由摩托罗拉公司提出的通讯协议,即串行外围设备接口,是一种高速全双工的通信总线。它被广泛地使用在 ADC、LCD 等设备与 MCU 间,要求通讯速率较高的场合。
芯片的管脚上只占用四根线。
MISO: 主器件数据输出,从器件数据输入。
MOSI:主器件数据输入,从器件数据输出。
SCK: 时钟信号,由主设备控制发出。
NSS(CS): 从设备选择信号,由主设备控制。当NSS为低电平则选中从器件。
1.1 ESP32中SPI
ESP32集成了
4个SPI
外设。
SPI0
和SPI1
在内部用于访问ESP32所连接的闪存。两个控制器共享相同的SPI总线信号,并且有一个仲裁器来确定哪个可以访问该总线。
在SPI1总线上使用SPI Master驱动程序时有很多限制,请参阅《在SPI1总线 上使用SPI Master驱动程序的注意事项》。SPI2
和SPI3
是通用SPI控制器,有时分别称为HSPI和VSPI。它们向用户开放。SPI2和SPI3具有独立的总线信号,分别具有相同的名称。每条总线具有3条CS线,最多能控制6个SPI从设备。
ESP32内部的SPI控制器可设置为主模式(Master),基本特点如下
- 适应多线程环境
- 可配置DMA辅助传输
- 在同一信号线上自动分配时间处理来自不同设备的的多路数据,请参见SPI总线锁定。
注意: SPI主驱动程序的概念是将多个设备连接到一条总线(共享一个ESP32 SPI外设)。只要仅通过一个任务访问每个设备,驱动程序就是线程安全的。但是,如果多个任务尝试访问同一SPI设备,则驱动程序不是线程安全的。在这种情况下,建议执行以下任一操作:
* 重构您的应用程序,以便每个SPI外围设备一次只能由一个任务访问。
* 使用围绕共享设备添加互斥锁xSemaphoreCreateMutex
。
1.2 SPI传输
SPI总线通信包含五个阶段,可以在下表中找到。这些阶段中的任何一个都可以跳过。
阶段 | 描述 |
---|---|
命令 | 在此阶段,主机将命令(0-16位)写入总线。 |
地址 | 在此阶段,主机通过总线发送地址(0-64位)。 |
写 | 主机将数据发送到设备。该数据遵循可选的命令和地址阶段,并且在电气级别上与它们是无法区分的。 |
空 | 此阶段是可配置的,用于满足时序要求。 |
读 | 设备将数据发送到其主机。 |
命令和地址段是可选的,因为并非每个SPI设备都需要命令和/或地址。这反映在
spi_device_interface_config_t
中:如果command_bits和/或address_bits设置为零
,则不会发送命令或地址段。
读取和写入段也可以是可选的,因为并非每个通信都需要写入和读取数据。如果rx_buffer为NULLSPI_TRANS_USE_RXDATA且未设置,则跳过读取阶段。如果tx_buffer为NULLSPI_TRANS_USE_TXDATA且未设置,则跳过写阶段。
配置GPIO的SPI复用引脚和SPI控制器spi_bus_config_t
//spi_bus_config_t用于配置GPIO的SPI复用引脚和SPI控制器
//注意:如果不使用QSPI可以直接不初始化quadwp_io_num和quadhd_io_num,总线会自动关闭未被配置的信号线
//如果不使用某线应将其设置为-1
struct spi_bus_config_t={
.miso_io_num,//MISO信号线,可复用为QSPI的D0
.mosi_io_num,//MOSI信号线,可复用为QSPI的D1
.sclk_io_num,//SCLK信号线
.quadwp_io_num,//WP信号线,专用于QSPI的D2
.quadhd_io_num,//HD信号线,专用于QSPI的D3
.max_transfer_sz,//最大传输数据大小,单位字节,默认为4094
.intr_flags,//中断指示位
};
配置SPI协议情况spi_transaction_t
//spi_transaction_t用于配置SPI的数据格式
//注意:这个结构体只定义了一种SPI传输格式,如果需要多种SPI传输则需要定义多个结构体并进行实例化
struct spi_transaction_t={
.cmd,//指令数据,其长度在spi_device_interface_config_t中的command_bits设置
.addr,//地址数据,其长度在spi_device_interface_config_t中的address_bits设置
.length,//数据总长度,单位:比特
.rxlength,//接收到的数据总长度,应小于length,如果设置为0则默认设置为length
.flags,//SPI传输属性设置
.user,//用户定义变量,可以用来存储传输ID等注释信息
.tx_buffer,//发送数据缓存区指针
.tx_data,//发送数据
.rx_buffer,//接收数据缓存区指针,如果启用DMA则需要至少4个字节
.rx_data//如果设置了SPI_TRANS_USE_RXDATA,数据会被这个变量直接接收
};
配置SPI的数据格式spi_device_interface_config_t
//spi_device_interface_config_t用于配置SPI协议情况
//需要根据从设备的数据手册进行设置
struct spi_device_interface_config_t={
.command_bits,//默认控制位长度,设置为0-16
.address_bits,//默认地址位长度,设置为0-64
.dummy_bits,//在地址和数据位段之间插入的dummy位长度,用于匹配时序,一般可以保持默认
.clock_speed_hz,//时钟频率,设置的是80MHz的分频系数,单位为Hz
.mode,//SPI模式,设置为0-3
.duty_cycle_pos,//
.cs_ena_pretrans,//传输前CS信号的建立时间,只在半双工模式下有用
.cs_ena_posttrans,//传输时CS信号的保持时间
.input_delay_ns,//从机的最大合法数据传输时间
.spics_io_num,//设置GPIO复用为CS引脚
.queue_size,//传输队列大小,决定了等待传输数据的数量
.flags,//SPI设备属性设置
.pre_cb,//传输开始时的回调函数
.post_cb,//传输结束时的回调函数
};
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
的length
和rxlength
成员确定。
1.2.1 中断传输
中断传输期间,CPU可以执行其他任务。传输结束时,SPI外设触发中断,CPU调用任务处理函数进行处理
注意:一个任务可以排列多个传输序列,驱动程序会自动在中断服务程序(ISR)中对传输结果进行处理;但是中断传输会导致很多中断,如果设置中断任务太多还会影响日常任务运行降低实时性能。
1.2.2 轮询传输
轮询传输会轮询SPI主机的状态位直到传输完成。
轮询传输可以节约ISR队列挂起等待和线程(任务)上下文切换所需时间,但是会导致CPU占用。
使用spi_device_polling_end()
传输完成后,至少需要1us时间解除对其他任务的阻塞;强烈建议使用spi_device_acquire_bus()
并spi_device_release_bus()
进行轮询传输,避免开销。
1.3 GPIO矩阵和IO_MUX
ESP32的大多数外设信号都直接连接到其专用的IO_MUX引脚。但是,也可以使用GPIO矩阵将信号转换到任何其他可用的引脚。如果至少一个信号通过GPIO矩阵转换,则所有信号都将通过GPIO矩阵转换。
GPIO矩阵引入了转换灵活性,但也带来了以下缺点:
- 增加了MISO信号的输入延迟,这更可能违反MISO设置时间。如果SPI需要高速运行,请使用专用的IO_MUX引脚。
- 如果使用IO_MUX引脚,则允许信号的时钟频率最多为40 MHz,而时钟频率最高为80 MHz。
SPI总线的IO_MUX引脚如下所示
引脚对应的GPIO | SPI2 | SPI3 |
---|---|---|
CS0 * | 15 | 5 |
SCLK | 14 | 18 |
MISO | 12 | 19 |
MOSI | 13 | 23 |
QUADWP | 2 | 22 |
QUADHD | 4 | 21 |
- 仅连接到总线的第一个设备可以使用CS0引脚。
二、API说明
以下 SPI 主机接口位于 driver/include/driver/spi_master.h。
2.1 spi_bus_initialize
2.2 spi_bus_add_device
2.3 spi_device_polling_transmit
2.4 spi_device_acquire_bus
2.5 spi_device_release_bus
2.6 spi_bus_remove_device
三、编程流程
3.1 设置通信参数
通过调用函数初始化SPI总线spi_bus_initialize()
。确保在struct中设置正确的I / O引脚spi_bus_config_t
。将不需要的信号设置为-1
。
3.2 驱动程序安装
通过调用函数在驱动程序中注册连接到总线的设备spi_bus_add_device()
。确保使用参数配置设备可能需要的任何时序要求dev_config
。现在,您应该已经获得了设备的句柄,该句柄将在向它发送事务时使用。
3.3 运行SPI通信
要与设备进行交互,请使用spi_transaction_t
所需的任何传输参数填充一个或多个结构。然后使用轮询事务或中断事务发送结构:
-
中断
通过调用函数将所有事务排队spi_device_queue_trans()
,然后在以后使用函数查询结果spi_device_get_trans_result()
,或者通过将所有请求馈入来同步处理所有请求spi_device_transmit()
。 -
轮询
调用该函数spi_device_polling_transmit()
以发送轮询事务。或者,如果要在两者之间插入内容,请使用spi_device_polling_start()
和发送事务spi_device_polling_end()
。
四、SPI主机代码
根据 esp-idf\examples\peripherals\spi_master\spi_eeprom 中的例程
注意:在SPI接收中,如果定义了t.flags = SPI_TRANS_USE_RXDATA,则使用t.rx_data接收数据,否则使用t.rx_buffer=data来接收数据
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/spi_master.h"
#include "driver/gpio.h"
#include "sdkconfig.h"
#include "esp_log.h"
#define DMA_CHAN 2
#define PIN_NUM_MISO 12
#define PIN_NUM_MOSI 13
#define PIN_NUM_CLK 14
#define PIN_NUM_CS 15
static const char TAG[] = "main";
esp_err_t spi_write(spi_device_handle_t spi, uint8_t *data, uint8_t len)
{
esp_err_t ret;
spi_transaction_t t;
if (len==0) return; //no need to send anything
memset(&t, 0, sizeof(t)); //Zero out the transaction
gpio_set_level(PIN_NUM_CS, 0);
t.length=len*8; //Len is in bytes, transaction length is in bits.
t.tx_buffer=data; //Data
t.user=(void*)1; //D/C needs to be set to 1
ret=spi_device_polling_transmit(spi, &t); //Transmit!
assert(ret==ESP_OK); //Should have had no issues.
gpio_set_level(PIN_NUM_CS, 1);
return ret;
}
esp_err_t spi_read(spi_device_handle_t spi, uint8_t *data)
{
spi_transaction_t t;
gpio_set_level(PIN_NUM_CS, 0);
memset(&t, 0, sizeof(t));
t.length=8;
t.flags = SPI_TRANS_USE_RXDATA;
t.user = (void*)1;
esp_err_t ret = spi_device_polling_transmit(spi, &t);
assert( ret == ESP_OK );
*data = t.rx_data[0];
gpio_set_level(PIN_NUM_CS, 1);
return ret;
}
void app_main(void)
{
esp_err_t ret;
spi_device_handle_t spi;
ESP_LOGI(TAG, "Initializing bus SPI%d...", SPI2_HOST+1);
spi_bus_config_t buscfg={
.miso_io_num = PIN_NUM_MISO, // MISO信号线
.mosi_io_num = PIN_NUM_MOSI, // MOSI信号线
.sclk_io_num = PIN_NUM_CLK, // SCLK信号线
.quadwp_io_num = -1, // WP信号线,专用于QSPI的D2
.quadhd_io_num = -1, // HD信号线,专用于QSPI的D3
.max_transfer_sz = 64*8, // 最大传输数据大小
};
spi_device_interface_config_t devcfg={
.clock_speed_hz = SPI_MASTER_FREQ_10M, // Clock out at 10 MHz,
.mode = 0, // SPI mode 0
/*
* The timing requirements to read the busy signal from the EEPROM cannot be easily emulated
* by SPI transactions. We need to control CS pin by SW to check the busy signal manually.
*/
.spics_io_num = -1,
.queue_size = 7, // 传输队列大小,决定了等待传输数据的数量
};
//Initialize the SPI bus
ret = spi_bus_initialize(SPI2_HOST, &buscfg, DMA_CHAN);
ESP_ERROR_CHECK(ret);
ret = spi_bus_add_device(SPI2_HOST, &devcfg, &spi);
ESP_ERROR_CHECK(ret);
gpio_pad_select_gpio(PIN_NUM_CS); // 选择一个GPIO
gpio_set_direction(PIN_NUM_CS, GPIO_MODE_OUTPUT);// 把这个GPIO作为输出
const char test_str[] = "Hello!";
uint8_t test_buf[4] = "";
while (1) {
spi_write(spi, test_str, 13);
ESP_LOGI(TAG, "Write: %s", test_str);
vTaskDelay(100);
for (int i = 0; i < sizeof(test_buf); i++) {
ret = spi_read(spi, &test_buf[i]);
ESP_ERROR_CHECK(ret);
}
ESP_LOGI(TAG, "Read: %s", test_buf);
memset(test_buf, 0, 4);
vTaskDelay(100);
}
}
ESP32做主机,NRF52832做从机,查看打印:
• 由 Leung 写于 2021 年 5 月 26 日
• 参考:ESPIDF开发ESP32学习笔记【SPI与片外FLASH基础】
ESP32 SPI驱动Li3dh&&kx203
【ESP32-IDF】 02-4 外设-SPI
ESP32设备SPI主设备驱动
网友评论