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

ESP-IDF+vscode开发ESP32第六讲——SPI目录 前言 一 SPI 是什么 二 SPI 从机 2 1 SPI c 2 2 SPI h 2 3 mian c 2 4 代码讲解 2 5 结果展示 三 SPI 主机 3 1 SPI C 3 2 SPI h 3 3 mian c 3 4 代码讲解 3 5 结果展示 SPI 是一个非常重要的通信方式 很多存储芯片和 lcd 显示屏都用到这种通讯方式

大家好,我是讯享网,很高兴认识大家。这里提供最前沿的Ai技术和互联网信息。



目录

前言

一、SPI是什么?

二、SPI从机

2.1 SPI.c

2.2 SPI.h

2.3 mian.c

2.4 代码讲解

2.5 结果展示

三、SPI主机

3.1 SPI.C

3.2 SPI.h

3.3 mian.c

3.4 代码讲解

3.5 结果展示


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

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


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

 
    
    
      
idf_component_register(SRCS "SPI.c" INCLUDE_DIRS "include" REQUIRES esp_driver_gpio PRIV_REQUIRES esp_hal_gpspi esp_driver_spi)

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

#include 
           
          

#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 = ; 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 } / ————————————————————————–/ /



  • @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 接收或发送完成"); }
 
    
    
      
#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

 
    
    
      
#include 
           
          

#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 毫秒 } 

}

 

首先关于各函数的解释可以参考官方文档《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控制从机传输,只需改动下面部分内容即可:

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

 
    
    
      
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。

我这是使用微雪的一款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

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

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

 
    
    
      
idf_component_register(SRCS "SPI.c" INCLUDE_DIRS "include" REQUIRES esp_driver_gpio PRIV_REQUIRES esp_hal_gpspi esp_driver_spi)
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 = , .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,""); 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: 

    ",t_master.rxlength/8);

    for(uint8_t i=0;i 
             
             
               

    ");

    vTaskDelay(pdMS_TO_TICKS(1500)); 

    } }

 
     
     
       
#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

 
     
     
       
#include 
            
           

#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 毫秒 } 

}

 

首先设置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 控制的主机分段配置传输中:数据长度字节数无限制

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

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

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

小讯
上一篇 2026-04-19 17:45
下一篇 2026-04-19 17:43

相关推荐

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/271787.html