1. DMA技术概述
DMA(Direct Memory Access)直接内存访问技术是现代计算机系统中用于提高数据传输效率的核心机制。这项技术允许外设设备在不经过CPU干预的情况下,直接与系统内存进行数据交换。我第一次接触DMA是在调试STM32的UART通信时,当时发现使用传统轮询方式传输大量数据会导致CPU负载过高,而切换到DMA后系统性能立即得到显著提升。
在嵌入式系统开发中,DMA控制器就像是一个专业的数据搬运工。当我们需要在内存与外设之间传输数据时,CPU只需初始化DMA控制器,之后的数据搬运工作就完全交给DMA来完成。这种机制特别适合以下场景:
- 高速ADC采样数据的实时存储
- 大容量Flash的读写操作
- 显示屏帧缓冲区的更新
- 网络数据包的收发
注意:虽然DMA能显著降低CPU负载,但不当使用可能导致内存访问冲突或数据一致性问题。我在早期项目中就曾因为DMA和CPU同时访问同一内存区域而导致系统崩溃。
2. DMA工作原理与架构设计
2.1 DMA核心工作机制
DMA工作的基本流程可以分为三个阶段:
- 初始化阶段:CPU配置DMA控制器的源地址、目标地址、传输长度等参数
- 传输阶段:DMA控制器接管总线控制权,直接在设备和内存间搬运数据
- 完成阶段:DMA控制器通过中断通知CPU传输完成
以STM32的DMA为例,其内部包含多个独立的通道,每个通道可配置为服务特定外设。例如:
- 通道1:SPI1_RX
- 通道2:SPI1_TX
- 通道3:USART1_RX
- 通道4:USART1_TX
2.2 现代DMA架构演进
随着技术进步,DMA架构也在不断发展:
- 传统单通道DMA:如早期的PC DMA控制器
- 多通道DMA:现代MCU普遍采用(如STM32系列)
- 智能DMA:支持链表传输、数据打包等高级功能(如STM32H7系列)
- AXI DMA:用于FPGA与处理器间的高速数据传输
在Xilinx Zynq平台上,AXI DMA控制器可以实现PS(处理器系统)和PL(可编程逻辑)之间的高速数据流传输。我曾在一个图像处理项目中,使用AXI DMA实现了1080p视频流从FPGA到DDR3内存的零拷贝传输,帧率提升了近10倍。
3. 嵌入式系统中的DMA应用实践
3.1 STM32 DMA配置详解
以STM32CubeMX配置UART DMA接收为例,关键步骤如下:
- 在CubeMX中启用UART和对应的DMA通道
- 配置DMA参数:
c复制hdma_usart1_rx.Instance = DMA1_Channel5; hdma_usart1_rx.Init.Direction = DMA_PERIPH_TO_MEMORY; hdma_usart1_rx.Init.PeriphInc = DMA_PINC_DISABLE; hdma_usart1_rx.Init.MemInc = DMA_MINC_ENABLE; hdma_usart1_rx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; hdma_usart1_rx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE; hdma_usart1_rx.Init.Mode = DMA_CIRCULAR; // 循环模式 hdma_usart1_rx.Init.Priority = DMA_PRIORITY_HIGH; - 在代码中启动DMA传输:
c复制
HAL_UART_Receive_DMA(&huart1, rx_buffer, BUFFER_SIZE);
经验分享:STM32H7系列的MDMA(Master DMA)性能极为强大,支持二维传输和FIFO缓冲。在实现SD卡高速读写时,使用MDMA相比传统DMA可将吞吐量提升3倍以上。
3.2 DMA中断处理技巧
正确处理DMA中断是稳定运行的关键。常见的中断类型包括:
- 传输完成中断(TC)
- 半传输中断(HT)
- 传输错误中断(TE)
一个健壮的中断处理例程应该包含:
c复制void DMA1_Channel5_IRQHandler(void)
{
if(__HAL_DMA_GET_FLAG(&hdma_usart1_rx, DMA_FLAG_TC5)){
__HAL_DMA_CLEAR_FLAG(&hdma_usart1_rx, DMA_FLAG_TC5);
// 处理完整数据包
process_complete_packet();
}
if(__HAL_DMA_GET_FLAG(&hdma_usart1_rx, DMA_FLAG_HT5)){
__HAL_DMA_CLEAR_FLAG(&hdma_usart1_rx, DMA_FLAG_HT5);
// 处理半包数据
process_half_packet();
}
if(__HAL_DMA_GET_FLAG(&hdma_usart1_rx, DMA_FLAG_TE5)){
__HAL_DMA_CLEAR_FLAG(&hdma_usart1_rx, DMA_FLAG_TE5);
// 错误处理
handle_dma_error();
}
}
4. 高级DMA应用与性能优化
4.1 双缓冲技术实现
对于实时性要求高的应用(如音频处理),双缓冲技术可以避免数据竞争:
c复制#define BUF_SIZE 256
uint8_t dma_buf[2][BUF_SIZE];
volatile uint8_t active_buf = 0;
void DMA1_Channel5_IRQHandler(void)
{
if(__HAL_DMA_GET_FLAG(&hdma_usart1_rx, DMA_FLAG_TC5)){
__HAL_DMA_CLEAR_FLAG(&hdma_usart1_rx, DMA_FLAG_TC5);
active_buf ^= 1; // 切换缓冲区
HAL_UART_Receive_DMA(&huart1, dma_buf[active_buf], BUF_SIZE);
process_data(dma_buf[active_buf^1], BUF_SIZE);
}
}
4.2 DMA与RTOS的协同工作
在FreeRTOS中使用DMA时需要注意:
- DMA缓冲区应使用静态内存或专用内存区域
- 中断优先级需正确配置,避免影响任务调度
- 使用信号量或消息队列进行DMA完成通知
在FreeModbus DMA实现中,我采用了以下策略:
- 为每个Modbus从站分配独立的DMA通道
- 使用RTOS信号量同步DMA完成事件
- 在DMA中断中仅做标记,实际处理放在任务中
5. 常见问题与解决方案
5.1 DMA传输卡顿问题排查
当发现DMA传输导致系统卡顿时,可按以下步骤排查:
- 检查总线矩阵冲突(使用STM32CubeMX的Clock Configuration视图)
- 确认DMA通道优先级设置合理
- 检查内存访问对齐(特别是使用DMA传输结构体时)
- 验证DMA缓冲区是否位于有效内存区域
5.2 STM32 DMA只能执行一次问题
这个问题通常由以下原因导致:
- 没有正确配置循环模式(DMA_CIRCULAR)
- 传输完成后未重新启动DMA
- 中断标志未正确清除
解决方案示例:
c复制// 初始化时配置为循环模式
hdma_usart1_rx.Init.Mode = DMA_CIRCULAR;
// 或者每次传输完成后重新启动
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == USART1){
HAL_UART_Receive_DMA(huart, rx_buf, BUF_SIZE);
}
}
5.3 RS485 DMA配置要点
在使用RS485半双工通信时,DMA配置需要特别注意:
- 使能UART的TX完成回调
- 在TX完成后延迟关闭发送使能
- 配置DMA为Normal模式(非循环)
典型实现:
c复制void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart)
{
if(huart->Instance == USART2){
// 等待最后一个字节发送完成
while(!(huart->Instance->ISR & UART_FLAG_TC));
// 延迟关闭DE引脚
HAL_Delay(1);
DE_GPIO_Port->BSRR = (uint32_t)DE_Pin << 16;
}
}
6. 跨平台DMA应用案例
6.1 FPGA与ARM的DMA协同
在Xilinx Zynq平台上,AXI DMA可以实现:
- 从PL到PS的内存映射传输(MM2S)
- 从PS到PL的流模式传输(S2MM)
- 分散-聚集(Scatter-Gather)传输
典型VDMA配置流程:
- 在Vivado中添加AXI VDMA IP核
- 配置帧缓冲数量和像素格式
- 在PS端初始化DMA控制器
- 启动传输并处理中断
6.2 RK系列芯片的DMA配置
Rockchip平台的DMA配置有其特殊性:
- 需要先配置CRU(Clock and Reset Unit)
- DMA通道与具体外设绑定严格
- 可能需要手动配置burst长度
RS485 DMA配置示例(RK3399):
c复制// 配置DMA控制器
struct dma_slave_config config = {
.direction = DMA_MEM_TO_DEV,
.dst_addr = UART3_THR,
.dst_maxburst = 16,
.device_fc = false,
};
dmaengine_slave_config(dma_chan, &config);
// 准备并提交传输
struct dma_async_tx_descriptor *desc;
desc = dmaengine_prep_slave_single(dma_chan, buf, len, DMA_MEM_TO_DEV, 0);
dmaengine_submit(desc);
dma_async_issue_pending(dma_chan);
7. DMA性能优化技巧
经过多个项目的实践验证,以下技巧可显著提升DMA性能:
-
内存对齐优化:确保DMA缓冲区地址按cache行对齐(通常32或64字节)
c复制__attribute__((aligned(32))) uint8_t dma_buffer[1024]; -
缓存一致性处理:对于Cortex-M7等带cache的芯片,必须正确维护cache
c复制SCB_InvalidateDCache_by_Addr((uint32_t*)rx_buf, BUFFER_SIZE); -
传输参数调优:
- 适当增大burst长度(如从单次传输改为4字burst)
- 使用FIFO阈值控制传输节奏
- 合理设置DMA通道优先级
-
数据打包技巧:对于不完整的数据包,可以使用DMA的数据打包功能
c复制
hdma.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD; hdma.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
在最近的一个工业通信网关项目中,通过综合应用这些技巧,我们将SPI DMA传输速率从8Mbps提升到了32Mbps,同时CPU负载从45%降低到了12%。
