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

张开发
2026/4/18 12:10:32 15 分钟阅读

分享文章

ESP-IDF+vscode开发ESP32第六讲——SPI
目录前言一、SPI是什么二、SPI从机2.1 SPI.c2.2 SPI.h2.3 mian.c2.4 代码讲解2.5 结果展示三、SPI主机3.1 SPI.C3.2 SPI.h3.3 mian.c3.4 代码讲解3.5 结果展示前言SPI是一个非常重要的通信方式很多存储芯片和lcd显示屏都用到这种通讯方式本文基于微雪的ESP32-P4-Module-DEV-KIT开发板和第一讲创建的工程模板来完成SPI主机和从机两种通讯方式。开发环境是VScodeESP-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 SPI1PSRAM 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的依赖项添加进去。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#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 i0;it_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 *transSPI 从机传输描述符 * 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 *transSPI 从机传输描述符 * note * return void */ /*--------------------------------------------------------------------------*/ void spi_end_rxcallback(spi_slave_transaction_t *trans) { ESP_LOGI(TAG, SPI2 slave 接收或发送完成); }2.2 SPI.h#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); #endif2.3 mian.c#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控制从机传输只需改动下面部分内容即可将全局缓冲区地址变为数组删除注释部分代码。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 06InData (6):01 02 03 04 05 000084#17:27:09:909:: CmdC2(StreamSpi) succ.,Time 0.001S.OutData(6):01 02 03 04 05 06InData (6):01 02 03 04 05 000085#17:41:57:494:: CmdC2(StreamSpi) succ.,Time 0.001S.OutData(7):01 02 03 04 05 06 07InData (7):01 02 03 04 05 00 000086#17:41:59:646:: CmdC2(StreamSpi) succ.,Time 0.001S.OutData(7):01 02 03 04 05 06 07InData (7):01 02 03 04 05 00 00三、SPI主机因为我的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)3.1 SPI.Cstatic 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 i0;it_master.rxlength/8;i) { printf(%d , M_recvbuf[i]); } printf(\n); vTaskDelay(pdMS_TO_TICKS(1500)); } }3.2 SPI.h#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); #endif3.3 mian.c#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 :: length16接收数据长度保存在spi_transaction_t :: rxlength单位都是bit。不过此时会忽略接受的前两个字节因为这是命令位和地址位写入过程的接收数据一般都是oxff所以系统自动忽略了。注意在CPU控制的主机和从机传输中数据长度为1∼64字节在DMA控制的主机单次传输中数据长度为1∼32 KB在DMA控制的主机分段配置传输中数据长度字节数无限制整体内容和从机驱动很像下面看结果。3.5 结果展示因为我这从机和主机设备都在一块开发板中且双方线程没有设置互斥操作。所以打印信息有些重杂。逻辑是主机发送18字节的数据给从机从机接收到这18字节数据后回传18字节的数据给主机主机忽略前两个字节数据从而主机接收16个字节数据。

更多文章