3. DMA配置实战:从初始化到完成中断

DMA配置,说白了就是告诉硬件三件事:数据从哪来、送到哪去、怎么传。我刚开始接触DMA时,总觉得寄存器多、配置复杂,后来摸清了套路,发现核心就那么几个步骤。今天咱们就把DMA初始化的完整流程拆开揉碎了讲。

核心要点: DMA配置 = 时钟使能 + 通道选择 + 源/目的地址 + 数据宽度 + 传输模式 + 中断使能

3.1 DMA初始化流程——先搭骨架

我个人习惯把DMA初始化分成三个阶段:使能阶段、配置阶段、启动阶段。每个阶段都有坑,咱们一个一个说。

3.1.1 时钟与复位

第一步永远是使能DMA控制器的时钟。很多新手上来就配寄存器,结果写进去的值根本没用——因为时钟没开。

// 以STM32F4为例
RCC->AHB1ENR |= RCC_AHB1ENR_DMA2EN;  // 使能DMA2时钟
// 别忘了先复位,清除上次残留配置
DMA2->LIFCR = 0x3F;  // 清除所有中断标志
DMA2->HIFCR = 0x3F;
我曾经踩过的坑: 某次调试时DMA死活不工作,查了半天发现是时钟使能位写错了寄存器。DMA1和DMA2的时钟使能位不在同一个寄存器里,一定要看数据手册确认。

3.1.2 通道与流的选择

DMA控制器内部有多个通道(或流),每个通道可以独立配置。选择哪个通道,取决于你的外设请求映射。

外设 DMA控制器 通道/流 请求信号
USART1_TX DMA2 流7 通道4
SPI1_RX DMA2 流0 通道3
ADC1 DMA2 流0 通道0

嗯,这里要注意:不同芯片的映射表不一样。我建议你直接把参考手册里的映射表截图存手机里,调试时随时翻。

3.2 源地址与目的地址配置——数据流向的核心

地址配置是DMA的灵魂。你想想看,地址配错了,数据要么写到不该写的地方,要么读了一堆垃圾。

3.2.1 地址类型与递增模式

DMA支持两种地址模式:固定地址和递增地址。怎么选?看你的数据源和目标是什么。

  • 外设到内存:外设地址固定(比如ADC数据寄存器),内存地址递增
  • 内存到外设:内存地址递增,外设地址固定
  • 内存到内存:源地址递增,目的地址递增
// 配置DMA流(以DMA2_Stream7为例)
DMA2_Stream7->CR &= ~DMA_SxCR_EN;  // 先关闭流,配置期间必须关

// 源地址:外设数据寄存器(固定地址)
DMA2_Stream7->PAR = (uint32_t)&(USART1->DR);
// 目的地址:内存缓冲区(递增地址)
DMA2_Stream7->M0AR = (uint32_t)uart_rx_buffer;

// 设置地址递增模式
DMA2_Stream7->CR |= DMA_SxCR_PINC_0;  // 外设地址不递增
DMA2_Stream7->CR |= DMA_SxCR_MINC;    // 内存地址递增
个人经验: 内存到内存传输时,源和目的地址都要递增。但要注意,内存到内存模式不支持外设请求触发,必须用软件触发。我做过一个数据搬运任务,就是用DMA内存到内存模式,速度比CPU memcpy快3倍。

3.3 传输数据宽度与对齐——细节决定成败

数据宽度就是每次传输的字节数。常见的有8位、16位、32位。这个看似简单,但对齐问题经常让人头疼。

3.3.1 宽度匹配规则

外设宽度 内存宽度 传输行为 注意事项
8位 8位 一对一传输 最简单,推荐
8位 16位 每2次外设传输打包1次内存写入 注意字节序
16位 32位 每2次外设传输打包1次内存写入 对齐要求严格
32位 8位 每次外设传输拆成4次内存写入 性能下降,不推荐

为什么会这样?因为DMA内部有FIFO,宽度不匹配时,FIFO会做打包或拆包。但打包拆包有代价——增加延迟,还可能丢数据。

3.3.2 对齐问题

说白了,就是地址必须能被数据宽度整除。32位传输时,地址必须是4的倍数;16位传输时,地址必须是2的倍数。

我曾经踩过的坑: 一次用DMA传输音频数据,源地址是16位对齐的,但目的地址我写了个奇数偏移。结果DMA传输到一半就卡死了,查了两天才发现是地址对齐问题。从那以后,我所有DMA缓冲区都用__attribute__((aligned(4)))强制对齐。
// 强制对齐的缓冲区定义
__attribute__((aligned(4))) uint8_t dma_buffer[1024];

