基于 RT-Thread 的 SPI 设备驱动框架下 DMA 传输超时故障定位手册

1. 问题背景与现象

在使用 RT-Thread 的 SPI 设备驱动框架时,开发者常通过 rt_spi_transfer_message()rt_spi_send_then_recv() 等接口发起 DMA 传输。当传输数据量较大(如 > 16 字节)时,DMA 模式被自动启用,但有时程序会卡死在等待传输完成的信号量上,最终触发超时错误(如 -RT_ETIMEOUT)。典型现象包括:

  • 传输函数返回超时,但 SPI 总线上有部分数据波形。
  • 偶发性超时,尤其在系统负载高或中断优先级配置不当的情况下。
  • 使用逻辑分析仪发现 DMA 传输未完成,但 SPI 外设已空闲。

2. 框架与 DMA 传输机制

RT-Thread 的 SPI 驱动框架将底层操作抽象为 struct rt_spi_ops,其中 xfer 函数负责实际传输。当配置了 DMA 时,驱动通常会在 xfer 中启动 DMA 传输,并挂起当前线程等待一个完成信号量。DMA 传输完成中断(或 SPI 传输完成中断)会释放该信号量。

关键点:

  • 传输超时通常意味着信号量未在预期时间内被释放。
  • 信号量释放依赖于中断服务程序(ISR)正确执行。
  • 中断优先级、DMA 配置、SPI 参数均可能影响传输完成。

3. 故障根因分类与排查步骤

3.1 硬件层面

  • DMA 通道冲突:确保 DMA 通道未被其他外设占用,且中断能正常触发。检查 DMA 中断是否在 NVIC 中使能。
  • SPI 引脚复用:错误的 GPIO 复用可能导致 SPI 时钟或数据线异常,DMA 无法完成传输。
  • 电气连接:片选信号不稳定或从设备无响应,可能导致 SPI 等待从设备时钟拉伸(如果支持),但 DMA 仍会完成,需注意。

3.2 软件配置层面

  • DMA 配置错误:数据宽度、方向、缓冲区地址对齐等。例如,SPI DMA 要求缓冲区地址按 4 字节对齐(部分 MCU)。
  • SPI 参数不匹配:时钟极性、相位、速率设置错误,导致从设备无响应,但 DMA 仍会完成传输,超时可能源于等待从设备 ACK(如 EEPROM 的写周期)。
  • RT-Thread 设备配置:检查 SPI 设备是否已正确注册,DMA 通道是否在 board.h 或驱动中初始化。

3.3 中断与优先级问题

  • 中断优先级过低:如果 DMA 完成中断优先级低于当前线程或其它高频中断,可能导致 ISR 延迟,信号量迟迟未释放。
  • 中断丢失:某些 MCU 的 DMA 中断标志需要软件清除,若未清除,后续传输无法触发中断。
  • 信号量超时时间设置rt_spi_transfer_message 中的超时参数可能过短,尤其在系统繁忙时。

4. 系统化定位步骤

步骤 1:确认 DMA 是否真正启动

xfer 函数中打印或断点检查 DMA 控制寄存器,确认 DMA 已使能且缓冲区地址正确。

// 示例:在 stm32_spi.c 的 xfer 函数中增加调试
rt_kprintf("DMA TX addr: 0x%08X, count: %d\n", (unsigned int)send_buf, count);

步骤 2:检查中断标志与信号量

在 DMA 完成中断服务函数中打印标志,确认中断是否触发。

void DMA1_Channel3_IRQHandler(void)
{
    if (DMA_GetITStatus(DMA1_IT_TC3)) {
        DMA_ClearITPendingBit(DMA1_IT_TC3);
        rt_sem_release(&spi_dma_sem);
        rt_kprintf("DMA TC\n"); // 调试输出
    }
}

步骤 3:验证信号量释放路径

如果中断触发但信号量未释放,检查信号量是否被其他线程消耗,或初始化是否正确。

步骤 4:调整超时与优先级

  • 将 DMA 中断优先级设为最高(如 0),避免延迟。
  • 增加超时时间,如从 100ms 改为 1s,观察是否仍超时。

5. 完整代码示例(以 STM32 为例)

以下是一个基于 RT-Thread 的 SPI DMA 传输配置示例,包含初始化与错误处理。

#include <rtthread.h>
#include <rtdevice.h>
#include "spi_flash.h" // 假设为 SPI 从设备

