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字节,那刚好搬完。

调试时的检查清单:

  1. 时钟开了吗?很多人忘了开DMA时钟,结果DMA根本不工作。
  2. 通道选对了吗?查外设映射表确认。
  3. 地址写反了吗?源地址和目标地址再读一遍。
  4. 长度算对了吗?宽度和长度的乘积要等于总字节数。
  5. 中断开了吗?如果需要传输完成中断,别忘了配置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变得灵活的关键。