DMA 关键寄存器详解:源地址、目的地址、传输数量、CCR、ISR 与 IFCR
DMA 控制器说白了就是一堆寄存器在干活。你配置好它们,它就能自己搬数据。搬完了,它还会告诉你结果。
我个人习惯把 DMA 寄存器分成三组:地址类、控制类、状态类。咱们一个一个拆开看。
源地址寄存器 (CMARx / CPARx)
源地址,就是数据从哪来。对于内存到外设的传输,源是内存;对于外设到内存,源是外设的数据寄存器。
- CMARx:内存地址寄存器。存放内存缓冲区的基地址。
- CPARx:外设地址寄存器。存放外设数据寄存器的地址。
我踩过的坑: 地址必须按传输宽度对齐。比如你配了 32 位传输,地址必须是 4 字节对齐。否则 DMA 会 stuck,或者产生总线错误。我曾经在 STM32F4 上因为地址没对齐,查了整整一个下午。
目的地址寄存器
数据往哪搬,目的地址就指向哪。和源地址一样,也分内存和外设两个角色。
| 传输方向 | 源地址 | 目的地址 |
|---|---|---|
| 内存 → 外设 | CMARx(内存) | CPARx(外设) |
| 外设 → 内存 | CPARx(外设) | CMARx(内存) |
| 内存 → 内存 | CMARx(源内存) | 另一个 CMARx(目的内存) |
嗯,这里要注意:内存到内存传输,两个地址寄存器都是 CMARx。外设地址寄存器 CPARx 这时候不用。
传输数量寄存器 (CNDTRx)
这个寄存器告诉 DMA:你要搬多少个数据单元。注意,是数据单元个数,不是字节数。
- 每次传输完成后,CNDTRx 自动减 1。
- 减到 0 时,DMA 传输完成,产生传输完成中断。
- 你可以在中断服务程序里重新给 CNDTRx 赋值,实现循环传输。
重要提醒: CNDTRx 是递减计数器。你在调试时读它,看到的是剩余次数,不是总次数。我见过不少新手把这个搞反了。
控制配置寄存器 (CCRx)
CCR 是 DMA 的大脑。它决定了传输的一切行为。我把它拆成几个关键位来讲。
| 位域 | 功能 | 我的建议 |
|---|---|---|
| EN | 使能 DMA 通道 | 最后再置位,先配好其他所有寄存器 |
| DIR | 传输方向:0=外设到内存,1=内存到外设 | 配错方向,数据就飞了 |
| CIRC | 循环模式:1=使能 | 环形缓冲区必须开 |
| PINC / MINC | 外设/内存地址增量:1=使能 | 连续传输必须开,固定地址(如 ADC 数据寄存器)关掉 |
| PSIZE / MSIZE | 外设/内存数据宽度:00=8位,01=16位,10=32位 | 两边宽度不一致时,DMA 会自动对齐,但效率会下降 |
| PL[1:0] | 通道优先级:00=低,01=中,10=高,11=最高 | 实时性要求高的外设(如音频 I2S)给高优先级 |
| TCIE / HTIE / TEIE | 中断使能:传输完成/半传输/传输错误 | 调试阶段全开,生产环境按需开 |
核心经验: 我建议你每次配置 CCR 时,都先清零再赋值。因为 DMA 通道可能被之前的中断残留状态污染。清零是个好习惯。
中断状态寄存器 (ISR)
ISR 是 DMA 的「告示牌」。它告诉你每个通道发生了什么事件。每个通道有 4 个标志位:
- TCIFx:传输完成标志。CNDTRx 减到 0 时置位。
- HTIFx:半传输标志。CNDTRx 减到一半时置位。环形缓冲区常用。
- TEIFx:传输错误标志。总线错误、地址错误时置位。
- GIFx:全局中断标志。上述任一事件发生时置位。
ISR 是只读的。你不能写它。要清除标志,得用 IFCR。
调试技巧: 我调试 DMA 时,第一件事就是在 ISR 里看 TEIF 位。如果它置位了,说明硬件层面有问题——地址不对、总线冲突、或者外设没准备好。别急着查软件逻辑,先看硬件。
中断清除寄存器 (IFCR)
IFCR 是 ISR 的「橡皮擦」。你往对应的位写 1,就能清除 ISR 里对应的标志。
| IFCR 位 | 清除的 ISR 标志 |
|---|---|
| CTCIFx | TCIFx |
| CHTIFx | HTIFx |
| CTEIFx | TEIFx |
| CGIFx | GIFx(同时清除所有标志) |
常见错误: 中断服务程序里,一定要先清除标志,再处理数据。否则你刚清完,DMA 又产生新中断,标志又置位了,导致中断嵌套或者丢失事件。我见过有人把顺序搞反,结果环形缓冲区数据错乱,查了两天。
寄存器操作流程图
下面这张图,是我自己总结的 DMA 寄存器配置流程。你照着走,基本不会出错。
实战:环形缓冲区中的寄存器配合
环形缓冲区用 DMA 时,CCR 的 CIRC 位必须置 1。这样 CNDTRx 减到 0 后会自动重载,DMA 继续传输。
中断处理里,我一般这样写:
void DMA1_Channel1_IRQHandler(void)
{
// 1. 读 ISR,判断中断类型
if (DMA1->ISR & DMA_ISR_TCIF1)
{
// 2. 先清除标志
DMA1->IFCR |= DMA_IFCR_CTCIF1;
// 3. 再处理数据
// 更新环形缓冲区的读指针
ring_buf->read_index = (ring_buf->read_index + BLOCK_SIZE) % BUF_SIZE;
// 4. 通知应用层
semaphore_post(&data_ready);
}
// 半传输中断同理
if (DMA1->ISR & DMA_ISR_HTIF1)
{
DMA1->IFCR |= DMA_IFCR_CHTIF1;
ring_buf->half_flag = 1;
}
// 错误处理
if (DMA1->ISR & DMA_ISR_TEIF1)
{
DMA1->IFCR |= DMA_IFCR_CTEIF1;
// 记录错误,重启 DMA
error_handler();
}
}
我的习惯: 每次中断都检查 TEIF。哪怕你觉得不会出错。因为总线错误、外设时钟没开、DMA 通道冲突,都会导致 TEIF 置位。早发现早解决。