DMA技术实战:如何用Scatter-Gather模式提升嵌入式系统性能(附代码示例)
在嵌入式系统开发中,性能优化往往需要从底层硬件着手。当面对高速数据采集、实时信号处理等场景时,传统CPU搬运数据的方式很容易成为瓶颈。这时,DMA(直接内存访问)技术就成为了工程师的秘密武器。而Scatter-Gather DMA作为DMA的高级模式,能够在不增加CPU负担的情况下,实现更复杂的内存访问模式。
本文将从一个实际项目案例出发,展示如何在STM32平台上配置Scatter-Gather DMA,通过具体代码演示如何优化内存访问效率。我们不仅会分析寄存器配置的关键细节,还会通过性能测试数据对比不同实现方式的差异。无论你是正在开发高速数据采集系统,还是需要优化现有嵌入式项目的I/O性能,这些实战经验都能为你提供直接参考。
1. Scatter-Gather DMA的核心优势
Scatter-Gather DMA(简称SG-DMA)与传统DMA的最大区别在于它能够处理非连续内存块的数据传输。想象一下这样的场景:你的传感器数据需要被分别存放到三个不同的内存区域——原始数据缓冲区、处理后的结果区,以及用于网络传输的打包区。使用普通DMA,你需要配置三次传输并处理三次中断;而SG-DMA只需一次配置就能完成全部工作。
SG-DMA的三大技术特点:
- 链表式传输管理:通过描述符(Descriptor)链表定义多个传输任务
- 自动地址递增:硬件自动根据描述符跳转到下一个内存区域
- 统一中断通知:所有传输完成后只触发一次中断
在STM32H7系列中,一个典型的SG-DMA描述符包含以下字段:
typedef struct { uint32_t SrcAddr; // 源地址 uint32_t DstAddr; // 目标地址 uint32_t NextDesc; // 下一个描述符地址 uint32_t Control; // 控制字(包含数据长度等信息) } DMA_DescriptorTypeDef;提示:描述符通常需要按32字节对齐,这是许多DMA控制器的硬件要求。在STM32CubeIDE中可以使用
__attribute__((aligned(32)))确保对齐。
2. STM32平台上的硬件配置
以STM32H743为例,我们需要配置以下硬件资源:
2.1 时钟与DMA控制器初始化
首先确保DMA控制器的时钟已使能:
__HAL_RCC_DMA2_CLK_ENABLE();然后配置DMA流参数。以下是MDMA(Master DMA,STM32H7的高性能DMA)的初始化代码片段:
MDMA_HandleTypeDef hmdma; hmdma.Instance = MDMA_Channel0; hmdma.Init.Request = MDMA_REQUEST_SW; hmdma.Init.TransferTriggerMode = MDMA_BUFFER_TRANSFER; hmdma.Init.Priority = MDMA_PRIORITY_HIGH; hmdma.Init.Endianness = MDMA_LITTLE_ENDIANNESS; hmdma.Init.SourceInc = MDMA_SRC_INC_WORD; hmdma.Init.DestinationInc = MDMA_DEST_INC_WORD; hmdma.Init.SourceDataSize = MDMA_SRC_DATASIZE_WORD; hmdma.Init.DestDataSize = MDMA_DEST_DATASIZE_WORD; hmdma.Init.DataAlignment = MDMA_DATAALIGN_PACKENABLE; HAL_MDMA_Init(&hmdma);2.2 描述符链表构建
创建两个描述符实现从内存到外设的分散写入:
DMA_DescriptorTypeDef Desc[2] __attribute__((aligned(32))); // 第一个描述符:传输数组前半部分到USART1 Desc[0].SrcAddr = (uint32_t)&buffer[0]; Desc[0].DstAddr = (uint32_t)&USART1->TDR; Desc[0].NextDesc = (uint32_t)&Desc[1]; Desc[0].Control = (100 << MDMA_CxCTCR_TLEN_Pos) | MDMA_CxCTCR_SWRM | MDMA_CxCTCR_SINC_ENABLE | MDMA_CxCTCR_DINC_DISABLE; // 第二个描述符:传输数组后半部分到USART1 Desc[1].SrcAddr = (uint32_t)&buffer[100]; Desc[1].DstAddr = (uint32_t)&USART1->TDR; Desc[1].NextDesc = 0; // 链表结束 Desc[1].Control = (100 << MDMA_CxCTCR_TLEN_Pos) | MDMA_CxCTCR_SWRM | MDMA_CxCTCR_SINC_ENABLE | MDMA_CxCTCR_DINC_DISABLE | MDMA_CxCTCR_BTIE; // 开启传输完成中断2.3 中断配置
启用DMA传输完成中断:
HAL_NVIC_SetPriority(MDMA_IRQn, 0, 0); HAL_NVIC_EnableIRQ(MDMA_IRQn);对应的中断服务函数中处理完成事件:
void MDMA_IRQHandler(void) { if(__HAL_MDMA_GET_FLAG(&hmdma, MDMA_FLAG_TC)) { __HAL_MDMA_CLEAR_FLAG(&hmdma, MDMA_FLAG_TC); // 处理传输完成逻辑 } }3. 性能优化关键技巧
3.1 缓存一致性处理
在使用带Cache的处理器(如STM32H7)时,必须注意DMA与CPU缓存的一致性问题。以下是常见解决方案对比:
| 方案 | 操作方式 | 适用场景 | 性能影响 |
|---|---|---|---|
| 手动维护 | 调用SCB_CleanDCache()等函数 | 小数据量 | 中等 |
| MPU配置 | 设置内存区域为Non-cacheable | 固定DMA缓冲区 | 最低 |
| 硬件自动 | 使用支持Cache的DMA(如STM32H7的Cache维护操作) | 大数据量 | 最低 |
推荐在分散-聚集传输前执行缓存清理:
SCB_CleanDCache_by_Addr((uint32_t*)buffer, sizeof(buffer));3.2 描述符预构建技术
对于实时性要求高的场景,可以预先构建多组描述符形成"描述符池"。当需要传输时,只需修改少量参数即可立即启动:
// 预构建描述符池 DMA_DescriptorTypeDef descPool[4] __attribute__((aligned(32))); // 使用时仅更新地址 descPool[0].SrcAddr = (uint32_t)newDataPtr;3.3 双缓冲与环形描述符
结合双缓冲技术,可以创建环形描述符链表实现持续传输:
Desc0 -> Desc1 -> Desc2 -> Desc0 (环形)配置最后一个描述符的NextDesc指向第一个描述符,并设置循环模式:
Desc[2].NextDesc = (uint32_t)&Desc[0]; hmdma.Init.TransferTriggerMode = MDMA_CIRCULAR_BUFFER;4. 实战案例:高速数据采集系统
我们在一个工业振动监测设备中应用了SG-DMA技术。系统需要同时采集8路加速度传感器数据,每路采样率10kHz,24位分辨率。原始需求如下:
- 数据需要实时FFT处理
- 原始数据需存储到SD卡
- 处理结果要通过以太网发送
传统方案性能瓶颈:
- CPU占用率达78%
- 数据丢失率约0.3%
- 系统响应延迟不稳定
采用SG-DMA优化后的架构:
- ADC使用双缓冲DMA直接采集到内存
- SG-DMA描述符配置为三向分发:
- 原始数据环形缓冲区
- FFT处理输入区
- SD卡写入缓冲区
- 每个处理环节使用独立的SG-DMA通道
优化后的关键指标对比:
| 指标 | 传统方案 | SG-DMA方案 | 提升幅度 |
|---|---|---|---|
| CPU占用率 | 78% | 32% | 59% ↓ |
| 数据丢失率 | 0.3% | 0.01% | 97% ↓ |
| 系统延迟 | 8-15ms | 2-3ms | 75% ↓ |
实现这一性能的关键代码如下:
// 三向分发描述符配置 typedef struct { DMA_DescriptorTypeDef adcToRawBuf; DMA_DescriptorTypeDef rawToFft; DMA_DescriptorTypeDef rawToSd; } TripleDesc; TripleDesc tdesc __attribute__((aligned(32))); // ADC到原始缓冲区 tdesc.adcToRawBuf.SrcAddr = (uint32_t)&hadc1.Instance->DR; tdesc.adcToRawBuf.DstAddr = (uint32_t)rawBuffer; tdesc.adcToRawBuf.NextDesc = (uint32_t)&tdesc.rawToFft; tdesc.adcToRawBuf.Control = ADC_SAMPLES * 4 | MDMA_CxCTCR_SINC_DISABLE | ...; // 原始数据到FFT输入 tdesc.rawToFft.SrcAddr = (uint32_t)rawBuffer; tdesc.rawToFft.DstAddr = (uint32_t)fftInput; tdesc.rawToFft.NextDesc = (uint32_t)&tdesc.rawToSd; tdesc.rawToFft.Control = FFT_SIZE * 4 | ...; // 原始数据到SD卡缓冲 tdesc.rawToSd.SrcAddr = (uint32_t)(rawBuffer + SD_OFFSET); tdesc.rawToSd.DstAddr = (uint32_t)sdBuffer; tdesc.rawToSd.NextDesc = 0; // 单次传输 tdesc.rawToSd.Control = SD_BLOCK_SIZE | MDMA_CxCTCR_BTIE;注意:在多核处理器中使用SG-DMA时,需要确保描述符的修改对所有核心可见。可以使用
__DSB()等内存屏障指令。
在实际调试过程中,我们发现描述符对齐问题和缓存一致性是最常见的两个陷阱。通过逻辑分析仪抓取DMA总线活动,我们优化了描述符的排列方式,使得MDMA能够以最优的方式预取描述符。