4. DMA配置流程:时钟使能、DMA初始化、通道选择、优先级设置、传输完成中断配置

好,咱们进入DMA配置的正题。说实话,很多初学者觉得DMA配置很玄乎,其实说白了就是几个固定步骤。我刚开始做项目时也踩过不少坑,今天就把这些经验掰开揉碎了讲给你听。

4.1 配置流程总览

DMA配置,我习惯把它分成五个核心步骤。每一步都有它的道理,少一步都不行。

步骤 操作 关键寄存器/函数 常见坑点
1 时钟使能 RCC_AHBENR, DMAxEN 忘记使能时钟,DMA寄存器写不进
2 DMA初始化 DMA_InitTypeDef 外设地址、内存地址方向搞反
3 通道选择 DMA_Channelx 通道与外设映射关系查错
4 优先级设置 DMA_CCRx, PL[1:0] 多通道竞争时优先级没规划
5 中断配置 DMA_IT_TC, NVIC 中断标志位没清干净

核心原则:DMA配置必须按顺序来。时钟没开就配寄存器,等于对牛弹琴。我见过有人调了半天DMA不工作,最后发现是时钟位没置1——这种低级错误,嗯,我也犯过。

4.2 第一步:时钟使能

DMA挂在AHB总线上。你要用DMA1还是DMA2,得先把对应的时钟打开。

// 使能DMA1时钟
RCC->AHBENR |= RCC_AHBENR_DMA1EN;

// 如果是STM32F4系列,用下面这个
// RCC->AHB1ENR |= RCC_AHB1ENR_DMA1EN;

警告:不同系列的单片机,DMA时钟使能位所在的寄存器不同。F1系列在AHBENR,F4/H7系列在AHB1ENR。别拿F1的代码直接往F4上套,我吃过这个亏。

4.3 第二步:DMA初始化结构体配置

配置DMA,核心就是填好初始化结构体。我个人习惯先把结构体清零,再逐个赋值,避免残留值捣乱。

DMA_InitTypeDef DMA_InitStruct;

// 清零结构体,防止意外
DMA_DeInit(DMA1_Channel1);

DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;
DMA_InitStruct.DMA_MemoryBaseAddr     = (uint32_t)uart1_rx_buf;
DMA_InitStruct.DMA_DIR                = DMA_DIR_PeripheralToMemory;
DMA_InitStruct.DMA_BufferSize         = 256;
DMA_InitStruct.DMA_PeripheralInc      = DMA_PeripheralInc_Disable;
DMA_InitStruct.DMA_MemoryInc          = DMA_MemoryInc_Enable;
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStruct.DMA_MemoryDataSize     = DMA_MemoryDataSize_Byte;
DMA_InitStruct.DMA_Mode               = DMA_Mode_Circular;
DMA_InitStruct.DMA_Priority           = DMA_Priority_High;
DMA_InitStruct.DMA_M2M                = DMA_M2M_Disable;

DMA_Init(DMA1_Channel1, &DMA_InitStruct);

这里有几个关键点:

  • 外设地址:必须是外设的数据寄存器地址,不是外设基地址。比如USART的DR寄存器。
  • 内存地址:建议用全局数组或静态缓冲区,别用局部变量——DMA传输时局部变量可能已经被释放了。
  • 传输方向:外设到内存、内存到外设、内存到内存,三种方向别搞混。

小技巧:配置循环模式(Circular)时,记得缓冲区大小要跟实际数据量匹配。我曾经设了128字节的缓冲区,但DMA配置成256,结果数据覆盖得一塌糊涂。

4.4 第三步:通道选择

DMA通道不是随便选的。每个外设都有固定的DMA通道映射。你想想看,如果通道选错了,DMA根本不会响应外设请求。

