美文网首页物联网单片机
ESP32学习笔记(19)——SPI(主机)接口使用

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

作者: Leung_ManWah | 来源:发表于2021-05-26 17:37 被阅读0次

    一、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接收中,如果定义了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主设备驱动

    相关文章

      网友评论

        本文标题:ESP32学习笔记(19)——SPI(主机)接口使用

        本文链接:https://www.haomeiwen.com/subject/entwjltx.html