#define SPI_DEV_NAME     "spi1"
#define DMA_SEM_TIMEOUT  rt_tick_from_millisecond(1000)

static struct rt_semaphore spi_dma_sem;

// DMA 完成中断回调(由驱动调用)
static void spi_dma_complete(void *param)
{
    rt_sem_release(&spi_dma_sem);
}

// SPI 设备初始化
static int spi_dma_init(void)
{
    rt_err_t res;
    rt_device_t spi_dev;
    struct rt_spi_device *spi_device;

    // 查找 SPI 设备
    spi_dev = rt_device_find(SPI_DEV_NAME);
    if (!spi_dev) {
        rt_kprintf("SPI device %s not found\n", SPI_DEV_NAME);
        return -RT_ERROR;
    }

    // 配置 SPI 参数
    struct rt_spi_configuration cfg;
    cfg.data_width = 8;
    cfg.mode = RT_SPI_MODE_0 | RT_SPI_MSB | RT_SPI_MASTER;
    cfg.max_hz = 1000000; // 1MHz
    rt_spi_configure((struct rt_spi_device *)spi_dev, &cfg);

    // 初始化信号量(二值信号量)
    rt_sem_init(&spi_dma_sem, "spi_dma", 0, RT_IPC_FLAG_FIFO);

    // 注册 DMA 完成回调(假设驱动支持)
    // 实际驱动中,可能通过 rt_spi_register_callback 或直接修改驱动
    // 此处仅为示例,需根据具体驱动调整
    // spi_device->parent.user_data = &spi_dma_sem;

    return RT_EOK;
}
INIT_APP_EXPORT(spi_dma_init);

// DMA 传输函数
static rt_err_t spi_dma_transfer(rt_device_t dev, rt_uint8_t *tx_buf, rt_uint8_t *rx_buf, rt_size_t len)
{
    rt_err_t res;
    struct rt_spi_message msg;

    msg.send_buf = tx_buf;
    msg.recv_buf = rx_buf;
    msg.length = len;
    msg.cs_take = 1;
    msg.cs_release = 1;

    // 发送前复位信号量
    rt_sem_control(&spi_dma_sem, RT_IPC_CMD_RESET, 0);

    // 调用 SPI 传输接口(内部会启动 DMA)
    res = rt_spi_transfer_message((struct rt_spi_device *)dev, &msg);
    if (res != RT_EOK) {
        rt_kprintf("SPI transfer error: %d\n", res);
        return res;
    }

    // 等待 DMA 完成信号量
    res = rt_sem_take(&spi_dma_sem, DMA_SEM_TIMEOUT);
    if (res != RT_EOK) {
        rt_kprintf("SPI DMA timeout!\n");
        return -RT_ETIMEOUT;
    }

    return RT_EOK;
}

// 测试函数
void spi_dma_test(void)
{
    rt_uint8_t tx_buf[32] = {0xAA, 0x55, 0x01, 0x02};
    rt_uint8_t rx_buf[32] = {0};
    rt_err_t res;

    res = spi_dma_transfer(rt_device_find(SPI_DEV_NAME), tx_buf, rx_buf, 32);
    if (res == RT_EOK) {
        rt_kprintf("DMA transfer OK\n");
    } else {
        rt_kprintf("DMA transfer failed\n");
    }
}
MSH_CMD_EXPORT(spi_dma_test, spi dma test);

6. 注意事项与最佳实践

  • 缓冲区对齐:DMA 要求缓冲区地址按 4 字节对齐,否则可能触发硬件错误。使用 rt_align(4)__attribute__((aligned(4)))
  • 超时时间设置:根据传输长度和 SPI 速率估算传输时间,设置合理超时,避免过长导致系统卡死。
  • 中断优先级:DMA 完成中断优先级应高于任何可能长时间阻塞的中断,但低于系统节拍中断(如果可能)。
  • 调试建议:使用逻辑分析仪观察 SPI 波形,确认 DMA 是否实际传输;在 ISR 中打印标志,避免优化器移除。
  • 驱动兼容性:不同 BSP 的 SPI 驱动实现可能不同,需阅读源码确认 DMA 完成回调的注册方式。

7. 总结

SPI DMA 超时问题往往源于中断配置、DMA 参数或信号量管理。通过系统化排查,从硬件到软件逐步验证,可快速定位根因。本文提供的方法和示例代码适用于大多数 RT-Thread 平台,希望帮助开发者减少调试时间,提升开发效率。