目录
前言
一、SPI是什么?
二、SPI从机
三、SPI主机
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 控制的主机和从机传输中:数据长度为 1∼64 字节
在 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个字节数据。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/271787.html