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/28_dma_mem2mem_register
需求:将 SRAM 中源数组的数据通过 DMA 复制到目的数组,完成后翻转 LED。
文件:stm32/28_dma_mem2mem_register/Hardware/DMA/dma.h
#ifndef __DMA_H
#define __DMA_H
#include "stm32f10x.h"
// 初始化
void DMA1_Init(void);
// DMA1通道1传输数据,需要指定源地址、目的地址、数据长度
void DMA1_Transmit(uint32_t srcAddr, uint32_t destAddr, uint16_t dataLen);
#endif
文件:stm32/28_dma_mem2mem_register/Hardware/DMA/dma.c
#include "dma.h"
// 初始化
void DMA1_Init(void)
{
// 1. 开启时钟
RCC->AHBENR |= RCC_AHBENR_DMA1EN;
// 2. DMA参数配置
// 2.1 传输方向DIR = 0,从外设到存储器(实际是ROM到RAM,存储器到存储器)
DMA1_Channel1->CCR &= ~DMA_CCR1_DIR;
DMA1_Channel1->CCR |= DMA_CCR1_MEM2MEM;
// 2.2 设置数据宽度,均为00 - 8位
DMA1_Channel1->CCR &= ~DMA_CCR1_PSIZE;
DMA1_Channel1->CCR &= ~DMA_CCR1_MSIZE;
// 2.3 外设和存储器地址增量
DMA1_Channel1->CCR |= DMA_CCR1_PINC;
DMA1_Channel1->CCR |= DMA_CCR1_MINC;
// 2.4 使能传输完成中断
DMA1_Channel1->CCR |= DMA_CCR1_TCIE;
// 3. NVIC配置
NVIC_SetPriorityGrouping(3);
NVIC_SetPriority(DMA1_Channel1_IRQn, 3);
NVIC_EnableIRQ(DMA1_Channel1_IRQn);
}
// DMA1通道1传输数据,需要指定源地址、目的地址、数据长度
void DMA1_Transmit(uint32_t srcAddr, uint32_t destAddr, uint16_t dataLen)
{
// 1. 将源地址写入外设地址寄存器
DMA1_Channel1->CPAR = srcAddr;
// 2. 将目的地址写入存储器地址寄存器
DMA1_Channel1->CMAR = destAddr;
// 3. 将数据长度写入 CNDTR
DMA1_Channel1->CNDTR = dataLen;
// 4. 使能通道,开始数据传输
DMA1_Channel1->CCR |= DMA_CCR1_EN;
}
extern uint8_t isOver;
// 中断服务函数
void DMA1_Channel1_IRQHandler(void)
{
// 判断是否是传输完成中断(TCIF)
if ( DMA1->ISR & DMA_ISR_TCIF1 )
{
// 清除中断标志位
DMA1->IFCR |= DMA_IFCR_CTCIF1;
// 关闭通道
DMA1_Channel1->CCR &= ~DMA_CCR1_EN;
isOver = 1;
}
}
文件:stm32/28_dma_mem2mem_register/User/main.c
#include "usart.h"
#include "dma.h"
// 常量全局数组,存储在 ROM 区
const uint8_t src[] = {10, 20, 30, 40};
// 变量全局数组,存储在 RAM 区
uint8_t dest[4];
// 全局变量,表示数据传输完毕
uint8_t isOver;
int main(void)
{
// 初始化
USART_Init();
DMA1_Init();
printf("Hello world!\n");
// 打印源和目的地址
printf("src = %p, dest = %p\n", src, dest);
// 调用函数启动传输
DMA1_Transmit((uint32_t)src, (uint32_t)dest, 4);
// 等待传输完毕,打印结果
while (!isOver)
{
}
for (uint8_t i = 0; i < 4; i++)
{
printf("%d\t", dest[i]);
}
while (1)
{
}
}
项目路径:stm32/30_dma_mem2usart_register
DMA + USART 使用 DMA1 通道4(USART1_TX 映射到此通道),配置为内存→外设方向:
文件:stm32/30_dma_mem2usart_register/Hardware/DMA/dma.h
#ifndef __DMA_H
#define __DMA_H
#include "stm32f10x.h"
// 初始化
void DMA1_Init(void);
// DMA1通道4传输数据,需要指定源地址、目的地址、数据长度
void DMA1_Transmit(uint32_t srcAddr, uint32_t destAddr, uint16_t dataLen);
#endif
文件:stm32/30_dma_mem2usart_register/Hardware/DMA/dma.c
#include "dma.h"
// 初始化
void DMA1_Init(void)
{
// 1. 开启时钟
RCC->AHBENR |= RCC_AHBENR_DMA1EN;
// 2. DMA参数配置
// 2.1 传输方向DIR = 1,从存储器到外设
DMA1_Channel4->CCR |= DMA_CCR4_DIR;
// 2.2 设置数据宽度,均为00 - 8位
DMA1_Channel4->CCR &= ~DMA_CCR4_PSIZE;
DMA1_Channel4->CCR &= ~DMA_CCR4_MSIZE;
// 2.3 存储器地址递增,外设地址不递增
DMA1_Channel4->CCR &= ~DMA_CCR4_PINC;
DMA1_Channel4->CCR |= DMA_CCR4_MINC;
// 2.4 使能传输完成中断
DMA1_Channel4->CCR |= DMA_CCR4_TCIE;
// 2.5 使能USART发送DMA请求
USART1->CR3 |= USART_CR3_DMAT;
// 循环模式
DMA1_Channel4->CCR |= DMA_CCR1_CIRC;
// 3. NVIC配置
NVIC_SetPriorityGrouping(3);
NVIC_SetPriority(DMA1_Channel4_IRQn, 3);
NVIC_EnableIRQ(DMA1_Channel4_IRQn);
}
// DMA1通道4传输数据,需要指定源地址、目的地址、数据长度
void DMA1_Transmit(uint32_t srcAddr, uint32_t destAddr, uint16_t dataLen)
{
// 1. 将源地址写入存储器地址寄存器
DMA1_Channel4->CMAR = srcAddr;
// 2. 将目的地址写入外设地址寄存器
DMA1_Channel4->CPAR = destAddr;
// 3. 将数据长度写入 CNDTR
DMA1_Channel4->CNDTR = dataLen;
// 4. 使能通道,开始数据传输
DMA1_Channel4->CCR |= DMA_CCR4_EN;
}
// 中断服务函数
void DMA1_Channel4_IRQHandler(void)
{
// 判断是否是传输完成中断(TCIF)
if ( DMA1->ISR & DMA_ISR_TCIF4 )
{
// 清除中断标志位
DMA1->IFCR |= DMA_IFCR_CTCIF4;
// 关闭通道(此处注释掉,循环模式下保持使能)
// DMA1_Channel4->CCR &= ~DMA_CCR4_EN;
}
}
文件:stm32/30_dma_mem2usart_register/User/main.c
#include "usart.h"
#include "dma.h"
#include "delay.h"
// 全局变量,需要发送的数据
uint8_t src[] = {'a', 'b', 'c', 'd', 'e'};
int main(void)
{
// 初始化
USART_Init();
DMA1_Init();
printf("Hello world!\n");
Delay_ms(1);
// 调用函数启动传输
DMA1_Transmit((uint32_t)src, (uint32_t)&(USART1->DR), 5);
while (1)
{
}
}
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` |