BLOG8
跳转至

DMA驱动开发:高效数据传输

概述

DMA(Direct Memory Access,直接内存访问)是一种无需CPU干预即可在内存和外设之间传输数据的技术。通过DMA,可以大幅提高数据传输效率,释放CPU资源用于其他任务,是高性能嵌入式系统的关键技术。

本教程将深入讲解DMA的工作原理和驱动开发方法,通过实战项目带你掌握DMA在UART、SPI、ADC等外设中的应用。

完成本教程后,你将能够:

  • 理解DMA的工作原理和硬件架构
  • 掌握DMA控制器的配置方法
  • 实现DMA在各种外设中的应用
  • 处理DMA传输中断和错误
  • 管理多个DMA通道
  • 优化DMA传输性能

背景知识

DMA的工作原理

传统的数据传输方式需要CPU参与每个字节的搬运,而DMA可以独立完成数据传输。

传统方式 vs DMA方式:

传统方式(CPU搬运):
外设 -> CPU读取 -> CPU写入 -> 内存
优点:简单直接
缺点:占用CPU,效率低

DMA方式(硬件搬运):
外设 <--DMA控制器--> 内存
优点:不占用CPU,效率高
缺点:配置复杂,需要硬件支持

DMA传输过程:

sequenceDiagram
    participant CPU
    participant DMA
    participant 外设
    participant 内存

    CPU->>DMA: 1. 配置DMA参数
    CPU->>DMA: 2. 启动DMA传输
    DMA->>外设: 3. 请求数据
    外设->>DMA: 4. 提供数据
    DMA->>内存: 5. 写入内存
    Note over DMA,内存: 重复步骤3-5
    DMA->>CPU: 6. 传输完成中断

STM32 DMA架构

STM32F4系列有两个DMA控制器(DMA1和DMA2),每个控制器有8个数据流(Stream)。

DMA控制器架构:

DMA1: 8个Stream (Stream0-7)
DMA2: 8个Stream (Stream0-7)

每个Stream可以配置为8个通道(Channel)之一
每个Stream有独立的:
- 源地址
- 目标地址
- 数据量
- 传输模式
- 优先级

DMA请求映射:

外设 DMA控制器 Stream Channel
USART1_TX DMA2 Stream7 Channel4
USART1_RX DMA2 Stream5 Channel4
SPI1_TX DMA2 Stream3 Channel3
SPI1_RX DMA2 Stream2 Channel3
ADC1 DMA2 Stream0 Channel0
TIM1_CH1 DMA2 Stream1 Channel6

DMA传输模式

DMA支持多种传输模式,适应不同的应用场景。

1. 传输方向: - 外设到内存(Peripheral to Memory) - 内存到外设(Memory to Peripheral) - 内存到内存(Memory to Memory)

2. 传输模式: - 普通模式(Normal Mode):传输完成后停止 - 循环模式(Circular Mode):传输完成后自动重新开始

3. 数据宽度: - 字节(8位) - 半字(16位) - 字(32位)

4. 地址递增: - 固定地址:适用于外设寄存器 - 递增地址:适用于内存缓冲区

DMA寄存器概览

STM32 DMA的主要寄存器:

寄存器 全称 功能
SxCR Stream x Configuration Register 流配置寄存器
SxNDTR Stream x Number of Data Register 数据传输数量
SxPAR Stream x Peripheral Address Register 外设地址
SxM0AR Stream x Memory 0 Address Register 内存地址0
SxM1AR Stream x Memory 1 Address Register 内存地址1(双缓冲)
SxFCR Stream x FIFO Control Register FIFO控制
LISR/HISR Low/High Interrupt Status Register 中断状态
LIFCR/HIFCR Low/High Interrupt Flag Clear Register 中断标志清除

环境准备

硬件要求

  • STM32F4系列开发板(如STM32F407VET6)
  • USB转TTL串口模块
  • 逻辑分析仪(可选,用于观察DMA传输)
  • 示波器(可选,用于性能测试)

软件要求

  • Keil MDK 5.x 或 STM32CubeIDE
  • STM32F4 HAL库或标准外设库
  • 串口调试工具

硬件连接

基本连接(UART DMA测试):
- PA9  (USART1_TX) ---> USB转TTL RX
- PA10 (USART1_RX) ---> USB转TTL TX
- GND              ---> GND

ADC DMA测试(可选):
- PA0 (ADC1_IN0) ---> 可变电阻或信号源

核心内容

步骤1:DMA基本配置

首先学习如何配置DMA的基本参数。

#include "stm32f4xx.h"

/**
 * @brief  DMA基本配置结构体
 */
typedef struct {
    DMA_Stream_TypeDef *stream;     // DMA流
    uint32_t channel;                // 通道号(0-7)
    uint32_t direction;              // 传输方向
    uint32_t periph_addr;            // 外设地址
    uint32_t memory_addr;            // 内存地址
    uint32_t data_size;              // 数据量
    uint32_t periph_data_width;      // 外设数据宽度
    uint32_t memory_data_width;      // 内存数据宽度
    uint32_t mode;                   // 传输模式(普通/循环)
    uint32_t priority;               // 优先级
    uint8_t  periph_inc;             // 外设地址递增
    uint8_t  memory_inc;             // 内存地址递增
} DMA_Config_t;

/**
 * @brief  使能DMA时钟
 * @param  dma: DMA1或DMA2
 * @retval 无
 */
void DMA_ClockEnable(DMA_TypeDef *dma) {
    if (dma == DMA1) {
        RCC->AHB1ENR |= RCC_AHB1ENR_DMA1EN;
    } else if (dma == DMA2) {
        RCC->AHB1ENR |= RCC_AHB1ENR_DMA2EN;
    }
}

/**
 * @brief  配置DMA流
 * @param  config: DMA配置结构体指针
 * @retval 无
 */
void DMA_StreamConfig(DMA_Config_t *config) {
    DMA_Stream_TypeDef *stream = config->stream;

    // 1. 禁用DMA流(配置前必须禁用)
    stream->CR &= ~DMA_SxCR_EN;
    while (stream->CR & DMA_SxCR_EN);  // 等待禁用完成

    // 2. 配置通道号
    stream->CR &= ~DMA_SxCR_CHSEL;
    stream->CR |= (config->channel << 25);

    // 3. 配置传输方向
    stream->CR &= ~DMA_SxCR_DIR;
    stream->CR |= config->direction;

    // 4. 配置数据宽度
    stream->CR &= ~(DMA_SxCR_PSIZE | DMA_SxCR_MSIZE);
    stream->CR |= (config->periph_data_width << 11);  // 外设数据宽度
    stream->CR |= (config->memory_data_width << 13);  // 内存数据宽度

    // 5. 配置地址递增
    if (config->periph_inc) {
        stream->CR |= DMA_SxCR_PINC;
    } else {
        stream->CR &= ~DMA_SxCR_PINC;
    }

    if (config->memory_inc) {
        stream->CR |= DMA_SxCR_MINC;
    } else {
        stream->CR &= ~DMA_SxCR_MINC;
    }

    // 6. 配置传输模式
    if (config->mode == DMA_SxCR_CIRC) {
        stream->CR |= DMA_SxCR_CIRC;  // 循环模式
    } else {
        stream->CR &= ~DMA_SxCR_CIRC;  // 普通模式
    }

    // 7. 配置优先级
    stream->CR &= ~DMA_SxCR_PL;
    stream->CR |= config->priority;

    // 8. 配置地址
    stream->PAR = config->periph_addr;   // 外设地址
    stream->M0AR = config->memory_addr;  // 内存地址

    // 9. 配置数据量
    stream->NDTR = config->data_size;
}

