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驱动时自己画的配置流程。你照着这个顺序走,基本不会漏东西。
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,直接完成中断。
嗯,这些坑我基本都踩过一遍。你记下来,写代码时多看一眼,能省下不少调试时间。