You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ESP32 VSPI半双工模式下非阻塞输出Dummy时钟的实现问询

ESP32 VSPI半双工通信:非阻塞式持续输出Dummy脉冲方案

我正在使用ESP32的VSPI接口以半双工方式与从设备通信,需求是:先向设备发送标准SPI事务,随后在同一VSPI接口持续输出Dummy脉冲(保持VSPICLK引脚同频率时钟,VSPID引脚输出0)。

目前已成功实现SPI事务发送,但找不到合适的方法输出Dummy脉冲,核心要求是Dummy脉冲不能阻塞代码,这样才能处理其他任务。

更新进展

通过调用writeBytes方法取得了一些进展,但该方法会阻塞代码:

/**
 * @param data 字节数组指针
 * @param size 数据长度
 */
void SPIClass::writeBytes(const uint8_t * data, uint32_t size)
{
    if(_inTransaction){
        return spiWriteNL(_spi, data, size);
    }
    spiSimpleTransaction(_spi);
    spiWriteNL(_spi, data, size);
    spiEndTransaction(_spi);
}

此前尝试

我曾尝试在常规SPI事务中发送空字节,但这是阻塞方法,且字节间存在明显延迟(可从VSCLK通道观测到)。

尝试代码

void write(uint8_t data)
{
    noInterrupts();

    // 与设备的有效事务
    // _spi_dev 为 SPIClass* 类型
    _spi_dev->beginTransaction();
    _spi_dev->transfer(data);
    _spi_dev->endTransaction();
    delayMicroseconds(10); // 从设备锁存命令所需的延迟

    // 简单发送Dummy字节 - 阻塞调用
    _spi_dev->beginTransaction();
    for (uint8_t n = 0; n < 100; n++) {
        for (int8_t c = 10; c > 0; c--) _spi_dev->transfer(0x0);
    }
    _spi_dev->endTransaction();
  
    interrupts();
}

这段代码中_spi_dev->transfer(data);最终调用的底层实现:

uint8_t spiTransferByte(spi_t * spi, uint8_t data)
{
    if(!spi) {
        return 0;
    }
    SPI_MUTEX_LOCK();
    spi->dev->mosi_dlen.usr_mosi_dbitlen = 7;
    spi->dev->miso_dlen.usr_miso_dbitlen = 7;
    spi->dev->data_buf[0] = data;
#if CONFIG_IDF_TARGET_ESP32C3
    spi->dev->cmd.update = 1;
    while (spi->dev->cmd.update);
#endif
    spi->dev->cmd.usr = 1;
    while(spi->dev->cmd.usr);
    data = spi->dev->data_buf[0] & 0xFF;
    SPI_MUTEX_UNLOCK();
    return data;
}

另外,_spi_dev->beginTransaction();对应的实现:

void SPIClass::beginTransaction(SPISettings settings)
{
    // 检查上次频率是否改变
    uint32_t cdiv = spiGetClockDiv(_spi);
    if(_freq != settings._clock || _div != cdiv) {
        _freq = settings._clock;
        _div = spiFrequencyToClockDiv(_freq);
    }
    spiTransaction(_spi, _div, settings._dataMode, settings._bitOrder);
    _inTransaction = true;
}

解决方案

要实现非阻塞的Dummy脉冲输出,建议直接使用ESP-IDF的DMA模式SPI传输或者硬件SPI的连续传输模式,避免使用Arduino封装的阻塞式API。

方案1:使用DMA+中断实现非阻塞连续Dummy输出

  1. 初始化VSPI接口时配置为DMA模式,指定发送缓冲区为全0的数组;
  2. 在完成初始SPI事务后,启动DMA连续传输,并通过中断回调处理传输完成后的重新启动,实现持续输出;
  3. 核心代码示例:
#include "driver/spi_master.h"

spi_device_handle_t spi_dev;
uint8_t dummy_buf[1024]; // 预分配全0的Dummy缓冲区
bool dummy_running = false;

// SPI传输完成中断回调
void spi_dummy_trans_done(spi_transaction_t *trans) {
    if(dummy_running) {
        // 重新启动传输,实现持续输出
        spi_device_queue_trans(spi_dev, trans, portMAX_DELAY);
    }
}

void init_spi() {
    // 初始化VSPI总线
    spi_bus_config_t buscfg={
        .miso_io_num=-1, // 半双工模式不需要MISO
        .mosi_io_num=VSPI_MOSI, // 对应VSPID引脚
        .sclk_io_num=VSPI_CLK,
        .quadwp_io_num=-1,
        .quadhd_io_num=-1,
        .max_transfer_sz=1024
    };
    spi_bus_initialize(SPI2_HOST, &buscfg, SPI_DMA_CH_AUTO);

    // 配置从设备
    spi_device_interface_config_t devcfg={
        .clock_speed_hz=1000000, // 同初始事务频率
        .mode=0, // 同初始事务模式
        .spics_io_num=-1, // 如果不需要CS可以设为-1
        .queue_size=1,
        .post_cb=spi_dummy_trans_done // 传输完成回调
    };
    spi_bus_add_device(SPI2_HOST, &devcfg, &spi_dev);

    // 初始化Dummy缓冲区为全0
    memset(dummy_buf, 0, sizeof(dummy_buf));
}

void send_initial_transaction(uint8_t cmd) {
    // 发送初始SPI事务
    spi_transaction_t trans={
        .length=8, // 1字节命令
        .tx_buffer=&cmd,
        .rx_buffer=NULL
    };
    spi_device_transmit(spi_dev, &trans);
}

void start_dummy_pulses() {
    dummy_running = true;
    spi_transaction_t *trans = spi_transaction_alloc();
    trans->length = sizeof(dummy_buf)*8;
    trans->tx_buffer = dummy_buf;
    trans->rx_buffer = NULL;
    // 启动第一次传输,后续由回调自动重启
    spi_device_queue_trans(spi_dev, trans, portMAX_DELAY);
}

void stop_dummy_pulses() {
    dummy_running = false;
    // 清空队列
    spi_transaction_t *trans;
    spi_device_get_trans_result(spi_dev, &trans, portMAX_DELAY);
    spi_transaction_free(trans);
}

方案2:利用FreeRTOS任务实现非阻塞连续传输

如果不需要DMA,可以通过FreeRTOS任务单独处理Dummy脉冲输出,利用任务调度实现非阻塞:

uint8_t dummy_buf[1024]; // 预分配全0的Dummy缓冲区

void dummy_spi_task(void *arg) {
    SPIClass* _spi_dev = (SPIClass*)arg;
    // 进入事务后保持激活,避免重复配置总线
    _spi_dev->beginTransaction(SPISettings(1000000, MSBFIRST, SPI_MODE0));
    while(1) {
        // 批量写入0,减少循环开销
        _spi_dev->writeBytes(dummy_buf, sizeof(dummy_buf));
        // 主动让出CPU,让其他任务执行
        vTaskDelay(0);
    }
}

// 在初始化时启动任务
void setup() {
    // 初始化VSPI等操作...
    memset(dummy_buf, 0, sizeof(dummy_buf));
    xTaskCreate(dummy_spi_task, "dummy_spi", 2048, _spi_dev, 5, NULL);
}

关键说明

  • 避免使用单个transfer(0x0)循环,会导致字节间延迟,改用批量写入writeBytes或DMA传输;
  • 非阻塞的核心是利用FreeRTOS任务调度或DMA中断,让CPU可以处理其他任务;
  • 确保Dummy脉冲的时钟频率、数据模式与初始SPI事务完全一致。

内容的提问来源于stack exchange,提问作者vibrantcloud

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.08 10:55:16