/**
 * @brief  启动DMA传输
 * @param  stream: DMA流指针
 * @retval 无
 */
void DMA_Start(DMA_Stream_TypeDef *stream) {
    stream->CR |= DMA_SxCR_EN;
}

/**
 * @brief  停止DMA传输
 * @param  stream: DMA流指针
 * @retval 无
 */
void DMA_Stop(DMA_Stream_TypeDef *stream) {
    stream->CR &= ~DMA_SxCR_EN;
    while (stream->CR & DMA_SxCR_EN);
}

配置参数说明:

  1. 传输方向:
  2. 0b00:外设到内存
  3. 0b01:内存到外设
  4. 0b10:内存到内存

  5. 数据宽度:

  6. 0b00:字节(8位)
  7. 0b01:半字(16位)
  8. 0b10:字(32位)

  9. 优先级:

  10. 0b00:低
  11. 0b01:中
  12. 0b10:高
  13. 0b11:非常高

步骤2:DMA中断配置

DMA支持多种中断,用于通知传输状态。

/**
 * @brief  DMA中断类型
 */
typedef enum {
    DMA_IT_TC   = DMA_SxCR_TCIE,   // 传输完成中断
    DMA_IT_HT   = DMA_SxCR_HTIE,   // 半传输中断
    DMA_IT_TE   = DMA_SxCR_TEIE,   // 传输错误中断
    DMA_IT_DME  = DMA_SxCR_DMEIE   // 直接模式错误中断
} DMA_IT_Type;

/**
 * @brief  使能DMA中断
 * @param  stream: DMA流指针
 * @param  it_type: 中断类型
 * @retval 无
 */
void DMA_EnableIT(DMA_Stream_TypeDef *stream, DMA_IT_Type it_type) {
    stream->CR |= it_type;
}

/**
 * @brief  禁用DMA中断
 * @param  stream: DMA流指针
 * @param  it_type: 中断类型
 * @retval 无
 */
void DMA_DisableIT(DMA_Stream_TypeDef *stream, DMA_IT_Type it_type) {
    stream->CR &= ~it_type;
}

/**
 * @brief  配置DMA NVIC中断
 * @param  irqn: 中断号
 * @param  priority: 优先级
 * @retval 无
 */
void DMA_NVIC_Config(IRQn_Type irqn, uint32_t priority) {
    NVIC_SetPriority(irqn, priority);
    NVIC_EnableIRQ(irqn);
}

/**
 * @brief  获取DMA中断标志
 * @param  dma: DMA1或DMA2
 * @param  stream_num: 流编号(0-7)
 * @retval 中断标志
 */
uint32_t DMA_GetITStatus(DMA_TypeDef *dma, uint8_t stream_num) {
    uint32_t flags = 0;

    if (stream_num < 4) {
        // Stream 0-3: 使用LISR
        flags = dma->LISR >> (stream_num * 6);
    } else {
        // Stream 4-7: 使用HISR
        flags = dma->HISR >> ((stream_num - 4) * 6);
    }

    return flags & 0x3F;  // 6位标志
}

/**
 * @brief  清除DMA中断标志
 * @param  dma: DMA1或DMA2
 * @param  stream_num: 流编号(0-7)
 * @param  flags: 要清除的标志
 * @retval 无
 */
void DMA_ClearITFlags(DMA_TypeDef *dma, uint8_t stream_num, uint32_t flags) {
    if (stream_num < 4) {
        // Stream 0-3: 使用LIFCR
        dma->LIFCR = (flags & 0x3F) << (stream_num * 6);
    } else {
        // Stream 4-7: 使用HIFCR
        dma->HIFCR = (flags & 0x3F) << ((stream_num - 4) * 6);
    }
}

// DMA中断标志位定义
#define DMA_FLAG_FEIF   (1 << 0)  // FIFO错误
#define DMA_FLAG_DMEIF  (1 << 2)  // 直接模式错误
#define DMA_FLAG_TEIF   (1 << 3)  // 传输错误
#define DMA_FLAG_HTIF   (1 << 4)  // 半传输完成
#define DMA_FLAG_TCIF   (1 << 5)  // 传输完成

/**
 * @brief  DMA2 Stream7中断服务函数示例(USART1 TX)
 * @param  无
 * @retval 无
 */
void DMA2_Stream7_IRQHandler(void) {
    // 检查传输完成标志
    if (DMA_GetITStatus(DMA2, 7) & DMA_FLAG_TCIF) {
        // 清除标志
        DMA_ClearITFlags(DMA2, 7, DMA_FLAG_TCIF);

        // 传输完成处理
        // ...
    }

    // 检查传输错误标志
    if (DMA_GetITStatus(DMA2, 7) & DMA_FLAG_TEIF) {
        // 清除标志
        DMA_ClearITFlags(DMA2, 7, DMA_FLAG_TEIF);

        // 错误处理
        // ...
    }
}

中断标志说明:

标志 说明 处理方法
TCIF 传输完成 清除标志,执行后续操作
HTIF 半传输完成 用于双缓冲处理
TEIF 传输错误 检查配置,重新启动
DMEIF 直接模式错误 检查FIFO配置
FEIF FIFO错误 检查FIFO阈值

步骤3:UART DMA发送

实现UART的DMA发送功能,提高串口发送效率。

/**
 * @brief  初始化UART1 DMA发送
 * @param  无
 * @retval 无
 */
void UART1_DMA_TX_Init(void) {
    // 1. 使能DMA2时钟
    DMA_ClockEnable(DMA2);

    // 2. 配置DMA参数
    DMA_Config_t dma_config = {
        .stream = DMA2_Stream7,
        .channel = 4,  // Channel 4 for USART1_TX
        .direction = (1 << 6),  // 内存到外设
        .periph_addr = (uint32_t)&USART1->DR,
        .memory_addr = 0,  // 稍后设置
        .data_size = 0,    // 稍后设置
        .periph_data_width = 0,  // 字节
        .memory_data_width = 0,  // 字节
        .mode = 0,  // 普通模式
        .priority = (1 << 16),  // 中等优先级
        .periph_inc = 0,  // 外设地址不递增
        .memory_inc = 1   // 内存地址递增
    };

    DMA_StreamConfig(&dma_config);

    // 3. 使能传输完成中断
    DMA_EnableIT(DMA2_Stream7, DMA_IT_TC);

    // 4. 配置NVIC
    DMA_NVIC_Config(DMA2_Stream7_IRQn, 2);

    // 5. 使能USART1的DMA发送
    USART1->CR3 |= USART_CR3_DMAT;
}

