ESP-IDF+vscode开发ESP32第六讲——SPI

目录

前言

一、SPI是什么?

二、SPI从机

[2.1 SPI.c](#2.1 SPI.c)

[2.2 SPI.h](#2.2 SPI.h)

[2.3 mian.c](#2.3 mian.c)

[2.4 代码讲解](#2.4 代码讲解)

[2.5 结果展示](#2.5 结果展示)

三、SPI主机

[3.1 SPI.C](#3.1 SPI.C)

[3.2 SPI.h](#3.2 SPI.h)

[3.3 mian.c](#3.3 mian.c)

[3.4 代码讲解](#3.4 代码讲解)

[3.5 结果展示](#3.5 结果展示)


前言

SPI是一个非常重要的通信方式,很多存储芯片和lcd显示屏都用到这种通讯方式,本文基于微雪的ESP32-P4-Module-DEV-KIT开发板和第一讲创建的工程模板来完成SPI主机和从机两种通讯方式。

开发环境是VScode+ESP-IDF6.0,开发芯片是ESP32P4。


一、SPI是什么?

SPI 即串行外设接口(Serial Peripheral Interface),是一种高速的、全双工、同步的通信总线,主要用于微控制器(MCU)与各种外围设备之间进行短距离、高速率的数据传输。

SPI 接口一般由 4 根线组成,分别是时钟线(SCLK)、主输出从输入线(MOSI)、主输入从输出线(MISO)和片选线(CS)。SCLK 用于提供时钟信号,MOSI 用于主设备向从设备发送数据,MISO 用于从设备向主设备发送数据,CS 用于选择特定的从设备。

ESP32-P4 芯片集成了四个 SPI 控制器:

  • MSPI 控制器,简称 MSPI,包括:
    • FLASH MSPI 控制器:FLASH MSPI SPI0 、FLASH MSPI SPI1
    • PSRAM MSPI 控制器 :PSRAM MSPI SPI0 、PSRAM MSPI SPI1
  • 通用 SPI2,简称 GP-SPI2
  • 通用 SPI3,简称 GP-SPI3
  • 低功耗 SPI,简称 LP-SP

管脚分配

ESP32不像stm32,外设引脚不是固定的,而是可以通过配置将一系列引脚复用到某个外设功能

FLASH MSPI 控制器使用专用数字管脚,管脚序号为 27~33。这些专用引脚,无法另作他用,只有这一个功能。

GP-SPI2 接口的管脚有两组,一组四线接口通过 IO MUX 与 GPIO6~GPIO11 复用,另一组八线接口通过 IO MUX 与 GPIO28~GPIO38管脚复用。对 GP-SPI2 接口速度要求不高时,也可以通过 GPIO 交换矩阵可配置使用任意 GPIO 管脚。

GP-SPI3 接口通过 GPIO 交换矩阵可配置使用任意 GPIO 管脚。

LP-SPI 接口通过 LP GPIO 交换矩阵可配置使用任意管脚。

其他内容,可参考官方的《技术参考手册》。

二、SPI从机

SPI 从机的工作频率最高可达 60 MHz。如果时钟频率过快或占空比不足 50%,数据就无法被正确识别或接收。先把SPI的依赖项添加进去。

cpp 复制代码
idf_component_register(SRCS "SPI.c"
                    INCLUDE_DIRS "include"
                    REQUIRES esp_driver_gpio
                    PRIV_REQUIRES esp_hal_gpspi esp_driver_spi)

下面是spi从机通讯各文件代码

2.1 SPI.c

cpp 复制代码
#include <stdio.h>
#include "SPI.h"
#include "hal/spi_types.h"
#include "driver/spi_common.h"
#include "driver/spi_master.h"
#include "driver/spi_slave.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"

static const char *TAG = "SPI";
static uint8_t* S_sendbuf = NULL;
static uint8_t* S_recvbuf = NULL;
void spi_start_rxCallback(spi_slave_transaction_t *trans);
void spi_end_rxcallback(spi_slave_transaction_t *trans);
void spi_Stransmit(void *pvParameters);
/*--------------------------------------------------------------------------*/
/**
 * @brief SPI 从机初始化函数
 * @param[in] void
 * @note 
 * @return void
 */
/*--------------------------------------------------------------------------*/
void spi_slave_init(void)
{
    spi_bus_config_t buscfg = {
        .miso_io_num = spi3_miso,
        .mosi_io_num = spi3_mosi,
        .sclk_io_num = spi3_sck,
        .quadwp_io_num = -1,
        .quadhd_io_num = -1,
    };
    spi_slave_interface_config_t slavecfg = {
        .spics_io_num = spi3_cs,
        .queue_size = 3,
        .mode = 0,
        .post_setup_cb = spi_start_rxCallback,
        .post_trans_cb = spi_end_rxcallback,
    };
    ESP_ERROR_CHECK(spi_slave_initialize(SPI3_HOST, &buscfg, &slavecfg, SPI_DMA_CH_AUTO));
    S_sendbuf = spi_bus_dma_memory_alloc(SPI3_HOST, 70, 0);
    S_recvbuf = spi_bus_dma_memory_alloc(SPI3_HOST, 70, 0);
    if(S_sendbuf != NULL && S_recvbuf != NULL){
        ESP_LOGI(TAG, "SPI slave memory allocation success");
    }
    S_sendbuf[0] = 0x01;
    S_sendbuf[1] = 0x02;
    S_sendbuf[2] = 0x03;
    S_sendbuf[3] = 0x04;
    S_sendbuf[4] = 0x05;
    xTaskCreate(spi_Stransmit, "spi_Stransmit", 2048, NULL, 4, NULL );
}
/*--------------------------------------------------------------------------*/
/**
 * @brief SPI从机线程
 * @param[in] void
 * @note 
 * @return void
 */
/*--------------------------------------------------------------------------*/
void spi_Stransmit(void *pvParameters)
{
    spi_slave_transaction_t t_slave = {
        .length = 64 * 8,
        .tx_buffer = S_sendbuf,
        .rx_buffer = S_recvbuf,
    };
    while(1)
    {
        ESP_ERROR_CHECK(spi_slave_transmit(SPI3_HOST, &t_slave, portMAX_DELAY));
        ESP_LOGI(TAG, "slave rx length:%d, slave rx buffer:",t_slave.trans_len/8);
        for(uint8_t i=0;i<t_slave.trans_len/8;i++)
        {
            printf("%d  ", S_recvbuf[i]);
        }
        printf("\n");
        vTaskDelay(pdMS_TO_TICKS(100));
    }   
}
/*--------------------------------------------------------------------------*/
/**
 * @brief SPI 从机开始传输回调函数,在传输开始前调用,一但返回传输立即开始
 * @param[in] spi_slave_transaction_t *trans:SPI 从机传输描述符
 * @note 
 * @return void
 */
/*--------------------------------------------------------------------------*/
void spi_start_rxCallback(spi_slave_transaction_t *trans)
{
    ESP_LOGI(TAG, "SPI2 slave 准备接收或发送");
}
/*--------------------------------------------------------------------------*/
/**
 * @brief SPI 从机传输结束回调函数,在传输结束后立即调用
 * @param[in] spi_slave_transaction_t *trans:SPI 从机传输描述符
 * @note 
 * @return void
 */
/*--------------------------------------------------------------------------*/
void spi_end_rxcallback(spi_slave_transaction_t *trans)
{
    ESP_LOGI(TAG, "SPI2 slave 接收或发送完成");
}

2.2 SPI.h

cpp 复制代码
#ifndef _SPI_H
#define _SPI_H

#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"

#define spi3_mosi 46
#define spi3_miso 27
#define spi3_sck 53
#define spi3_cs 47

void spi_slave_init(void);

#endif

2.3 mian.c

cpp 复制代码
#include <stdio.h>
#include "user.h"
#include "SPI.h"
void app_main(void)
{
    CONSOLE_REPL_INIT(); // 初始化控制台REPL环境
    ESP_LDOV4_SET(3300);
    spi_slave_init();
    while (1)
    {
        vTaskDelay(pdMS_TO_TICKS(300)); // 任务延时 300 毫秒
    }
}

2.4 代码讲解

首先关于各函数的解释可以参考官方文档《SPI从机驱动程序》,或者是我总结的《ESP32 实用API指南2-CSDN博客》。

我这里用的是4位传输模式,这也是最常用的模式。

首先先创建SPI 总线配置结构体和从机配置结构体,从而确定好SPI从机的工作模式。其中post_setup_cb 设置的是开始传输回调函数,从机SPI会在接收或发送数据的前一刻调用这个函数;post_trans_cb设置的是传输结束回调函数,从机SPI的一个数据包接收或发送完成后会第一时间调用这个函数。当把函数设置为NULL,代表跳过不使用这个回调函数。

这两个回调函数就是中断回调,不允许在函数内允许任何需要延时的代码。比如你想打印消息,只能用ESP_LOG,如果使用printf则会报错。

接下来初始化SPI从机。

接着使用为SPI总线DMA 传输分配专用内存函数spi_bus_dma_memory_alloc来分配发送缓冲区和接收缓冲区。这两个缓冲区并不是都必须分配,如果只需要发送数据,那就只分配发送缓冲区,接收同理。

判断缓冲区地址没问题,给发送缓冲区设一些初值,接着创建一个spi从机工作线程就完成初始化了

在spi从机工作线程里先创建从机传输结构体,注意,如果使用了DMA传输,则传输的数据长度length必须是64字节的倍数。但这并不是一次SPI 传输的实际长度。传输实际的长度由主机的时钟线和 CS 线决定,并且在传输完成后,能从spi_slave_transaction_t::trans_len 中读取实际长度。length设置的是传输预期最大值,超过该长度的数据会被舍弃

spi_slave_transmit 函数一但运行会阻塞线程等待SPI数据的到来,如果超时内还没有等到,则会返回。可以将超时设为永久,那便会一直等待。一旦等待到了SPI数据,便会立即进入传输开始回调函数post_setup_cb ,此时数据还没有传输到缓冲区。等函数post_setup_cb 运行退出后,便立即开始传输,将数据传输到接收缓冲区并同时将发送缓冲区的数据发送出去。等数据传输完成后理解调用回调函数post_trans_cb,此时一个数据包传输完成。接着线程从阻塞态恢复到运行态,线程开始运行。

spi_slave_transaction_t::trans_len保存的是实际接收到的数据长度,单位是bit,除以8得到实际接收字节。注意:

在 CPU 控制的主机和从机传输中:数据长度为 164 字节

在 DMA 控制的从机单次或连续传输中:数据长度字节数无限制

如果使用cpu控制从机传输,只需改动下面部分内容即可:

将全局缓冲区地址变为数组,删除注释部分代码。

cpp 复制代码
static uint8_t S_sendbuf[12] = {0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0a,0x0b,0x0c};
static uint8_t S_recvbuf[12];

    //sendbuf = spi_bus_dma_memory_alloc(SPI2_HOST, 64, 0);
   //recvbuf = spi_bus_dma_memory_alloc(SPI2_HOST, 64, 0);
    //if(sendbuf != NULL && recvbuf != NULL){
    //    ESP_LOGI(TAG, "SPI slave memory allocation success");
    //}

改为cpu控制后,传输长度length可设置为任意字节。

**注意:**要保证spi复用的4个管脚电平和主机电平一致,一般是3.3V或5V。

2.5 结果展示

我这是使用微雪的一款USB转SPI转换器作为主机。

从机传输日志:

主机传输日志:

0083#17:27:08:458:: CmdC2(StreamSpi) succ.,Time 0.001S.

OutData(6):01 02 03 04 05 06

InData (6):01 02 03 04 05 00

0084#17:27:09:909:: CmdC2(StreamSpi) succ.,Time 0.001S.

OutData(6):01 02 03 04 05 06

InData (6):01 02 03 04 05 00

0085#17:41:57:494:: CmdC2(StreamSpi) succ.,Time 0.001S.

OutData(7):01 02 03 04 05 06 07

InData (7):01 02 03 04 05 00 00

0086#17:41:59:646:: CmdC2(StreamSpi) succ.,Time 0.001S.

OutData(7):01 02 03 04 05 06 07

InData (7):01 02 03 04 05 00 00

三、SPI主机

因为我的USB转SPI转换器只能做主机,所以我打算给开发板初始化两个SPI控制器,上面的SPI3作为从机,下面的SPI2作为主机。

同样把SPI的依赖项添加进去。

cpp 复制代码
idf_component_register(SRCS "SPI.c"
                    INCLUDE_DIRS "include"
                    REQUIRES esp_driver_gpio
                    PRIV_REQUIRES esp_hal_gpspi esp_driver_spi)

3.1 SPI.C

cpp 复制代码
static char M_sendbuf[20];
static char M_recvbuf[20];
void spi_Mtransmit(void *pvParameters);
void spi_Stransmit(void *pvParameters);
spi_device_handle_t spi2_handle;

/*--------------------------------------------------------------------------*/
/**
 * @brief SPI 主机初始化函数
 * @param[in] void
 * @note 
 * @return void
 */
/*--------------------------------------------------------------------------*/
void spi_master_init(void)
{
    spi_bus_config_t buscfg = {
        .miso_io_num = spi2_miso,
        .mosi_io_num = spi2_mosi,
        .sclk_io_num = spi2_sck,
        .quadwp_io_num = -1,
        .quadhd_io_num = -1,
    };
    spi_device_interface_config_t devcfg = {
        .command_bits = 8,
        .address_bits = 8,
        .clock_speed_hz = 30000000,
        .mode = 0,
        .spics_io_num = spi2_cs,
        .queue_size = 10,
    };
    ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_DISABLED));
    ESP_ERROR_CHECK(spi_bus_add_device(SPI2_HOST, &devcfg, &spi2_handle));
    xTaskCreate(spi_Mtransmit, "spi_Mtransmit", 2048, NULL, 6, NULL );
}
/*--------------------------------------------------------------------------*/
/**
 * @brief SPI 主机线程函数
 * @param[in] void
 * @note 
 * @return void
 */
/*--------------------------------------------------------------------------*/
void spi_Mtransmit(void *pvParameters)
{
    sprintf(M_sendbuf,"44445555");
    spi_transaction_t t_master = {
        .cmd = 0xfa,
        .addr = 0xfe,
        .length = 16 * 8,
        .tx_buffer = M_sendbuf,
        .rx_buffer = M_recvbuf,
    };
    while(1)
    {
        ESP_ERROR_CHECK(spi_device_transmit(spi2_handle, &t_master));  
        printf("master rx length:%d, master rx buffer:\n",t_master.rxlength/8);
        for(uint8_t i=0;i<t_master.rxlength/8;i++)
        {
            printf("%d  ", M_recvbuf[i]);
        }
        printf("\n");
        vTaskDelay(pdMS_TO_TICKS(1500));
    }
}

3.2 SPI.h

cpp 复制代码
#ifndef _SPI_H
#define _SPI_H

#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"

#define spi2_mosi 22
#define spi2_miso 21
#define spi2_sck 5
#define spi2_cs 6

void spi_master_init(void);

#endif

3.3 mian.c

cpp 复制代码
#include <stdio.h>
#include "user.h"
#include "SPI.h"
void app_main(void)
{
    CONSOLE_REPL_INIT(); // 初始化控制台REPL环境
    ESP_LDOV4_SET(3300);
    spi_slave_init();
    spi_master_init();
    while (1)
    {
        vTaskDelay(pdMS_TO_TICKS(300)); // 任务延时 300 毫秒

    }
}

3.4 代码讲解

首先设置SPI总线配置和设备配置,这些配置包括了spi所有的功能,同样关于各函数的解释可以参考官方文档《SPI主机驱动程序》,或者是我总结的《ESP32 实用API指南2-CSDN博客》。

接着初始化总线、添加一个spi主机设备和创建一个主机线程。

同样是spi主机也可以和从机一样,可以设置传输起始回调函数和传输结束回调函数,不过我这没设置。

我这设置了一个字节的命令位和地址位,那么最后总的发送数据为spi_transaction_t :: length+16,接收数据长度保存在spi_transaction_t :: rxlength,单位都是bit。不过此时会忽略接受的前两个字节,因为这是命令位和地址位写入过程的接收数据,一般都是oxff,所以系统自动忽略了。注意:

在 CPU 控制的主机和从机传输中:数据长度为 1*∼*64 字节

在 DMA 控制的主机单次传输中:数据长度为 1*∼*32 KB

在 DMA 控制的主机分段配置传输中:数据长度字节数无限制

整体内容和从机驱动很像,下面看结果。

3.5 结果展示

因为我这从机和主机设备都在一块开发板中,且双方线程没有设置互斥操作。所以打印信息有些重杂。

逻辑是主机发送18字节的数据给从机,从机接收到这18字节数据后回传18字节的数据给主机,主机忽略前两个字节数据,从而主机接收16个字节数据。

相关推荐
SiYuanFeng2 小时前
打开vscode总是提示未找到python的解决办法(打开终端却能找到)
ide·vscode·python
Webgiserin1 天前
nvm+vscode配置安装暂记
ide·vscode·编辑器
charlie1145141911 天前
嵌入式C++工程实践——第13篇:第一次重构 —— enum class取代宏,类型安全的开始
开发语言·c++·vscode·stm32·安全·重构·现代c++
weifengdq1 天前
SJA1124 SPI转4路LIN STM32 测试笔记
stm32·spi·nxp·lin·sja1124·spi4lin
传说中胖子1 天前
Magento服务器VSCode开启XDebug方法
服务器·vscode·php
davidson14711 天前
VSCode配置Claude Code
vscode·ai·大模型·claude
AI 赋能1 天前
Claude Code for VS Code 使用手册
vscode·claude
H Journey1 天前
Windows 下 使用VSCode 编写C++程序中文乱码问题
c++·windows·vscode·cmake/gcc
ntGrace2 天前
Windows环境下,在Vscode里利用ESP-IDF开发ESP32S3项目时,不能构建的问题1及解决方法
vscode·编辑器