4. DMA 与 SPI 结合:SPI 的 DMA 请求线、双缓冲与循环模式、中断与完成回调
好,咱们进入正题。DMA 和 SPI 结合,说白了就是让硬件自己搬数据,CPU 腾出手干别的活。我刚开始搞嵌入式那会儿,总觉得 DMA 是个玄学,配来配去不如直接中断读写省心。后来被一个高速 ADC 采集项目逼得没办法——CPU 忙到连喂狗的时间都没有,才老老实实把 DMA 吃透。今天我把这些经验掰开揉碎讲给你听。
4.1 SPI 的 DMA 请求线:谁在喊“我要数据”?
SPI 外设和 DMA 控制器之间,靠的是硬件请求线通信。SPI 的发送缓冲区空了,它就拉高请求线,告诉 DMA:“快给我塞数据!” 接收缓冲区有数据了,同样拉高请求线:“快来取走!”
不同芯片的请求线编号不一样。拿 STM32F4 系列举例:
| SPI 实例 | TX 请求(DMA 流) | RX 请求(DMA 流) |
|---|---|---|
| SPI1 | DMA2_Stream3 | DMA2_Stream0 |
| SPI2 | DMA1_Stream4 | DMA1_Stream3 |
| SPI3 | DMA1_Stream7 | DMA1_Stream2 |
4.2 双缓冲模式:一边发一边收,不打架
双缓冲,顾名思义,就是准备两个缓冲区。DMA 在搬第一个缓冲区的时候,CPU 可以往第二个缓冲区里写新数据。等 DMA 搬完了,自动切到第二个缓冲区,CPU 接着填第一个。
我习惯把这种模式叫“乒乓操作”。你看这个流程就明白了:
4.3 循环模式:让数据流永不停止
循环模式是 DMA 的“自动重播”功能。DMA 把缓冲区搬完一遍后,自动把地址指针重置到开头,接着搬第二遍。这对连续数据流场景特别有用——比如音频播放、波形输出。
配置循环模式时,DMA 控制寄存器里有个 CIRC 位(Circular Mode)。置 1 就开启。但要注意:
- 循环模式必须配合双缓冲,否则 DMA 会反复覆盖同一个缓冲区,数据全乱套。
- 中断触发时机要选对。我建议用“半传输完成中断”+“传输完成中断”。半传输完成时,CPU 处理前半段数据;传输完成时,CPU 处理后半段。这样 CPU 永远有半个缓冲区的时间来干活。
// STM32 HAL 库配置示例:SPI DMA 循环模式 + 双缓冲
// 假设 SPI 句柄为 hspi1,DMA 句柄为 hdma_tx
// 1. 配置 DMA 为循环模式
hdma_tx.Init.Mode = DMA_CIRCULAR; // 循环模式
hdma_tx.Init.FIFOMode = DMA_FIFOMODE_ENABLE;
hdma_tx.Init.FIFOThreshold = DMA_FIFO_THRESHOLD_FULL;
// 2. 配置双缓冲
// 注意:HAL 库的 DMA 双缓冲需要手动设置内存地址
// 第一次调用时指定 bufferA,传输完成回调里切换为 bufferB
HAL_DMAEx_ConfigMuxSync(&hdma_tx, ...);
// 3. 启动传输
HAL_SPI_Transmit_DMA(&hspi1, bufferA, BUFFER_SIZE);
// 4. 在传输完成回调中切换缓冲区
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi)
{
if (hspi->Instance == SPI1)
{
// 切换 DMA 内存地址到另一个缓冲区
HAL_DMAEx_ChangeMemory(&hdma_tx, (uint32_t)bufferB, MEMORY_0);
// 或者用寄存器直接操作
// hdma_tx.Instance->M0AR = (uint32_t)bufferB;
}
}
4.4 中断与完成回调:什么时候该干活?
DMA 传输完成时,会触发中断。中断服务程序里该干什么?我总结了三件事:
- 切换缓冲区指针(双缓冲模式下)
- 更新数据计数器(比如记录已传输的字节数)
- 通知应用层(通过回调函数或信号量)
回调函数的设计有个讲究:不要在回调里做复杂计算。回调是在中断上下文里执行的,时间长了会影响其他中断。我习惯的做法是:
// 推荐的回调设计模式
volatile uint8_t dma_tx_done = 0;
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi)
{
// 只做两件事:设标志 + 切换缓冲区
dma_tx_done = 1;
// 切换缓冲区代码...
}
void main_loop(void)
{
while (1)
{
if (dma_tx_done)
{
dma_tx_done = 0;
// 在这里处理数据,不在中断里处理
process_data();
}
// 其他任务...
}
}
- DMA 中断优先级不要高于系统滴答定时器,否则 HAL_Delay() 会失效。
- 如果 SPI 时钟超过 10MHz,建议开启 DMA 的 FIFO 模式(深度 4 或 8 级),减少总线争抢。
- 双缓冲的缓冲区建议放在 DTCM 或 SRAM 中,不要放在 AHB 总线上,否则 DMA 和 CPU 会抢总线。
嗯,到这里 DMA 和 SPI 结合的核心机制就讲完了。你想想看,双缓冲解决了数据覆盖问题,循环模式解决了连续传输问题,中断回调解决了同步问题——这三个东西凑在一起,才是一个能跑高速、不丢数据、CPU 还闲着的 SPI 传输方案。我在一个 20MHz SPI 的 OLED 显示项目里就是这么干的,CPU 占用率从 60% 降到了 5% 以下。