/**
 * @brief  使用DMA发送数据
 * @param  data: 数据指针
 * @param  length: 数据长度
 * @retval 0=成功,1=DMA忙
 */
uint8_t UART1_DMA_Send(const uint8_t *data, uint16_t length) {
    // 检查DMA是否空闲
    if (DMA2_Stream7->CR & DMA_SxCR_EN) {
        return 1;  // DMA忙
    }

    // 清除所有标志
    DMA_ClearITFlags(DMA2, 7, 0x3F);

    // 配置内存地址和数据量
    DMA2_Stream7->M0AR = (uint32_t)data;
    DMA2_Stream7->NDTR = length;

    // 启动DMA传输
    DMA_Start(DMA2_Stream7);

    return 0;  // 成功
}

/**
 * @brief  检查DMA发送是否完成
 * @param  无
 * @retval 1=完成,0=未完成
 */
uint8_t UART1_DMA_IsTxComplete(void) {
    return !(DMA2_Stream7->CR & DMA_SxCR_EN);
}

/**
 * @brief  等待DMA发送完成
 * @param  timeout: 超时时间(毫秒)
 * @retval 0=成功,1=超时
 */
uint8_t UART1_DMA_WaitTxComplete(uint32_t timeout) {
    uint32_t start = GetTick();  // 获取当前时间

    while (DMA2_Stream7->CR & DMA_SxCR_EN) {
        if (GetTick() - start > timeout) {
            return 1;  // 超时
        }
    }

    return 0;  // 成功
}

// 全局变量
volatile uint8_t g_uart_dma_tx_complete = 0;

/**
 * @brief  DMA2 Stream7中断服务函数(USART1 TX)
 * @param  无
 * @retval 无
 */
void DMA2_Stream7_IRQHandler(void) {
    if (DMA_GetITStatus(DMA2, 7) & DMA_FLAG_TCIF) {
        // 清除传输完成标志
        DMA_ClearITFlags(DMA2, 7, DMA_FLAG_TCIF);

        // 设置完成标志
        g_uart_dma_tx_complete = 1;
    }
}

使用示例:

int main(void) {
    SystemInit();
    UART1_Init(115200);
    UART1_DMA_TX_Init();

    const char *message = "Hello, DMA!\r\n";

    while (1) {
        // 发送数据
        if (UART1_DMA_Send((uint8_t*)message, strlen(message)) == 0) {
            // 等待发送完成
            while (!g_uart_dma_tx_complete);
            g_uart_dma_tx_complete = 0;

            // 延时
            Delay_Ms(1000);
        }
    }
}

步骤4:UART DMA接收

实现UART的DMA接收功能,结合IDLE中断实现不定长数据接收。

#define UART_DMA_RX_BUFFER_SIZE 256

uint8_t uart_dma_rx_buffer[UART_DMA_RX_BUFFER_SIZE];
volatile uint16_t uart_rx_length = 0;
volatile uint8_t uart_rx_complete = 0;

/**
 * @brief  初始化UART1 DMA接收
 * @param  无
 * @retval 无
 */
void UART1_DMA_RX_Init(void) {
    // 1. 使能DMA2时钟
    DMA_ClockEnable(DMA2);

    // 2. 配置DMA参数
    DMA_Config_t dma_config = {
        .stream = DMA2_Stream5,
        .channel = 4,  // Channel 4 for USART1_RX
        .direction = 0,  // 外设到内存
        .periph_addr = (uint32_t)&USART1->DR,
        .memory_addr = (uint32_t)uart_dma_rx_buffer,
        .data_size = UART_DMA_RX_BUFFER_SIZE,
        .periph_data_width = 0,  // 字节
        .memory_data_width = 0,  // 字节
        .mode = DMA_SxCR_CIRC,  // 循环模式
        .priority = (2 << 16),  // 高优先级
        .periph_inc = 0,  // 外设地址不递增
        .memory_inc = 1   // 内存地址递增
    };

    DMA_StreamConfig(&dma_config);

    // 3. 使能USART1的DMA接收
    USART1->CR3 |= USART_CR3_DMAR;

    // 4. 使能USART1的IDLE中断
    USART1->CR1 |= USART_CR1_IDLEIE;
    NVIC_EnableIRQ(USART1_IRQn);

    // 5. 启动DMA接收
    DMA_Start(DMA2_Stream5);
}

/**
 * @brief  获取DMA接收的数据量
 * @param  无
 * @retval 已接收的字节数
 */
uint16_t UART1_DMA_GetRxCount(void) {
    return UART_DMA_RX_BUFFER_SIZE - DMA2_Stream5->NDTR;
}

/**
 * @brief  USART1中断服务函数(IDLE中断)
 * @param  无
 * @retval 无
 */
void USART1_IRQHandler(void) {
    if (USART1->SR & USART_SR_IDLE) {
        // 清除IDLE标志(读SR后读DR)
        volatile uint32_t temp = USART1->SR;
        temp = USART1->DR;
        (void)temp;

        // 获取接收到的数据量
        uart_rx_length = UART1_DMA_GetRxCount();
        uart_rx_complete = 1;

        // 重启DMA接收
        DMA_Stop(DMA2_Stream5);
        DMA2_Stream5->NDTR = UART_DMA_RX_BUFFER_SIZE;
        DMA_Start(DMA2_Stream5);
    }
}

使用示例:

int main(void) {
    SystemInit();
    UART1_Init(115200);
    UART1_DMA_RX_Init();

    printf("UART DMA RX Test\r\n");
    printf("Send data...\r\n");

    while (1) {
        if (uart_rx_complete) {
            // 处理接收到的数据
            printf("Received %d bytes: ", uart_rx_length);
            for (int i = 0; i < uart_rx_length; i++) {
                printf("%02X ", uart_dma_rx_buffer[i]);
            }
            printf("\r\n");

            // 重置标志
            uart_rx_complete = 0;
        }
    }
}

步骤5:ADC DMA采集

使用DMA实现ADC的连续采集,无需CPU干预。

#define ADC_CHANNEL_COUNT 4
#define ADC_SAMPLE_COUNT 100

uint16_t adc_dma_buffer[ADC_CHANNEL_COUNT * ADC_SAMPLE_COUNT];
volatile uint8_t adc_conversion_complete = 0;

/**
 * @brief  初始化ADC1
 * @param  无
 * @retval 无
 */
void ADC1_Init(void) {
    // 1. 使能ADC1时钟
    RCC->APB2ENR |= RCC_APB2ENR_ADC1EN;

    // 2. 配置ADC参数
    ADC1->CR1 = 0;
    ADC1->CR1 |= ADC_CR1_SCAN;  // 扫描模式

    ADC1->CR2 = 0;
    ADC1->CR2 |= ADC_CR2_CONT;  // 连续转换
    ADC1->CR2 |= ADC_CR2_DMA;   // 使能DMA
    ADC1->CR2 |= ADC_CR2_DDS;   // DMA连续请求

    // 3. 配置采样时间(所有通道)
    ADC1->SMPR2 = 0x00249249;  // 84个周期

    // 4. 配置规则序列
    ADC1->SQR1 = (ADC_CHANNEL_COUNT - 1) << 20;  // 4个通道
    ADC1->SQR3 = (0 << 0) | (1 << 5) | (2 << 10) | (3 << 15);  // CH0-3

    // 5. 使能ADC
    ADC1->CR2 |= ADC_CR2_ADON;
}