外设 请求信号 DMA1通道 DMA2通道
USART1_TX USART1_TX_DMA 通道4 -
USART1_RX USART1_RX_DMA 通道5 -
SPI1_TX SPI1_TX_DMA 通道3 -
ADC1 ADC1_DMA 通道1 -
TIM2_UP TIM2_UP_DMA - 通道1

重要:查芯片参考手册的DMA请求映射表。不同型号、不同封装,映射关系可能有差异。别偷懒,一定要翻手册确认。

4.5 第四步:优先级设置

当多个DMA通道同时请求传输时,优先级就派上用场了。DMA优先级分四级:低、中、高、最高。

// 优先级枚举定义
#define DMA_Priority_Low      ((uint32_t)0x00000000)
#define DMA_Priority_Medium   ((uint32_t)0x00001000)
#define DMA_Priority_High     ((uint32_t)0x00002000)
#define DMA_Priority_VeryHigh ((uint32_t)0x00003000)

// 配置示例:串口接收通道用高优先级
DMA_InitStruct.DMA_Priority = DMA_Priority_High;

优先级规划建议:

  • 实时性要求高的:比如音频数据流,用最高优先级。
  • 批量数据传输:比如ADC采集,用中优先级就够了。
  • 后台慢速传输:比如LED扫描,用低优先级。

注意:优先级不是越高越好。如果所有通道都设成最高,那优先级机制就形同虚设了。而且高优先级通道会频繁抢占总线,影响CPU执行效率。我一般只给1-2个关键通道设高优先级。

4.6 第五步:传输完成中断配置

DMA传输完成后,我们需要知道这个事件。中断是最常用的方式。

// 使能DMA传输完成中断
DMA_ITConfig(DMA1_Channel1, DMA_IT_TC, ENABLE);

// 配置NVIC
NVIC_InitTypeDef NVIC_InitStruct;
NVIC_InitStruct.NVIC_IRQChannel                   = DMA1_Channel1_IRQn;
NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority = 1;
NVIC_InitStruct.NVIC_IRQChannelSubPriority        = 0;
NVIC_InitStruct.NVIC_IRQChannelCmd                = ENABLE;
NVIC_Init(&NVIC_InitStruct);

中断服务函数里,一定要做两件事:

  1. 清除中断标志位:调用DMA_ClearITPendingBit()。
  2. 处理数据:比如通知主程序数据已就绪。
void DMA1_Channel1_IRQHandler(void)
{
    if(DMA_GetITStatus(DMA1_Channel1, DMA_IT_TC))
    {
        // 清除中断标志
        DMA_ClearITPendingBit(DMA1_Channel1, DMA_IT_TC);
        
        // 设置标志位,通知主循环处理数据
        dma_rx_complete_flag = 1;
    }
}

避坑指南:我曾经在中断里直接调用printf打印数据,结果程序卡死了。为什么?因为printf本身可能用了DMA,中断里又触发DMA,形成了死锁。记住:中断服务函数要短小精悍,复杂处理交给主循环。

4.7 完整配置流程SVG图

下面这张图把整个配置流程串起来了,你可以对照着看:

DMA配置流程 DMA配置五步流程图 1. 时钟使能 2. DMA初始化 3. 通道选择 4. 优先级设置 5. 中断配置 关键说明: • 步骤1-4必须在DMA使能(EN位)之前完成 • 中断配置可以在使能之后,但建议在使能前配好NVIC • 通道选择必须查手册确认外设映射关系 最后使能DMA通道:DMA_Cmd(DMA1_Channel1, ENABLE);

4.8 最后一步:使能DMA通道

所有配置完成后,别忘了使能DMA通道。这一步经常被忽略。

// 使能DMA通道
DMA_Cmd(DMA1_Channel1, ENABLE);

总结:DMA配置其实不复杂,关键是顺序要对、细节要细。时钟使能→初始化结构体→通道选择→优先级→中断配置→使能通道,按这个顺序走,基本不会出大问题。当然,具体参数还得根据你的外设来调。多试几次,找到感觉就好了。