DMA技术实战:如何用Scatter-Gather模式提升嵌入式系统性能(附代码示例)

张开发
2026/5/5 11:25:50 15 分钟阅读

分享文章

DMA技术实战:如何用Scatter-Gather模式提升嵌入式系统性能(附代码示例)
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流参数。以下是MDMAMaster DMASTM32H7的高性能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路加速度传感器数据每路采样率10kHz24位分辨率。原始需求如下数据需要实时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-15ms2-3ms75% ↓实现这一性能的关键代码如下// 三向分发描述符配置 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能够以最优的方式预取描述符。

更多文章