/**
 * @brief  初始化ADC1 DMA
 * @param  无
 * @retval 无
 */
void ADC1_DMA_Init(void) {
    // 1. 使能DMA2时钟
    DMA_ClockEnable(DMA2);

    // 2. 配置DMA参数
    DMA_Config_t dma_config = {
        .stream = DMA2_Stream0,
        .channel = 0,  // Channel 0 for ADC1
        .direction = 0,  // 外设到内存
        .periph_addr = (uint32_t)&ADC1->DR,
        .memory_addr = (uint32_t)adc_dma_buffer,
        .data_size = ADC_CHANNEL_COUNT * ADC_SAMPLE_COUNT,
        .periph_data_width = (1 << 11),  // 半字(16位)
        .memory_data_width = (1 << 13),  // 半字(16位)
        .mode = DMA_SxCR_CIRC,  // 循环模式
        .priority = (2 << 16),  // 高优先级
        .periph_inc = 0,  // 外设地址不递增
        .memory_inc = 1   // 内存地址递增
    };

    DMA_StreamConfig(&dma_config);

    // 3. 使能传输完成中断
    DMA_EnableIT(DMA2_Stream0, DMA_IT_TC);

    // 4. 配置NVIC
    DMA_NVIC_Config(DMA2_Stream0_IRQn, 2);

    // 5. 启动DMA
    DMA_Start(DMA2_Stream0);
}

/**
 * @brief  启动ADC转换
 * @param  无
 * @retval 无
 */
void ADC1_Start(void) {
    ADC1->CR2 |= ADC_CR2_SWSTART;
}

/**
 * @brief  DMA2 Stream0中断服务函数(ADC1)
 * @param  无
 * @retval 无
 */
void DMA2_Stream0_IRQHandler(void) {
    if (DMA_GetITStatus(DMA2, 0) & DMA_FLAG_TCIF) {
        // 清除传输完成标志
        DMA_ClearITFlags(DMA2, 0, DMA_FLAG_TCIF);

        // 设置完成标志
        adc_conversion_complete = 1;
    }
}

/**
 * @brief  计算ADC平均值
 * @param  channel: 通道号(0-3)
 * @retval 平均值
 */
uint16_t ADC_GetAverage(uint8_t channel) {
    uint32_t sum = 0;

    for (int i = 0; i < ADC_SAMPLE_COUNT; i++) {
        sum += adc_dma_buffer[i * ADC_CHANNEL_COUNT + channel];
    }

    return sum / ADC_SAMPLE_COUNT;
}

使用示例:

int main(void) {
    SystemInit();
    UART1_Init(115200);
    ADC1_Init();
    ADC1_DMA_Init();
    ADC1_Start();

    printf("ADC DMA Test\r\n");

    while (1) {
        if (adc_conversion_complete) {
            adc_conversion_complete = 0;

            // 计算并显示各通道平均值
            for (int ch = 0; ch < ADC_CHANNEL_COUNT; ch++) {
                uint16_t avg = ADC_GetAverage(ch);
                float voltage = avg * 3.3f / 4096.0f;
                printf("CH%d: %d (%.2fV)  ", ch, avg, voltage);
            }
            printf("\r\n");

            Delay_Ms(500);
        }
    }
}

步骤6:内存到内存DMA传输

DMA还可以用于内存之间的快速数据拷贝。

/**
 * @brief  初始化内存到内存DMA
 * @param  无
 * @retval 无
 */
void DMA_MemToMem_Init(void) {
    // 使能DMA2时钟
    DMA_ClockEnable(DMA2);

    // 注意:内存到内存传输只能使用DMA2
}

/**
 * @brief  内存到内存DMA传输
 * @param  src: 源地址
 * @param  dst: 目标地址
 * @param  size: 数据量(字节数)
 * @retval 无
 */
void DMA_MemCopy(const void *src, void *dst, uint32_t size) {
    // 1. 禁用DMA流
    DMA_Stop(DMA2_Stream0);

    // 2. 配置DMA参数
    DMA2_Stream0->CR = 0;
    DMA2_Stream0->CR |= (0 << 25);  // Channel 0
    DMA2_Stream0->CR |= (2 << 6);   // 内存到内存
    DMA2_Stream0->CR |= (0 << 11);  // 外设数据宽度:字节
    DMA2_Stream0->CR |= (0 << 13);  // 内存数据宽度:字节
    DMA2_Stream0->CR |= DMA_SxCR_PINC;  // 源地址递增
    DMA2_Stream0->CR |= DMA_SxCR_MINC;  // 目标地址递增
    DMA2_Stream0->CR |= (3 << 16);  // 非常高优先级

    // 3. 配置地址和数据量
    DMA2_Stream0->PAR = (uint32_t)src;   // 源地址
    DMA2_Stream0->M0AR = (uint32_t)dst;  // 目标地址
    DMA2_Stream0->NDTR = size;

    // 4. 启动DMA传输
    DMA_Start(DMA2_Stream0);

    // 5. 等待传输完成
    while (DMA2_Stream0->CR & DMA_SxCR_EN);
}

/**
 * @brief  快速内存拷贝(32位对齐)
 * @param  src: 源地址(必须4字节对齐)
 * @param  dst: 目标地址(必须4字节对齐)
 * @param  size: 数据量(字节数,必须是4的倍数)
 * @retval 无
 */
void DMA_MemCopy32(const void *src, void *dst, uint32_t size) {
    // 检查对齐
    if (((uint32_t)src % 4) || ((uint32_t)dst % 4) || (size % 4)) {
        // 不对齐,使用字节拷贝
        DMA_MemCopy(src, dst, size);
        return;
    }

    // 配置为32位传输
    DMA_Stop(DMA2_Stream0);

    DMA2_Stream0->CR = 0;
    DMA2_Stream0->CR |= (0 << 25);  // Channel 0
    DMA2_Stream0->CR |= (2 << 6);   // 内存到内存
    DMA2_Stream0->CR |= (2 << 11);  // 外设数据宽度:字(32位)
    DMA2_Stream0->CR |= (2 << 13);  // 内存数据宽度:字(32位)
    DMA2_Stream0->CR |= DMA_SxCR_PINC;
    DMA2_Stream0->CR |= DMA_SxCR_MINC;
    DMA2_Stream0->CR |= (3 << 16);

    DMA2_Stream0->PAR = (uint32_t)src;
    DMA2_Stream0->M0AR = (uint32_t)dst;
    DMA2_Stream0->NDTR = size / 4;  // 字数量

    DMA_Start(DMA2_Stream0);
    while (DMA2_Stream0->CR & DMA_SxCR_EN);
}

性能对比测试:

#define TEST_SIZE 1024

uint8_t src_buffer[TEST_SIZE];
uint8_t dst_buffer[TEST_SIZE];

