tags: [source-summary] type: source source: "尚硅谷嵌入式技术之STM32单片机(进阶篇)V1.0.1 — DMA章节 + 配套代码28~31" author: "尚硅谷研究院" date: 2026-07-15
用生活理解:DMA 就像公司里的快递员——原来你要自己去取文件(CPU 轮询/中断读外设),DMA 就是专门配了个快递员自动帮你搬数据,搬完了喊你一声(完成中断),你就可以专心做核心工作。CPU 只负责"下单"(配置 DMA 参数),不负责"搬货"。
DMA = Direct Memory Access(直接存储器访问),允许外设和存储器之间直接传输数据,传输过程不需要 CPU 参与。
轮询方式(无 DMA):
for (i = 0; i < 100; i++) {
while (!(USART1->SR & USART_SR_RXNE)); // CPU 一直等
buffer[i] = USART1->DR; // CPU 亲自搬
}
// CPU 在此过程中无法做其他任何事
中断方式:
// CPU 每次只搬 1 字节(进一次中断),搬 100 次要进 100 次中断
DMA 方式:
// CPU 配置好 DMA(源地址+目的地址+长度)后,去干别的事
// DMA 控制器自动搬完 100 字节后产生中断通知 CPU
// 100 次传输只进 1 次中断
| 控制器 | 通道数 | 可访问的存储器和外设 |
|---|---|---|
| DMA1 | 7 通道 | SRAM、APB1/APB2 外设 |
| DMA2 | 5 通道 | SRAM、APB1/APB2 外设、FSMC(仅 F103ZET6 等大容量型号有) |
| 通道 | 外设请求 |
|---|---|
| CH1 | ADC1、TIM2_CH3、TIM4_CH1 |
| CH2 | USART1_TX、SPI1_RX、TIM1_CH1 |
| CH3 | USART1_RX、SPI1_TX、TIM1_CH2 |
| CH4 | SPI2_RX、SPI1_RX、USART1_TX |
| CH5 | SPI2_TX、USART2_TX、TIM1_CH4 |
| CH6 | USART2_RX、ADC3、TIM1_CH1 |
| CH7 | USART3_TX、ADC3、TIM1_CH2 |
注意:同一时刻一个 DMA 通道只能服务一个外设。需要查参考手册 §10 确认当前通道哪个外设请求有效。
| 模式 | 说明 | 适用场景 |
|---|---|---|
| 内存→内存 | 源地址和目的地址都是内存 | Flash→SRAM 数据搬移 |
| 内存→外设 | 源是内存,目标是外设数据寄存器 | 向串口发送一批数据 |
| 外设→内存 | 源是外设数据寄存器,目标是内存 | 从 ADC/串口接收一批数据 |
| 寄存器 | 功能 | 重要位域 |
|---|---|---|
| CCR | 通道配置寄存器 | EN(使能)、DIR(方向)、PL1:0、MSIZE1:0、PSIZE1:0、MINC(存储地址增量)、PINC(外设地址增量)、CIRC(循环模式)、MEM2MEM(内存到内存) |
| CNDTR | 传输数量寄存器 | 16 位(最大 65535),每次传输后自减 |
| CPAR | 外设地址寄存器 | 外设数据寄存器的地址(如 USART1->DR = 0x40013804) |
| CMAR | 存储器地址寄存器 | 内存缓冲区的地址 |
| ISR | 中断状态寄存器 | TCIFx(通道x传输完成)、HTIFx(通道x半传输)、TEIFx(通道x传输错误) |
| IFCR | 中断标志清除寄存器 | CTCIFx(写1清完成)、CHTIFx(写1清半传) |
参考:参考手册 §10(DMA 寄存器描述)完整位域表
| 位 | 名称 | 值 | 说明 |
|---|---|---|---|
| 0 | EN | 1 | 通道使能(配置完所有参数后再置 1) |
| 4 | DIR | 0=外设→内存, 1=内存→外设 | M2M=1 时此位忽略 |
| 6:5 | MSIZE | 00=8位, 01=16位, 10=32位 | 存储器数据宽度 |
| 7:6 | PSIZE | 00=8位, 01=16位, 10=32位 | 外设数据宽度(必须与外设寄存器宽度匹配) |
| 9 | MINC | 1 | 存储器地址自动递增(增量=MSIZE 字节) |
| 8 | PINC | 1 | 外设地址自动递增 |
| 10 | CIRC | 1 | 循环模式(CNDTR 到 0 后自动重装,持续传输) |
| 14 | MEM2MEM | 1 | 内存到内存模式(不需要外设请求,配置完即启动) |
1. 开启 DMA 时钟(AHBENR 寄存器)
2. 设置 CPAR(外设地址)和 CMAR(内存地址)
3. 设置 CNDTR(传输次数)
4. 配置 CCR(优先级、数据宽度、增量模式、传输方向)
5. 使能通道(CCR.EN = 1)
6. DMA 自动开始传输(外设请求触发或 MEM2MEM 立即启动)
7. 传输完成 → CNDTR=0 → 产生 TC 中断(如果使能)或 CIRC 重装
项目路径:stm32_base/28_dma_mem2mem_register
需求:将 SRAM 中源数组的数据通过 DMA 复制到目的数组,完成后翻转 LED。
文件:stm32_base/28_dma_mem2mem_register/Hardware/DMA/dma.h
#ifndef __DMA_H
#define __DMA_H
#include "stm32f10x.h"
void DMA_Init(void);
void DMA_Start(void);
uint8_t DMA_IsComplete(void);
#endif
文件:stm32_base/28_dma_mem2mem_register/Hardware/DMA/dma.c
#include "dma.h"
// 定义源和目的缓冲区(放在 SRAM 中)
#define BUFFER_SIZE 10
uint32_t src_buffer[BUFFER_SIZE] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
uint32_t dst_buffer[BUFFER_SIZE] = {0};
void DMA_Init(void)
{
// 1. 开启 DMA1 时钟
// DMA 挂载在 AHB 总线上,时钟由 AHBENR 控制
RCC->AHBENR |= RCC_AHBENR_DMA1EN; // |=:DMA1时钟使能
// 2. 清除通道1的所有中断标志
// IFCR 写 1 清除对应标志位
// CGIF1 = 清除通道1全局标志(包含 TCIF/HTIF/TEIF)
DMA1->IFCR |= DMA_IFCR_CGIF1; // |=:写1清除所有标志
// 3. 配置 DMA1 通道1
// 内存到内存传输:从 src_buffer 复制到 dst_buffer
// 注意:必须先配置 CPAR/CMAR/CNDTR,再配 CCR
// 外设地址寄存器 ← 源地址(在内存到内存模式下,"外设"端就是源)
DMA1_Channel1->CPAR = (uint32_t)src_buffer;
// 存储器地址寄存器 ← 目的地址
DMA1_Channel1->CMAR = (uint32_t)dst_buffer;
// 传输数量寄存器 ← 传输次数(一次传输的宽度由 MSIZE 决定)
// CNDTR 是 16 位,最大 65535
DMA1_Channel1->CNDTR = BUFFER_SIZE;
// 4. 配置 CCR
// MEM2MEM=1: 内存到内存模式(不需要外设请求信号,配置完立即开始)
// PL[1:0]=11: 最高优先级
// MSIZE[1:0]=10: 存储器数据宽度 = 32 位
// PSIZE[1:0]=10: 外设数据宽度 = 32 位
// MINC=1: 存储器地址递增(每传一次 +4 字节)
// PINC=1: 外设地址递增(每传一次 +4 字节)
// CIRC=1: 循环模式(传输完成后自动重新开始,适用于持续采集)
// TCIE=1: 传输完成中断使能
DMA1_Channel1->CCR |= DMA_CCR1_MEM2MEM // MEM2MEM 位置 1
| DMA_CCR1_PL // 优先级 11
| DMA_CCR1_MSIZE // MSIZE = 32位
| DMA_CCR1_PSIZE // PSIZE = 32位
| DMA_CCR1_MINC // 存储器地址递增
| DMA_CCR1_PINC // 外设地址递增
| DMA_CCR1_CIRC // 循环模式
| DMA_CCR1_TCIE; // 传输完成中断使能
// 5. 使能通道1
// EN=1 表示通道启动。配置完所有参数后最后才置 EN
// 对于 MEM2MEM 模式,EN=1 后 DMA 立即开始传输
DMA1_Channel1->CCR |= DMA_CCR1_EN; // |=: EN位置1
}
// 检查传输是否完成(查询方式)
uint8_t DMA_IsComplete(void)
{
// ISR.TCIF1 = 1 表示通道1传输完成
if (DMA1->ISR & DMA_ISR_TCIF1) // &测试: TCIF1位=1?
{
DMA1->IFCR |= DMA_IFCR_CTCIF1; // 清除完成标志(写1)
return 1;
}
return 0;
}
文件:stm32_base/28_dma_mem2mem_register/User/main.c
#include "dma.h"
#include "led.h"
int main(void)
{
LED_Init();
DMA_Init(); // DMA 配置完立即开始传输
while (1)
{
if (DMA_IsComplete())
{
// 传输完成:dst_buffer 数组已包含 src_buffer 的副本
// 验证第一个元素是否复制正确
if (dst_buffer[0] == 0x00) {
LED_On(LED_1); // 正确则点亮 LED1
}
}
}
}
项目路径:stm32_base/30_dma_mem2usart_register
// 通过 DMA 自动将缓冲区数据发送到 USART1
// CPU 仅需配置一次,DMA 自动搬完所有数据
#define TX_BUFFER_SIZE 100
uint8_t tx_buffer[TX_BUFFER_SIZE] = "Hello from DMA! This is sent without CPU intervention!\r\n";
void DMA_USART_Init(void)
{
// 1. DMA 时钟
RCC->AHBENR |= RCC_AHBENR_DMA1EN;
// 2. 配置 DMA 通道2(USART1_TX 使用 DMA1 通道2)
// 外设地址 = USART1 的数据寄存器地址(0x40013804)
DMA1_Channel2->CPAR = (uint32_t)&(USART1->DR);
// 存储器地址 = 待发送的缓冲区
DMA1_Channel2->CMAR = (uint32_t)tx_buffer;
// 传输次数 = 缓冲区大小
DMA1_Channel2->CNDTR = TX_BUFFER_SIZE;
// 3. 配置 CCR
// DIR = 1: 内存→外设方向
// MSIZE=8位, PSIZE=8位(USART1->DR 是 8 位寄存器)
// MINC=1: 地址递增(从 buffer 中逐字节取数据)
// PINC=0: 外设地址不递增(始终写入同一个 DR 寄存器)
DMA1_Channel2->CCR |= DMA_CCR2_DIR; // DIR=1: 内存→外设
DMA1_Channel2->CCR |= DMA_CCR2_MINC; // MINC: 内存地址递增
DMA1_Channel2->CCR |= DMA_CCR2_TCIE; // TCIE: 完成后中断
// 4. 注意:此时不使能 DMA 通道
// DMA 传输由 USART 的发送请求触发
// USART 配置中使能 DMA 发送(USART_CR3_DMAT)后,DMA 才开始工作
}
void USART_Init_With_DMA(void)
{
// USART 常规初始化 ...(同之前的 USART 配置)
// 关键是 USART CR3 寄存器的 DMAT 位
// DMAT=1: 使能 USART 发送的 DMA 请求
// 当 USART 的 TXE 置 1 时,自动向 DMA 发送请求
// DMA 收到请求后,从内存搬 1 字节到 USART->DR
USART1->CR3 |= USART_CR3_DMAT; // |=: DMAT位置1
// 使能 DMA 通道(必须在 USART 使能 DMA 之后)
DMA1_Channel2->CCR |= DMA_CCR2_EN; // |=: EN位置1
// 此后,DMA 自动将 tx_buffer 中的数据逐字节发送到串口
}
HAL 库提供了 HAL_UART_Transmit_DMA() 函数,封装了上述所有寄存器操作:
// HAL 库 DMA 发送
HAL_UART_Transmit_DMA(&huart1, tx_buffer, size);
// 参数: uart句柄, 数据指针, 数据长度
// 异步: 函数立即返回,传输在后台进行
// 完成时回调: HAL_UART_TxCpltCallback(&huart1)
// HAL 库 DMA 接收
HAL_UART_Receive_DMA(&huart1, rx_buffer, size);
// 完成时回调: HAL_UART_RxCpltCallback(&huart1)
DMA 中断默认优先级较低,但具有硬件优先级:
| DMA 优先级设置(CCR.PL) | 说明 |
|---|---|
| 00 (最低) | 次于所有外设中断 |
| 01 (低) | 与低优先级外设中断同级 |
| 10 (高) | 与高优先级外设中断同级 |
| 11 (最高) | 优于外设中断 |
当多个 DMA 通道同时请求时,优先级高的先执行;同级时通道号小的优先。
| 寄存器操作 | 代码 | 说明 |
|---|---|---|
| 时钟使能 | `RCC->AHBENR | = RCC_AHBENR_DMA1EN` |
| 清除标志 | `DMA1->IFCR | = DMA_IFCR_CGIF1` |
| 设外设地址 | DMA1_Channel1->CPAR = (uint32_t)&USART1->DR |
外设数据寄存器地址 |
| 设内存地址 | DMA1_Channel1->CMAR = (uint32_t)buffer |
内存缓冲区地址 |
| 设传输次数 | DMA1_Channel1->CNDTR = size |
16 位,传输次数 |
| 使能通道 | `DMA1_Channel1->CCR | = DMA_CCR1_EN` |
| 查询完成 | DMA1->ISR & DMA_ISR_TCIF1 |
TCIF=1 表示完成 |
| 清完成标志 | `DMA1->IFCR | = DMA_IFCR_CTCIF1` |