4. DMA配置寄存器详解:控制、状态、地址与计数

DMA传输的核心,说白了就是几个寄存器在干活。很多初学者一上来就盯着中断、回调函数看,结果DMA死活不工作——回头一查,寄存器配错了。我刚开始做DMA驱动时也犯过这毛病,折腾了两天才发现是传输计数寄存器少写了一个1。

这一章,咱们就把DMA的五个关键寄存器掰开揉碎讲清楚。你把这些寄存器吃透了,DMA驱动开发就成功了一大半。

4.1 控制寄存器(CR)—— DMA的大脑

控制寄存器决定了DMA怎么干活。每个DMA通道都有自己的CR,你往里面写什么,DMA就怎么跑。

位域 名称 功能描述
[0] EN 通道使能,1启动,0停止
[1] TCIE 传输完成中断使能
[2] HTIE 半传输中断使能
[4] DIR 传输方向:0=外设→内存,1=内存→外设
[5] CIRC 循环模式使能,常用于连续采样
[6:7] PSIZE 外设数据宽度:00=8位,01=16位,10=32位
[8:9] MSIZE 存储器数据宽度,同上
[10] MINC 存储器地址增量使能,1=地址递增
[11] PINC 外设地址增量使能

关键点:DIR位决定了数据流向。我见过有人把外设到内存的方向配反了,结果DMA把内存数据往传感器寄存器里写——传感器直接死机。配之前,先想清楚数据从哪来、到哪去。

4.2 状态寄存器(SR)—— DMA的晴雨表

状态寄存器是只读的,告诉你DMA当前在干什么。调试DMA时,我第一件事就是读SR。

  • TCIF(传输完成标志):传输计数减到0时置1。我习惯在中断里先读这个位,确认传输真的完成了再处理数据。
  • HTIF(半传输标志):传输计数减到一半时置1。做双缓冲音频播放时,这个位特别好用——半传输完成就切换缓冲区,保证音频不卡顿。
  • TEIF(传输错误标志):总线错误或FIFO溢出时置1。嗯,这个位一旦置起来,说明硬件层面出问题了,得查查时钟、总线配置。
  • DMEIF(直接模式错误):某些芯片特有,FIFO模式下才会出现。

注意:读SR后,通常需要写0来清除标志位。我曾经踩过一个坑:中断服务函数里忘了清TCIF,结果DMA只触发了一次中断,后面全卡死了。清标志位这件事,写进你的代码模板里。

4.3 源地址寄存器(SAR)与目的地址寄存器(DAR)

这两个寄存器成对出现,一个管数据从哪来,一个管数据往哪去。

SAR(Source Address Register):存放源数据的起始地址。如果是外设到内存,SAR就是外设数据寄存器的地址;如果是内存到外设,SAR就是内存缓冲区的地址。

DAR(Destination Address Register):存放目的数据的起始地址。方向反过来理解就行。

我的习惯:初始化时,我会把SAR和DAR都打印出来看一眼。特别是外设地址,很多芯片的外设寄存器地址在参考手册里是偏移量,你得加上外设基地址才是真正的物理地址。少加一个0x4000,DMA就跑到未知区域去了。

地址对齐也很重要。如果PSIZE配的是32位,但SAR给的地址不是4字节对齐的,DMA可能会产生错误。我遇到过一块开发板,就是因为外设地址没对齐,DMA传输老是丢数据。

4.4 传输计数寄存器(CNDTR)—— 管着传输长度

CNDTR(Current Number of Data Transfer Register)存的是要传输的数据项个数,不是字节数。举个例子:你要传1024个字节,PSIZE配的是16位,那CNDTR就写512。

这个寄存器有个特点:传输过程中它会递减。每次DMA传输完一个数据项,CNDTR就减1。减到0时,TCIF置位,传输结束。

避坑指南:我曾经在配置DMA时,把CNDTR写成了字节数1024,但PSIZE配的是32位。结果DMA传了256个32位数据就停了,因为CNDTR减到0了。数据只传了1/4,剩下的全丢了。记住:CNDTR = 总字节数 / 数据宽度。

另外,CNDTR在通道使能后才能写入。有些芯片在EN=1时写CNDTR是无效的,你得先关通道,写值,再开通道。这个顺序搞反了,DMA可能根本不启动。

4.5 寄存器配置流程与SVG结构图

下面这张图,是我做DMA驱动时自己画的配置流程。你照着这个顺序走,基本不会漏东西。

DMA寄存器配置流程图 1. 关通道(EN=0) 2. 配SAR、DAR地址 3. 写CNDTR传输计数 4. 配CR控制寄存器 5. 开通道(EN=1) 6. 读SR确认状态 配置要点 顺序不能乱 先关通道再配参数 CNDTR在EN=0时写

4.6 实战代码片段:STM32 DMA配置

拿STM32F4系列举个例子。这段代码配置DMA2通道0,把数据从USART1的DR寄存器搬到内存缓冲区。

void DMA_Config(void)
{
    // 1. 关通道
    DMA2->S0CR &= ~DMA_SxCR_EN;

    // 2. 配源地址(外设)和目的地址(内存)
    DMA2->S0PAR = (uint32_t)&USART1->DR;   // 源:USART数据寄存器
    DMA2->S0M0AR = (uint32_t)uart_buffer;   // 目的:内存缓冲区

    // 3. 传输计数:传256个字节,PSIZE=8位,所以CNDTR=256
    DMA2->S0NDTR = 256;

    // 4. 配控制寄存器
    // DIR=0(外设→内存),PSIZE=8位,MSIZE=8位
    // MINC=1(内存地址递增),PINC=0(外设地址固定)
    DMA2->S0CR = DMA_SxCR_DIR_0          // 外设到内存
                | DMA_SxCR_PSIZE_0        // 8位
                | DMA_SxCR_MSIZE_0        // 8位
                | DMA_SxCR_MINC           // 内存递增
                | DMA_SxCR_TCIE;          // 开传输完成中断

    // 5. 开通道
    DMA2->S0CR |= DMA_SxCR_EN;

    // 6. 读状态寄存器确认(调试用)
    if(DMA2->LISR & DMA_LISR_TCIF0)
    {
        // 传输完成,清标志
        DMA2->LIFCR = DMA_LIFCR_CTCIF0;
    }
}

小技巧:调试时,我习惯在开通道后加一个延时,然后读CNDTR的值。如果CNDTR开始递减了,说明DMA确实在跑。如果CNDTR纹丝不动,八成是触发源没配好,或者外设没准备好数据。

4.7 寄存器配置常见错误

我总结了几种最常见的配置错误,你写代码时对照着检查一遍:

  • 地址写错:外设地址用了偏移量,没加基地址。DMA跑到未知区域,数据全乱。
  • CNDTR与数据宽度不匹配:总字节数除以数据宽度,结果不是整数。DMA传到最后几个字节时行为未定义。
  • MINC/PINC配反:外设地址递增了,但外设寄存器只有一个地址。DMA读到的全是垃圾数据。
  • 中断使能位没开:TCIE=0,传输完成不产生中断。CPU不知道DMA干完了,一直傻等。
  • 通道使能顺序错:先开了EN再写CNDTR,CNDTR写不进去。DMA启动后传输计数是0,直接完成中断。

嗯,这些坑我基本都踩过一遍。你记下来,写代码时多看一眼,能省下不少调试时间。