void TestMemCopyPerformance(void) {
    uint32_t start, end;

    // 初始化源数据
    for (int i = 0; i < TEST_SIZE; i++) {
        src_buffer[i] = i & 0xFF;
    }

    // 测试1:标准memcpy
    start = GetTick();
    memcpy(dst_buffer, src_buffer, TEST_SIZE);
    end = GetTick();
    printf("memcpy: %d us\r\n", end - start);

    // 测试2:DMA拷贝(字节)
    start = GetTick();
    DMA_MemCopy(src_buffer, dst_buffer, TEST_SIZE);
    end = GetTick();
    printf("DMA (byte): %d us\r\n", end - start);

    // 测试3:DMA拷贝(字)
    start = GetTick();
    DMA_MemCopy32(src_buffer, dst_buffer, TEST_SIZE);
    end = GetTick();
    printf("DMA (word): %d us\r\n", end - start);
}

实践示例

示例1:DMA双缓冲接收

使用双缓冲模式实现连续数据接收,避免数据丢失。

#define BUFFER_SIZE 512

uint8_t rx_buffer0[BUFFER_SIZE];
uint8_t rx_buffer1[BUFFER_SIZE];
volatile uint8_t current_buffer = 0;
volatile uint8_t buffer_ready = 0;

/**
 * @brief  初始化DMA双缓冲接收
 * @param  无
 * @retval 无
 */
void UART1_DMA_DoubleBuffer_Init(void) {
    DMA_ClockEnable(DMA2);

    // 配置DMA
    DMA2_Stream5->CR = 0;
    DMA2_Stream5->CR |= (4 << 25);  // Channel 4
    DMA2_Stream5->CR |= (0 << 6);   // 外设到内存
    DMA2_Stream5->CR |= DMA_SxCR_MINC;
    DMA2_Stream5->CR |= DMA_SxCR_CIRC;
    DMA2_Stream5->CR |= DMA_SxCR_DBM;  // 双缓冲模式

    // 配置地址
    DMA2_Stream5->PAR = (uint32_t)&USART1->DR;
    DMA2_Stream5->M0AR = (uint32_t)rx_buffer0;
    DMA2_Stream5->M1AR = (uint32_t)rx_buffer1;
    DMA2_Stream5->NDTR = BUFFER_SIZE;

    // 使能传输完成中断
    DMA_EnableIT(DMA2_Stream5, DMA_IT_TC);
    DMA_NVIC_Config(DMA2_Stream5_IRQn, 2);

    // 使能USART DMA
    USART1->CR3 |= USART_CR3_DMAR;

    // 启动DMA
    DMA_Start(DMA2_Stream5);
}

/**
 * @brief  DMA2 Stream5中断服务函数
 * @param  无
 * @retval 无
 */
void DMA2_Stream5_IRQHandler(void) {
    if (DMA_GetITStatus(DMA2, 5) & DMA_FLAG_TCIF) {
        DMA_ClearITFlags(DMA2, 5, DMA_FLAG_TCIF);

        // 检查当前使用的缓冲区
        if (DMA2_Stream5->CR & DMA_SxCR_CT) {
            current_buffer = 1;  // 正在使用buffer1,buffer0已满
        } else {
            current_buffer = 0;  // 正在使用buffer0,buffer1已满
        }

        buffer_ready = 1;
    }
}

/**
 * @brief  主函数 - 双缓冲接收
 */
int main(void) {
    SystemInit();
    UART1_Init(115200);
    UART1_DMA_DoubleBuffer_Init();

    while (1) {
        if (buffer_ready) {
            buffer_ready = 0;

            // 处理已满的缓冲区
            uint8_t *buffer = (current_buffer == 0) ? rx_buffer1 : rx_buffer0;

            printf("Buffer %d received %d bytes\r\n", 
                   (current_buffer == 0) ? 1 : 0, BUFFER_SIZE);

            // 处理数据...
        }
    }
}

示例2:SPI DMA全双工通信

使用DMA实现SPI的高速全双工通信。

#define SPI_BUFFER_SIZE 256

uint8_t spi_tx_buffer[SPI_BUFFER_SIZE];
uint8_t spi_rx_buffer[SPI_BUFFER_SIZE];
volatile uint8_t spi_transfer_complete = 0;

/**
 * @brief  初始化SPI1 DMA
 * @param  无
 * @retval 无
 */
void SPI1_DMA_Init(void) {
    DMA_ClockEnable(DMA2);

    // 配置DMA TX (Stream3, Channel3)
    DMA_Config_t tx_config = {
        .stream = DMA2_Stream3,
        .channel = 3,
        .direction = (1 << 6),  // 内存到外设
        .periph_addr = (uint32_t)&SPI1->DR,
        .memory_addr = (uint32_t)spi_tx_buffer,
        .data_size = SPI_BUFFER_SIZE,
        .periph_data_width = 0,  // 字节
        .memory_data_width = 0,  // 字节
        .mode = 0,  // 普通模式
        .priority = (2 << 16),
        .periph_inc = 0,
        .memory_inc = 1
    };
    DMA_StreamConfig(&tx_config);

    // 配置DMA RX (Stream2, Channel3)
    DMA_Config_t rx_config = {
        .stream = DMA2_Stream2,
        .channel = 3,
        .direction = 0,  // 外设到内存
        .periph_addr = (uint32_t)&SPI1->DR,
        .memory_addr = (uint32_t)spi_rx_buffer,
        .data_size = SPI_BUFFER_SIZE,
        .periph_data_width = 0,
        .memory_data_width = 0,
        .mode = 0,
        .priority = (3 << 16),  // RX优先级更高
        .periph_inc = 0,
        .memory_inc = 1
    };
    DMA_StreamConfig(&rx_config);

    // 使能RX传输完成中断
    DMA_EnableIT(DMA2_Stream2, DMA_IT_TC);
    DMA_NVIC_Config(DMA2_Stream2_IRQn, 2);

    // 使能SPI DMA
    SPI1->CR2 |= SPI_CR2_TXDMAEN | SPI_CR2_RXDMAEN;
}

/**
 * @brief  SPI DMA传输
 * @param  tx_data: 发送数据指针
 * @param  rx_data: 接收数据指针
 * @param  length: 数据长度
 * @retval 无
 */
void SPI1_DMA_Transfer(const uint8_t *tx_data, uint8_t *rx_data, uint16_t length) {
    // 等待上一次传输完成
    while (DMA2_Stream3->CR & DMA_SxCR_EN);
    while (DMA2_Stream2->CR & DMA_SxCR_EN);

    // 配置地址和数据量
    DMA2_Stream3->M0AR = (uint32_t)tx_data;
    DMA2_Stream3->NDTR = length;
    DMA2_Stream2->M0AR = (uint32_t)rx_data;
    DMA2_Stream2->NDTR = length;

    // 清除标志
    DMA_ClearITFlags(DMA2, 2, 0x3F);
    DMA_ClearITFlags(DMA2, 3, 0x3F);

    spi_transfer_complete = 0;

    // 先启动RX,再启动TX
    DMA_Start(DMA2_Stream2);
    DMA_Start(DMA2_Stream3);
}

