跳转至

DMA直接内存访问

概述

DMA(Direct Memory Access,直接内存访问)允许外设直接与内存交换数据,无需CPU干预,大幅提高数据传输效率。

DMA优势

  1. 解放CPU:CPU只需配置DMA,然后可以做其他工作
  2. 高吞吐量:适合大批量数据传输
  3. 低延迟:硬件级数据搬运
  4. 实时性:确定性传输时间

DMA传输模式

模式 说明 适用场景
内存到内存 两个内存区域复制 数据搬运
外设到内存 外设读取数据到内存 ADC、UART接收
内存到外设 内存数据发送到外设 DAC、UART发送
外设到外设 外设间直接传输 高级应用

STM32 DMA编程

HAL库初始化

C
#include "stm32f4xx_hal.h"

DMA_HandleTypeDef hdma_usart1_rx;

void DMA_Init(void)
{
    __HAL_RCC_DMA2_CLK_ENABLE();
    
    hdma_usart1_rx.Instance = DMA2_Stream2;
    hdma_usart1_rx.Init.Channel = DMA_CHANNEL_4;
    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;
    hdma_usart1_rx.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
    
    HAL_DMA_Init(&hdma_usart1_rx);
    
    // 关联到UART
    __HAL_LINKDMA(&huart1, hdmarx, hdma_usart1_rx);
}

UART DMA接收

C
uint8_t rx_buffer[256];

void start_uart_dma_rx(void)
{
    HAL_UART_Receive_DMA(&huart1, rx_buffer, sizeof(rx_buffer));
}

// DMA接收完成回调
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
    if (huart->Instance == USART1) {
        // 处理接收数据
        process_data(rx_buffer, sizeof(rx_buffer));
        
        // 循环模式会自动重启
    }
}

// DMA半传输完成(循环模式)
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart)
{
    // 处理前半部分数据
    process_data(rx_buffer, sizeof(rx_buffer) / 2);
}

ADC DMA采样

C
uint16_t adc_buffer[1000];

void start_adc_dma(void)
{
    // 配置ADC为循环模式
    hadc1.Init.ContinuousConvMode = ENABLE;
    hadc1.Init.DMAContinuousRequests = ENABLE;
    
    // 启动ADC DMA
    HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buffer, 1000);
}

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc)
{
    // 1000次采样完成
    process_adc_data(adc_buffer, 1000);
}

内存到内存DMA

C
void mem_to_mem_dma(void)
{
    uint32_t src[256];
    uint32_t dst[256];
    
    DMA_HandleTypeDef hdma_mem;
    
    hdma_mem.Instance = DMA2_Stream0;
    hdma_mem.Init.Channel = DMA_CHANNEL_0;
    hdma_mem.Init.Direction = DMA_MEMORY_TO_MEMORY;
    hdma_mem.Init.PeriphInc = DMA_PINC_ENABLE;
    hdma_mem.Init.MemInc = DMA_MINC_ENABLE;
    hdma_mem.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD;
    hdma_mem.Init.MemDataAlignment = DMA_MDATAALIGN_WORD;
    hdma_mem.Init.Mode = DMA_NORMAL;
    hdma_mem.Init.Priority = DMA_PRIORITY_HIGH;
    
    HAL_DMA_Init(&hdma_mem);
    
    // 启动传输
    HAL_DMA_Start(&hdma_mem, (uint32_t)src, (uint32_t)dst, 256);
    
    // 等待完成
    HAL_DMA_PollForTransfer(&hdma_mem, HAL_DMA_FULL_TRANSFER, 1000);
}

Linux DMA编程

DMA缓冲区分配

C
#include <linux/dma-mapping.h>

// 一致性DMA缓冲区
void *vaddr;
dma_addr_t dma_handle;

vaddr = dma_alloc_coherent(dev, size, &dma_handle, GFP_KERNEL);
// vaddr: CPU访问的虚拟地址
// dma_handle: DMA控制器使用的物理地址

// 使用完成后释放
dma_free_coherent(dev, size, vaddr, dma_handle);

流式DMA映射

C
// 映射单次传输缓冲区
dma_addr_t dma_handle;

// 外设读取内存(CPU写入 -> DMA读取 -> 外设)
dma_handle = dma_map_single(dev, vaddr, size, DMA_TO_DEVICE);

// 等待DMA完成
dma_unmap_single(dev, dma_handle, size, DMA_TO_DEVICE);

