ESP32学习笔记(19)——SPI(主机)接口使用

一、SPI简介

SPI(Serial Peripheral Interface) 协议是由摩托罗拉公司提出的通讯协议,即串行外围设备接口,是一种高速全双工的通信总线。它被广泛地使用在 ADC、LCD 等设备与 MCU 间,要求通讯速率较高的场合。

芯片的管脚上只占用四根线。
MISO: 主器件数据输出,从器件数据输入。
MOSI:主器件数据输入,从器件数据输出。
SCK: 时钟信号,由主设备控制发出。
NSS(CS): 从设备选择信号,由主设备控制。当NSS为低电平则选中从器件。

1.1 ESP32中SPI

ESP32集成了4个SPI外设。

  • SPI0SPI1在内部用于访问ESP32所连接的闪存。两个控制器共享相同的SPI总线信号,并且有一个仲裁器来确定哪个可以访问该总线。
    在SPI1总线上使用SPI Master驱动程序时有很多限制,请参阅《在SPI1总线 上使用SPI Master驱动程序的注意事项》
  • SPI2SPI3通用SPI控制器,有时分别称为HSPI和VSPI。它们向用户开放。SPI2和SPI3具有独立的总线信号,分别具有相同的名称。每条总线具有3条CS线,最多能控制6个SPI从设备。

ESP32内部的SPI控制器可设置为主模式(Master),基本特点如下

  • 适应多线程环境
  • 可配置DMA辅助传输
  • 在同一信号线上自动分配时间处理来自不同设备的的多路数据,请参见SPI总线锁定

注意: SPI主驱动程序的概念是将多个设备连接到一条总线(共享一个ESP32 SPI外设)。只要仅通过一个任务访问每个设备,驱动程序就是线程安全的。但是,如果多个任务尝试访问同一SPI设备,则驱动程序不是线程安全的。在这种情况下,建议执行以下任一操作:
* 重构您的应用程序,以便每个SPI外围设备一次只能由一个任务访问。
* 使用围绕共享设备添加互斥锁xSemaphoreCreateMutex


ESP-IDF 编程指南——SPI主驱动

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_transaction_t::rxlength仅确定接收到缓冲区的数据长度。

在半双工通信中,读取和写入阶段不是同时的(一次是一个方向)。写入和读取阶段的长度分别由spi_transaction_tlengthrxlength成员确定。

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主机代码

根据 esp-idf\examples\peripherals\spi_master\spi_eeprom 中的例程修改
SPI读取改的有点问题

#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    0
#define PIN_NUM_MOSI    5
#define PIN_NUM_CLK     25
#define PIN_NUM_CS      26

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

版权声明:本文为CSDN博主「Leung_ManWah」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_36347513/article/details/117299126

生成海报
点赞 0

Leung_ManWah

我还没有学会写个人说明!

暂无评论

相关推荐