/**
 * @brief  DMA2 Stream2中断服务函数(SPI1 RX)
 * @param  无
 * @retval 无
 */
void DMA2_Stream2_IRQHandler(void) {
    if (DMA_GetITStatus(DMA2, 2) & DMA_FLAG_TCIF) {
        DMA_ClearITFlags(DMA2, 2, DMA_FLAG_TCIF);
        spi_transfer_complete = 1;
    }
}

示例3:多通道DMA管理

实现一个通用的DMA管理器,支持多个DMA通道。

/**
 * @brief  DMA通道信息结构体
 */
typedef struct {
    DMA_Stream_TypeDef *stream;
    uint8_t stream_num;
    DMA_TypeDef *dma;
    IRQn_Type irqn;
    void (*callback)(void);  // 传输完成回调
    volatile uint8_t busy;
} DMA_Channel_t;

// DMA通道池
#define MAX_DMA_CHANNELS 8
DMA_Channel_t dma_channels[MAX_DMA_CHANNELS];
uint8_t dma_channel_count = 0;

/**
 * @brief  注册DMA通道
 * @param  stream: DMA流指针
 * @param  stream_num: 流编号
 * @param  dma: DMA控制器
 * @param  irqn: 中断号
 * @param  callback: 完成回调函数
 * @retval 通道ID,-1表示失败
 */
int8_t DMA_RegisterChannel(DMA_Stream_TypeDef *stream, uint8_t stream_num,
                           DMA_TypeDef *dma, IRQn_Type irqn,
                           void (*callback)(void)) {
    if (dma_channel_count >= MAX_DMA_CHANNELS) {
        return -1;  // 通道已满
    }

    DMA_Channel_t *ch = &dma_channels[dma_channel_count];
    ch->stream = stream;
    ch->stream_num = stream_num;
    ch->dma = dma;
    ch->irqn = irqn;
    ch->callback = callback;
    ch->busy = 0;

    // 配置NVIC
    NVIC_SetPriority(irqn, 2);
    NVIC_EnableIRQ(irqn);

    return dma_channel_count++;
}

/**
 * @brief  启动DMA传输
 * @param  channel_id: 通道ID
 * @param  src: 源地址
 * @param  dst: 目标地址
 * @param  size: 数据量
 * @retval 0=成功,1=通道忙
 */
uint8_t DMA_StartTransfer(int8_t channel_id, uint32_t src, uint32_t dst, uint32_t size) {
    if (channel_id < 0 || channel_id >= dma_channel_count) {
        return 1;
    }

    DMA_Channel_t *ch = &dma_channels[channel_id];

    if (ch->busy) {
        return 1;  // 通道忙
    }

    // 配置DMA
    DMA_Stop(ch->stream);
    ch->stream->PAR = src;
    ch->stream->M0AR = dst;
    ch->stream->NDTR = size;

    // 清除标志
    DMA_ClearITFlags(ch->dma, ch->stream_num, 0x3F);

    // 标记为忙
    ch->busy = 1;

    // 启动DMA
    DMA_Start(ch->stream);

    return 0;
}

/**
 * @brief  通用DMA中断处理函数
 * @param  channel_id: 通道ID
 * @retval 无
 */
void DMA_IRQ_Handler(int8_t channel_id) {
    if (channel_id < 0 || channel_id >= dma_channel_count) {
        return;
    }

    DMA_Channel_t *ch = &dma_channels[channel_id];

    if (DMA_GetITStatus(ch->dma, ch->stream_num) & DMA_FLAG_TCIF) {
        // 清除标志
        DMA_ClearITFlags(ch->dma, ch->stream_num, DMA_FLAG_TCIF);

        // 标记为空闲
        ch->busy = 0;

        // 调用回调函数
        if (ch->callback) {
            ch->callback();
        }
    }
}

// 使用示例
void uart_tx_complete(void) {
    printf("UART TX complete\r\n");
}

void adc_conversion_complete(void) {
    printf("ADC conversion complete\r\n");
}

int main(void) {
    SystemInit();

    // 注册DMA通道
    int8_t uart_tx_ch = DMA_RegisterChannel(DMA2_Stream7, 7, DMA2, 
                                            DMA2_Stream7_IRQn, uart_tx_complete);
    int8_t adc_ch = DMA_RegisterChannel(DMA2_Stream0, 0, DMA2,
                                        DMA2_Stream0_IRQn, adc_conversion_complete);

    // 使用DMA通道
    // ...
}

深入理解

DMA仲裁和优先级

当多个DMA请求同时发生时,DMA控制器需要进行仲裁。

仲裁机制:

  1. 软件优先级:在CR寄存器中配置(低、中、高、非常高)
  2. 硬件优先级:当软件优先级相同时,流编号小的优先

优先级配置建议:

应用 优先级 原因
ADC采集 非常高 实时性要求高,数据不能丢失
UART接收 高 防止数据溢出
UART发送 中 可以等待
内存拷贝 低 不紧急
/**
 * @brief  配置DMA优先级
 * @param  stream: DMA流指针
 * @param  priority: 优先级(0-3)
 * @retval 无
 */
void DMA_SetPriority(DMA_Stream_TypeDef *stream, uint32_t priority) {
    stream->CR &= ~DMA_SxCR_PL;
    stream->CR |= (priority << 16);
}

DMA FIFO模式

DMA支持FIFO模式,可以提高传输效率。

直接模式 vs FIFO模式:

直接模式:
- 每个数据请求立即传输
- 适合低速外设
- 配置简单

FIFO模式:
- 数据先存入FIFO,达到阈值后批量传输
- 适合高速外设
- 可以减少总线占用

FIFO配置:

/**
 * @brief  配置DMA FIFO
 * @param  stream: DMA流指针
 * @param  threshold: FIFO阈值
 * @retval 无
 */
void DMA_ConfigFIFO(DMA_Stream_TypeDef *stream, uint32_t threshold) {
    // 使能FIFO
    stream->FCR |= DMA_SxFCR_DMDIS;

    // 配置阈值
    stream->FCR &= ~DMA_SxFCR_FTH;
    stream->FCR |= threshold;
}

// FIFO阈值定义
#define DMA_FIFO_THRESHOLD_1_4   (0 << 0)  // 1/4满
#define DMA_FIFO_THRESHOLD_1_2   (1 << 0)  // 1/2满
#define DMA_FIFO_THRESHOLD_3_4   (2 << 0)  // 3/4满
#define DMA_FIFO_THRESHOLD_FULL  (3 << 0)  // 全满

DMA性能优化

1. 数据对齐优化:

/**
 * @brief  检查地址对齐
 * @param  addr: 地址
 * @param  align: 对齐字节数(2、4)
 * @retval 1=对齐,0=不对齐
 */
uint8_t IsAligned(uint32_t addr, uint32_t align) {
    return (addr % align) == 0;
}

/**
 * @brief  优化的DMA配置
 * @param  src: 源地址
 * @param  dst: 目标地址
 * @param  size: 数据量
 * @retval 无
 */