// 配置数据宽度
DMA2_Stream7->CR |= DMA_SxCR_PSIZE_0;  // 外设宽度:16位
DMA2_Stream7->CR |= DMA_SxCR_MSIZE_1;  // 内存宽度:32位
// 注意:PSIZE和MSIZE要匹配实际硬件

3.4 DMA传输完成中断配置——让CPU知道活干完了

DMA传输是后台进行的,CPU不能一直轮询状态。所以我们需要中断——传输完成时,DMA通知CPU来收数据。

3.4.1 中断使能

DMA中断有好几种:传输完成、半传输、传输错误。最常用的是传输完成中断。

// 使能传输完成中断
DMA2_Stream7->CR |= DMA_SxCR_TCIE;

// 在NVIC中使能DMA中断
NVIC_EnableIRQ(DMA2_Stream7_IRQn);
NVIC_SetPriority(DMA2_Stream7_IRQn, 1);  // 优先级设低一点,别影响实时任务

3.4.2 中断服务函数

中断来了,第一件事是判断中断源,第二件事是清标志位。不清标志位的话,中断会一直触发,CPU就卡死在中断里了。

void DMA2_Stream7_IRQHandler(void)
{
    // 检查传输完成标志
    if(DMA2->LISR & DMA_LISR_TCIF7)
    {
        // 清标志位
        DMA2->LIFCR = DMA_LIFCR_CTCIF7;
        
        // 处理数据
        process_uart_data(uart_rx_buffer, DMA2_Stream7->NDTR);
        
        // 重新启动DMA(循环模式不需要)
        // DMA2_Stream7->CR |= DMA_SxCR_EN;
    }
    
    // 检查错误标志
    if(DMA2->LISR & DMA_LISR_FEIF7)
    {
        DMA2->LIFCR = DMA_LIFCR_CFEIF7;
        // 错误处理
    }
}
个人习惯: 我一般会在中断里只做两件事:清标志位 + 设置一个全局标志。真正的数据处理放在主循环里做。这样中断服务函数执行时间短,不会影响其他中断响应。

3.5 完整配置流程——串个糖葫芦

把上面所有步骤串起来,就是一个完整的DMA初始化函数。我写代码时习惯按这个顺序来,不容易漏配置。

void DMA_UART_Init(void)
{
    // 1. 时钟使能
    RCC->AHB1ENR |= RCC_AHB1ENR_DMA2EN;
    
    // 2. 关闭流,开始配置
    DMA2_Stream7->CR &= ~DMA_SxCR_EN;
    
    // 3. 等待硬件就绪
    while(DMA2_Stream7->CR & DMA_SxCR_EN);
    
    // 4. 配置通道选择
    DMA2_Stream7->CR |= DMA_SxCR_CHSEL_2;  // 通道4
    
    // 5. 配置地址
    DMA2_Stream7->PAR = (uint32_t)&(USART1->DR);
    DMA2_Stream7->M0AR = (uint32_t)uart_rx_buffer;
    
    // 6. 配置传输方向(外设到内存)
    DMA2_Stream7->CR |= DMA_SxCR_DIR_0;
    
    // 7. 配置数据宽度
    DMA2_Stream7->CR |= DMA_SxCR_PSIZE_0;  // 16位
    DMA2_Stream7->CR |= DMA_SxCR_MSIZE_0;  // 16位
    
    // 8. 配置递增模式
    DMA2_Stream7->CR |= DMA_SxCR_MINC;     // 内存递增
    
    // 9. 配置传输数量
    DMA2_Stream7->NDTR = UART_BUFFER_SIZE;
    
    // 10. 使能中断
    DMA2_Stream7->CR |= DMA_SxCR_TCIE;
    NVIC_EnableIRQ(DMA2_Stream7_IRQn);
    
    // 11. 启动DMA
    DMA2_Stream7->CR |= DMA_SxCR_EN;
}
关键提醒: 配置DMA时,一定要先关闭流(EN=0),配置完再打开。在流使能状态下修改配置,硬件行为是未定义的。我见过有人因为这个原因,DMA偶尔传错数据,查了半个月。

3.6 知识体系总览

下面这张图把DMA配置的核心逻辑串起来了。你可以把它当作调试时的检查清单。

DMA配置核心流程 1. 时钟使能 2. 通道选择 3. 关闭流(EN=0) 4. 配置源地址 5. 配置目的地址 6. 数据宽度 7. 递增模式 8. 传输数量 9. 中断使能 10. 启动(EN=1) 11. 等待中断 ⚠ 关键原则: 配置期间必须关闭流(EN=0);地址对齐要匹配数据宽度;中断服务函数要快速退出。

这张图把11个步骤分成了四个阶段。你调试时如果DMA不工作,就按这个顺序逐个检查。我个人经验是,80%的问题出在地址配置和时钟使能上。