目录
[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 结果展示)
[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 控制的主机和从机传输中:数据长度为 1∼64 字节
在 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个字节数据。