void DMA_OptimizedConfig(uint32_t src, uint32_t dst, uint32_t size) {
    uint32_t data_width;
    uint32_t count;

    // 根据对齐情况选择数据宽度
    if (IsAligned(src, 4) && IsAligned(dst, 4) && (size % 4 == 0)) {
        data_width = 2;  // 32位
        count = size / 4;
    } else if (IsAligned(src, 2) && IsAligned(dst, 2) && (size % 2 == 0)) {
        data_width = 1;  // 16位
        count = size / 2;
    } else {
        data_width = 0;  // 8位
        count = size;
    }

    // 配置DMA
    DMA2_Stream0->CR &= ~(DMA_SxCR_PSIZE | DMA_SxCR_MSIZE);
    DMA2_Stream0->CR |= (data_width << 11) | (data_width << 13);
    DMA2_Stream0->NDTR = count;
}

2. 突发传输优化:

/**
 * @brief  配置DMA突发传输
 * @param  stream: DMA流指针
 * @param  pburst: 外设突发长度
 * @param  mburst: 内存突发长度
 * @retval 无
 */
void DMA_ConfigBurst(DMA_Stream_TypeDef *stream, uint32_t pburst, uint32_t mburst) {
    stream->CR &= ~(DMA_SxCR_PBURST | DMA_SxCR_MBURST);
    stream->CR |= (pburst << 21) | (mburst << 23);
}

// 突发长度定义
#define DMA_BURST_SINGLE  (0 << 0)  // 单次传输
#define DMA_BURST_INCR4   (1 << 0)  // 4次突发
#define DMA_BURST_INCR8   (2 << 0)  // 8次突发
#define DMA_BURST_INCR16  (3 << 0)  // 16次突发

3. 减少中断开销:

/**
 * @brief  使用轮询方式等待DMA完成(小数据量)
 * @param  stream: DMA流指针
 * @retval 无
 */
void DMA_WaitComplete(DMA_Stream_TypeDef *stream) {
    while (stream->CR & DMA_SxCR_EN);
}

/**
 * @brief  使用半传输中断处理大数据(双缓冲效果)
 * @param  stream: DMA流指针
 * @retval 无
 */
void DMA_EnableHalfTransferIT(DMA_Stream_TypeDef *stream) {
    stream->CR |= DMA_SxCR_HTIE;
}

常见问题

Q1: DMA传输不工作,数据没有传输?

可能原因: 1. DMA时钟未使能 2. 外设DMA请求未使能 3. 通道号配置错误 4. 地址配置错误

排查步骤:

// 1. 检查DMA时钟
if (!(RCC->AHB1ENR & RCC_AHB1ENR_DMA2EN)) {
    printf("DMA2 clock not enabled!\r\n");
}

// 2. 检查DMA使能
if (!(DMA2_Stream7->CR & DMA_SxCR_EN)) {
    printf("DMA stream not enabled!\r\n");
}

// 3. 检查外设DMA请求
if (!(USART1->CR3 & USART_CR3_DMAT)) {
    printf("USART1 DMA TX not enabled!\r\n");
}

// 4. 检查通道号
uint32_t channel = (DMA2_Stream7->CR >> 25) & 0x07;
printf("DMA Channel: %d (should be 4 for USART1)\r\n", channel);

// 5. 检查地址
printf("PAR: 0x%08X (should be 0x%08X)\r\n", 
       DMA2_Stream7->PAR, (uint32_t)&USART1->DR);
printf("M0AR: 0x%08X\r\n", DMA2_Stream7->M0AR);
printf("NDTR: %d\r\n", DMA2_Stream7->NDTR);

Q2: DMA传输速度不如预期?

可能原因: 1. 数据宽度配置不当 2. 未使用FIFO模式 3. 优先级过低 4. 总线冲突

优化方法:

// 1. 使用最大数据宽度
if (IsAligned(addr, 4)) {
    // 使用32位传输
    stream->CR |= (2 << 11) | (2 << 13);
}

// 2. 使能FIFO
stream->FCR |= DMA_SxFCR_DMDIS;
stream->FCR |= DMA_FIFO_THRESHOLD_FULL;

// 3. 提高优先级
stream->CR |= (3 << 16);  // 非常高

// 4. 使用突发传输
stream->CR |= (3 << 21) | (3 << 23);  // 16次突发

Q3: DMA中断不触发?

可能原因: 1. 中断未使能 2. NVIC未配置 3. 中断标志未清除 4. 中断向量表错误

解决方案:

// 1. 检查DMA中断使能
if (!(DMA2_Stream7->CR & DMA_SxCR_TCIE)) {
    printf("DMA TC interrupt not enabled!\r\n");
}

// 2. 检查NVIC
if (!NVIC_GetEnableIRQ(DMA2_Stream7_IRQn)) {
    printf("NVIC not enabled!\r\n");
}

// 3. 在中断中清除标志
void DMA2_Stream7_IRQHandler(void) {
    // 必须清除标志
    DMA_ClearITFlags(DMA2, 7, DMA_FLAG_TCIF);

    // 处理...
}

// 4. 检查中断向量表
extern uint32_t __Vectors;
uint32_t *vector_table = (uint32_t*)&__Vectors;
printf("DMA2_Stream7 vector: 0x%08X\r\n", 
       vector_table[16 + DMA2_Stream7_IRQn]);

Q4: 如何处理DMA传输错误?

错误类型和处理:

/**
 * @brief  DMA错误处理
 * @param  dma: DMA控制器
 * @param  stream_num: 流编号
 * @retval 错误码
 */
uint32_t DMA_ErrorHandler(DMA_TypeDef *dma, uint8_t stream_num) {
    uint32_t flags = DMA_GetITStatus(dma, stream_num);
    uint32_t errors = 0;

    // FIFO错误
    if (flags & DMA_FLAG_FEIF) {
        errors |= (1 << 0);
        printf("DMA FIFO Error\r\n");
        // 检查FIFO配置
    }

    // 直接模式错误
    if (flags & DMA_FLAG_DMEIF) {
        errors |= (1 << 1);
        printf("DMA Direct Mode Error\r\n");
        // 检查FIFO设置
    }

    // 传输错误
    if (flags & DMA_FLAG_TEIF) {
        errors |= (1 << 2);
        printf("DMA Transfer Error\r\n");
        // 检查地址和配置
    }

    // 清除错误标志
    DMA_ClearITFlags(dma, stream_num, flags);

    return errors;
}

Q5: 如何在RTOS中使用DMA?

线程安全的DMA使用:

#include "cmsis_os.h"

osSemaphoreId dma_sem;
osMutexId dma_mutex;

/**
 * @brief  初始化DMA同步机制
 * @param  无
 * @retval 无
 */
void DMA_RTOS_Init(void) {
    // 创建信号量(用于等待传输完成)
    osSemaphoreDef(dma_sem);
    dma_sem = osSemaphoreCreate(osSemaphore(dma_sem), 1);

    // 创建互斥锁(用于保护DMA资源)
    osMutexDef(dma_mutex);
    dma_mutex = osMutexCreate(osMutex(dma_mutex));
}

