3. 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;
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; // 内存地址递增
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的倍数。
// 强制对齐的缓冲区定义
__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;
}
3.6 知识体系总览
下面这张图把DMA配置的核心逻辑串起来了。你可以把它当作调试时的检查清单。
这张图把11个步骤分成了四个阶段。你调试时如果DMA不工作,就按这个顺序逐个检查。我个人经验是,80%的问题出在地址配置和时钟使能上。