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);
中断服务函数里,一定要做两件事:
- 清除中断标志位:调用DMA_ClearITPendingBit()。
- 处理数据:比如通知主程序数据已就绪。
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图
下面这张图把整个配置流程串起来了,你可以对照着看:
4.8 最后一步:使能DMA通道
所有配置完成后,别忘了使能DMA通道。这一步经常被忽略。
// 使能DMA通道
DMA_Cmd(DMA1_Channel1, ENABLE);
总结:DMA配置其实不复杂,关键是顺序要对、细节要细。时钟使能→初始化结构体→通道选择→优先级→中断配置→使能通道,按这个顺序走,基本不会出大问题。当然,具体参数还得根据你的外设来调。多试几次,找到感觉就好了。