4、DMA配置实战:通道选择、地址设置与传输配置
好,咱们直接进入正题。DMA的配置,说白了就是回答三个问题:谁来做搬运工?从哪里搬?搬多少?这三个问题搞定了,DMA就跑起来了。我当年第一次调DMA时,就是栽在地址对齐上,折腾了一整天……嗯,咱们今天就把这些坑都填上。
4.1 DMA通道选择——别抢车道
大多数MCU都有多个DMA通道,比如STM32有8个或16个。每个通道就像一条独立的高速车道。你想想看,如果所有外设都挤在一条车道上,那肯定堵车。
选通道的原则其实很简单:
- 看外设映射表:每个外设都有固定的通道绑定。比如USART1_TX可能只能走DMA1通道4。查手册!查手册!查手册!重要的事说三遍。
- 优先级分配:高实时性的任务(比如音频流)走高优先级通道。我习惯把显示刷新放在最高优先级,因为画面撕裂比数据延迟更明显。
- 避免冲突:同一个通道不能同时服务两个外设。如果你发现两个外设都绑定了同一个通道,那就得考虑用DMA2或者重新规划。
实际项目中的经验:
我在做掌机游戏机时,LCD刷新和音频播放都抢DMA1通道3。后来我把音频挪到DMA2通道1,问题就解决了。说白了,就是别让两个急活挤在一起。
4.2 源地址与目标地址设置——指针指对了没?
地址设置是DMA配置里最容易出bug的地方。我见过太多人把源地址和目标地址写反了,结果数据全跑到不该去的地方。
地址类型分三种:
| 地址类型 | 说明 | 典型场景 |
|---|---|---|
| 外设地址 | 固定不变,比如USART_DR寄存器 | 串口发送/接收 |
| 内存地址 | 可以是固定的,也可以是递增的 | 数组搬运、帧缓冲 |
| 固定地址 | 地址不变,比如ADC数据寄存器 | 单次采样读取 |
设置时要注意几个关键点:
- 地址对齐:如果传输宽度是32位,源地址和目标地址都必须是4字节对齐的。我踩过这个坑——把一个16位数组的地址+1传给DMA,结果数据全乱了。
- 递增模式:搬运数组时,内存地址要设成递增;搬运外设寄存器时,地址要设成固定。千万别搞反了。
- 地址范围:DMA能访问的地址空间有限。有些低端MCU的DMA只能访问SRAM,不能访问Flash。查手册确认一下。
⚠️ 我曾经犯过的错:
有一次做图像旋转,我把源地址设成了递增,目标地址设成了固定。结果所有像素都写到了同一个位置,画面变成了一团色块。调试了三个小时才发现……嗯,从那以后我每次配置地址都会反复核对三遍。
4.3 传输宽度与长度配置——一次搬多少?
传输宽度决定了DMA每次搬运的数据量。常见的有8位(字节)、16位(半字)、32位(字)。
宽度选择的原则:
- 匹配外设宽度:如果外设寄存器是16位的,传输宽度就用16位。强行用32位读16位寄存器,会读到垃圾数据。
- 匹配内存宽度:如果数据是8位的像素值,就用8位传输。用32位传输8位数据,虽然也能跑,但效率反而可能降低。
- 源和目标宽度可以不同:有些DMA支持宽度转换。比如从8位的ADC读数据,写到16位的数组里。但要注意数据对齐问题。
传输长度怎么算?
长度就是DMA要搬运的数据项个数。注意,不是字节数,是项数。
举个例子:你要搬运一个1024字节的数组,如果传输宽度是8位,长度就是1024;如果宽度是16位,长度就是512;如果宽度是32位,长度就是256。
💡 我的个人习惯:
我一般把传输宽度设成和外设一致,然后用总字节数除以宽度得到长度。这样不容易算错。比如搬运一个640×480的16位色图像,总字节数是640×480×2=614400字节,宽度16位,长度就是307200。
4.4 实战配置示例——以STM32为例
咱们看一个实际代码。这个例子是从内存数组搬运数据到USART发送寄存器。
// DMA配置结构体
DMA_InitTypeDef DMA_InitStruct;
// 开启DMA时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
// 选择通道4(USART1_TX绑定通道4)
DMA_InitStruct.DMA_Channel = DMA_Channel_4;
// 源地址:内存数组
DMA_InitStruct.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;
DMA_InitStruct.DMA_MemoryBaseAddr = (uint32_t)tx_buffer;
// 方向:内存到外设
DMA_InitStruct.DMA_DIR = DMA_DIR_PeripheralDST;
// 传输宽度:8位(USART数据寄存器是8位的)
DMA_InitStruct.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStruct.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
// 地址模式:外设固定,内存递增
DMA_InitStruct.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
DMA_InitStruct.DMA_MemoryInc = DMA_MemoryInc_Enable;
// 传输长度:256字节
DMA_InitStruct.DMA_BufferSize = 256;
// 循环模式:不循环(发完就停)
DMA_InitStruct.DMA_Mode = DMA_Mode_Normal;
// 优先级:高
DMA_InitStruct.DMA_Priority = DMA_Priority_High;
// 初始化DMA
DMA_Init(DMA1_Channel4, &DMA_InitStruct);
// 使能DMA通道
DMA_Cmd(DMA1_Channel4, ENABLE);
这段代码里要注意几个细节:
- 外设地址是
USART1->DR,这是发送数据寄存器。写数据到这个寄存器,USART就会自动发送出去。 - 内存地址是
tx_buffer,这是一个uint8_t数组。注意要取地址。 - 方向是
DMA_DIR_PeripheralDST,意思是外设是目标。说白了就是数据从内存搬到外设。 - 长度256,对应256个字节。如果tx_buffer只有256字节,那刚好搬完。
调试时的检查清单:
- 时钟开了吗?很多人忘了开DMA时钟,结果DMA根本不工作。
- 通道选对了吗?查外设映射表确认。
- 地址写反了吗?源地址和目标地址再读一遍。
- 长度算对了吗?宽度和长度的乘积要等于总字节数。
- 中断开了吗?如果需要传输完成中断,别忘了配置NVIC。
4.5 常见问题与避坑指南
问题1:DMA传输了一半就停了
原因通常是总线冲突或者优先级不够。检查一下有没有其他高优先级的外设在抢总线。我曾经在SPI和DMA同时工作时遇到这个问题,后来把SPI的优先级调低就解决了。
问题2:数据错位
多半是地址对齐问题。32位传输时,地址必须是4的倍数。16位传输时,地址必须是2的倍数。如果地址不对齐,DMA会报错或者传输错误数据。
问题3:传输完成后没有中断
检查中断使能位有没有设置。DMA的传输完成中断和错误中断是分开使能的。我习惯把错误中断也打开,这样出问题能第一时间知道。
⚠️ 我曾经踩过的坑:
有一次做音频播放,DMA配置看起来完全正确,但声音就是断断续续的。查了两天,最后发现是DMA的FIFO阈值设置不对。FIFO满了才触发传输,导致数据跟不上。把阈值从1/4改成1/2就正常了。嗯,这种问题手册上写得清清楚楚,但你不遇到一次根本不会注意。
好了,DMA配置的核心内容就这些。说白了就是通道、地址、宽度、长度这四个参数。你只要把这四个参数搞清楚了,DMA配置就没什么难度了。下一节咱们聊聊DMA的中断和事件管理,那个才是真正让DMA变得灵活的关键。