// 外设写入内存(外设 -> DMA写入 -> CPU读取)
dma_handle = dma_map_single(dev, vaddr, size, DMA_FROM_DEVICE);
dma_unmap_single(dev, dma_handle, size, DMA_FROM_DEVICE);

// 双向传输
dma_handle = dma_map_single(dev, vaddr, size, DMA_BIDIRECTIONAL);

缓存同步

C
1
2
3
4
5
// CPU访问前同步
dma_sync_single_for_cpu(dev, dma_handle, size, DMA_FROM_DEVICE);

// DMA传输前同步
dma_sync_single_for_device(dev, dma_handle, size, DMA_TO_DEVICE);

Scatter-Gather DMA

C
#include <linux/dmapool.h>

struct scatterlist sg[10];
int nents;

// 设置scatter-gather列表
sg_init_table(sg, 10);
for (i = 0; i < 10; i++) {
    sg_set_buf(&sg[i], buffers[i], sizes[i]);
}

// 映射
nents = dma_map_sg(dev, sg, 10, DMA_TO_DEVICE);

// 取消映射
dma_unmap_sg(dev, sg, nents, DMA_TO_DEVICE);

DMA配置参数

通道/流选择

C
1
2
3
4
5
6
// STM32F4系列
DMA1: Stream 0-7
DMA2: Stream 0-7

// 每个流可配置不同通道
hdma.Init.Channel = DMA_CHANNEL_0;  // 通道0-7

优先级

C
1
2
3
4
hdma.Init.Priority = DMA_PRIORITY_LOW;      // 低优先级
hdma.Init.Priority = DMA_PRIORITY_MEDIUM;   // 中优先级
hdma.Init.Priority = DMA_PRIORITY_HIGH;     // 高优先级
hdma.Init.Priority = DMA_PRIORITY_VERY_HIGH; // 最高优先级

传输模式

C
1
2
3
4
5
6
7
8
// 正常模式:传输完成后停止
hdma.Init.Mode = DMA_NORMAL;

// 循环模式:传输完成后自动重启
hdma.Init.Mode = DMA_CIRCULAR;

// 双缓冲模式
hdma.Init.Mode = DMA_DOUBLE_BUFFER;

数据宽度和对齐

C
1
2
3
4
5
6
7
8
9
// 外设数据宽度
hdma.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;   // 8位
hdma.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; // 16位
hdma.Init.PeriphDataAlignment = DMA_PDATAALIGN_WORD;   // 32位

// 内存数据宽度
hdma.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
hdma.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
hdma.Init.MemDataAlignment = DMA_MDATAALIGN_WORD;

地址递增

C
1
2
3
4
5
6
7
// 外设地址递增(通常禁用)
hdma.Init.PeriphInc = DMA_PINC_DISABLE;  // 外设寄存器固定
hdma.Init.PeriphInc = DMA_PINC_ENABLE;   // 外设地址递增

// 内存地址递增(通常启用)
hdma.Init.MemInc = DMA_MINC_ENABLE;      // 内存地址递增
hdma.Init.MemInc = DMA_MINC_DISABLE;     // 内存地址固定

DMA中断

中断类型

C
1
2
3
4
5
6
7
8
// 传输完成中断
void HAL_DMA_XferCpltCallback(DMA_HandleTypeDef *hdma);

// 半传输完成中断
void HAL_DMA_XferHalfCpltCallback(DMA_HandleTypeDef *hdma);

// 错误中断
void HAL_DMA_XferErrorCallback(DMA_HandleTypeDef *hdma);

使能中断

C
1
2
3
4
// 使能DMA中断
__HAL_DMA_ENABLE_IT(&hdma, DMA_IT_TC);   // 传输完成
__HAL_DMA_ENABLE_IT(&hdma, DMA_IT_HT);   // 半传输
__HAL_DMA_ENABLE_IT(&hdma, DMA_IT_TE);   // 传输错误

DMA调试

查看DMA状态

C
1
2
3
4
5
6
7
// 检查DMA是否忙
if (HAL_DMA_GetState(&hdma) == HAL_DMA_STATE_BUSY) {
    // DMA正在传输
}

// 获取剩余数据量
uint32_t remaining = __HAL_DMA_GET_COUNTER(&hdma);

常见问题

  1. 数据错位
  2. 检查数据宽度配置
  3. 检查地址对齐

  4. 传输不完整

  5. 检查传输长度配置
  6. 检查是否被其他中断打断

  7. 缓冲区溢出

  8. 使用循环模式时注意处理速度
  9. 考虑双缓冲模式

参考资料