/**
 * @brief  线程安全的DMA传输
 * @param  data: 数据指针
 * @param  length: 数据长度
 * @param  timeout: 超时时间(毫秒)
 * @retval 0=成功,1=失败
 */
uint8_t DMA_Transfer_RTOS(const uint8_t *data, uint16_t length, uint32_t timeout) {
    // 获取互斥锁
    if (osMutexWait(dma_mutex, timeout) != osOK) {
        return 1;  // 获取锁失败
    }

    // 启动DMA传输
    UART1_DMA_Send(data, length);

    // 等待传输完成
    if (osSemaphoreWait(dma_sem, timeout) != osOK) {
        osMutexRelease(dma_mutex);
        return 1;  // 超时
    }

    // 释放互斥锁
    osMutexRelease(dma_mutex);

    return 0;  // 成功
}

/**
 * @brief  DMA中断服务函数(RTOS版本)
 * @param  无
 * @retval 无
 */
void DMA2_Stream7_IRQHandler(void) {
    if (DMA_GetITStatus(DMA2, 7) & DMA_FLAG_TCIF) {
        DMA_ClearITFlags(DMA2, 7, DMA_FLAG_TCIF);

        // 释放信号量
        osSemaphoreRelease(dma_sem);
    }
}

总结

本教程全面介绍了DMA驱动开发的核心知识和实践技能。让我们回顾一下要点:

核心知识点: - DMA工作原理:无需CPU干预的数据传输 - DMA架构:控制器、流、通道的关系 - DMA寄存器:CR、NDTR、PAR、M0AR等配置 - 传输模式:普通模式、循环模式、双缓冲模式 - 中断处理:TC、HT、TE等中断的使用

实践技能: - UART DMA收发:提高串口通信效率 - ADC DMA采集:实现连续数据采集 - SPI DMA传输:高速全双工通信 - 内存拷贝:快速数据搬运 - 多通道管理:统一的DMA资源管理

最佳实践: - 合理配置优先级,避免冲突 - 使用FIFO模式提高效率 - 数据对齐优化传输速度 - 完善的错误处理机制 - 在RTOS中注意线程安全

性能优化: - 选择合适的数据宽度 - 使用突发传输模式 - 减少中断开销 - 避免总线冲突

DMA是提高嵌入式系统性能的关键技术,掌握DMA驱动开发可以显著提升系统的数据处理能力。通过本教程的学习和实践,你应该能够在各种场景中灵活运用DMA技术。

延伸阅读

推荐进一步学习的内容:

同模块内容: - UART串口驱动开发与调试 - DMA的基础应用 - SPI驱动开发:外部Flash读写 - SPI DMA应用 - ADC驱动开发:模拟信号采集 - ADC DMA应用

相关主题: - 中断系统基础概念 - 理解DMA中断 - 内存管理 - 理解内存访问 - 驱动性能优化技术 - 高级优化技巧

高级应用: - DMA与RTOS集成 - DMA在音频处理中的应用 - DMA在图像处理中的应用 - DMA在网络通信中的应用

官方文档: - STM32F4xx参考手册 - DMA章节 - STM32F4xx数据手册 - DMA特性 - ARM Cortex-M4编程手册 - DMA控制器

开源项目: - STM32 HAL库 - 官方DMA驱动 - RT-Thread - RTOS的DMA驱动 - libopencm3 - 开源STM32库

参考资料

  1. STM32F4xx参考手册 - STMicroelectronics
  2. ARM Cortex-M4权威指南 - Joseph Yiu
  3. 嵌入式系统设计与实践 - Elecia White
  4. STM32库开发实战指南 - 野火团队
  5. DMA技术详解 - 清华大学出版社

练习题

基础练习

  1. DMA基本配置练习:
  2. 配置DMA实现UART发送
  3. 配置DMA实现UART接收
  4. 测试不同数据宽度的传输

  5. 中断处理练习:

  6. 实现DMA传输完成中断
  7. 实现DMA半传输中断
  8. 实现DMA错误处理

  9. 性能测试练习:

  10. 对比轮询、中断、DMA的性能
  11. 测试不同数据宽度的传输速度
  12. 测试FIFO模式的效果

进阶练习

  1. 双缓冲实现:
  2. 实现DMA双缓冲接收
  3. 实现乒乓缓冲机制
  4. 测试连续数据流处理

  5. 多通道管理:

  6. 实现DMA通道管理器
  7. 支持动态分配通道
  8. 实现优先级调度

  9. RTOS集成:

  10. 在FreeRTOS中使用DMA
  11. 实现线程安全的DMA接口
  12. 实现DMA资源池

综合项目

  1. 高速数据采集系统:
  2. 使用ADC DMA采集多通道数据
  3. 实现数据缓存和处理
  4. 通过UART DMA发送到PC

  5. 音频播放器:

  6. 使用DMA实现DAC音频输出
  7. 实现双缓冲播放
  8. 支持不同采样率

  9. 高速通信系统:

  10. 使用SPI DMA实现高速通信
  11. 实现数据包协议
  12. 测试最大传输速度

思考题

  1. DMA传输为什么能提高系统性能?在什么情况下使用DMA反而会降低性能?

  2. DMA双缓冲模式和循环模式有什么区别?各适用于什么场景?

  3. 如何选择合适的DMA优先级?多个高优先级DMA同时工作会有什么问题?

  4. DMA传输和CPU拷贝在什么数据量下性能相当?为什么?

  5. 在多核系统中使用DMA需要注意什么问题?如何保证缓存一致性?

实验任务

任务1:UART DMA收发(必做)

要求: - 实现UART DMA发送功能 - 实现UART DMA接收功能(结合IDLE中断) - 测试不同波特率下的性能 - 对比DMA和中断方式的CPU占用率

任务2:ADC多通道采集(必做)

要求: - 使用DMA采集4个ADC通道 - 实现连续采集模式 - 计算各通道的平均值 - 通过串口输出采集结果

任务3:高速数据传输(选做)

要求: - 使用SPI DMA实现高速数据传输 - 测试最大传输速度 - 实现数据校验机制 - 分析性能瓶颈

任务4:DMA管理器(选做)

要求: - 设计通用的DMA管理器 - 支持多个DMA通道 - 实现优先级调度 - 提供统一的API接口

评分标准: - 代码规范性(20分) - 功能完整性(30分) - 性能和效率(30分) - 创新性和扩展性(20分)


下一步学习建议:

完成本教程后,建议按以下顺序继续学习:

  1. USB驱动开发基础 - 学习USB通信
  2. CAN总线驱动开发实战 - 学习CAN通信
  3. 驱动性能优化技术 - 深入优化技巧

学习路线图:

graph LR
    A[DMA驱动] --> B[USB驱动]
    A --> C[CAN驱动]
    A --> D[高速ADC]
    B --> E[USB大容量存储]
    C --> F[汽车电子]
    D --> G[数据采集系统]
    A --> H[驱动优化]

祝你学习顺利!如有问题,欢迎在社区讨论。


文档信息: - 最后更新:2024-01-15 - 版本:v1.0 - 作者:嵌入式知识平台 - 许可:CC BY-NC-SA 4.0