10-DMA数据传输.md 13 KB


tags: [source-summary] type: source source: "尚硅谷嵌入式技术之STM32单片机(进阶篇)V1.0.1 — DMA章节 + 配套代码28~31" author: "尚硅谷研究院" date: 2026-07-15

created: 2026-07-15

DMA数据传输

用生活理解:DMA 就像公司里的快递员——原来你要自己去取文件(CPU 轮询/中断读外设),DMA 就是专门配了个快递员自动帮你搬数据,搬完了喊你一声(完成中断),你就可以专心做核心工作。CPU 只负责"下单"(配置 DMA 参数),不负责"搬货"。


DMA 概述

DMA = Direct Memory Access(直接存储器访问),允许外设和存储器之间直接传输数据,传输过程不需要 CPU 参与

传统方式 vs DMA 方式

轮询方式(无 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 次中断

STM32F103 DMA 控制器

控制器 通道数 可访问的存储器和外设
DMA1 7 通道 SRAM、APB1/APB2 外设
DMA2 5 通道 SRAM、APB1/APB2 外设、FSMC(仅 F103ZET6 等大容量型号有)

DMA 通道映射表(DMA1)

通道 外设请求
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 确认当前通道哪个外设请求有效。

DMA 传输模式

模式 说明 适用场景
内存→内存 源地址和目的地址都是内存 Flash→SRAM 数据搬移
内存→外设 源是内存,目标是外设数据寄存器 向串口发送一批数据
外设→内存 源是外设数据寄存器,目标是内存 从 ADC/串口接收一批数据

DMA 关键寄存器

寄存器 功能 重要位域
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 寄存器描述)完整位域表

CCR 关键位详解

名称 说明
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 内存到内存模式(不需要外设请求,配置完即启动)

DMA 传输流程

1. 开启 DMA 时钟(AHBENR 寄存器)
2. 设置 CPAR(外设地址)和 CMAR(内存地址)
3. 设置 CNDTR(传输次数)
4. 配置 CCR(优先级、数据宽度、增量模式、传输方向)
5. 使能通道(CCR.EN = 1)
6. DMA 自动开始传输(外设请求触发或 MEM2MEM 立即启动)
7. 传输完成 → CNDTR=0 → 产生 TC 中断(如果使能)或 CIRC 重装

实验:DMA 内存到内存传输

软件设计(寄存器版)

项目路径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 DMA1_Init(void);

// DMA1通道1传输数据,需要指定源地址、目的地址、数据长度
void DMA1_Transmit(uint32_t srcAddr, uint32_t destAddr, uint16_t dataLen);

#endif

文件:stm32_base/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_base/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)
	{
	}
}

实验:DMA + USART 自动发送

软件设计(寄存器版 — 内存到外设)

项目路径stm32_base/30_dma_mem2usart_register

DMA + USART 使用 DMA1 通道4(USART1_TX 映射到此通道),配置为内存→外设方向:

文件:stm32_base/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_base/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_base/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 库版 DMA 传输

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 中断默认优先级较低,但具有硬件优先级:

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`

常见问题与避坑

  1. DMA 传输没开始 → 检查 CCR.EN 是否使能、CNDTR≠0、外设是否已发出 DMA 请求(如 USART 的 CR3.DMAT=1)
  2. 传输数据错了 → 检查 MSIZE/PSIZE 是否与外设匹配(8/16/32 位)、地址增量方向是否正确
  3. 中断只进一次 → 检查 ISR.GIF 是否清除、NVIC 是否使能、CCR.TCIE 是否置 1
  4. DMA 和 Cache 一致性 → STM32F103 无 Cache,不必担心(Cortex-M7 需注意)
  5. CNDTR 初始值 = 最大 65535 → 如果传输超过 65535 次,需分段传输或使用循环模式
  6. MEM2MEM 传输方向 → "外设地址寄存器"放源地址,"存储器地址寄存器"放目的地址(命名反直觉但实际如此)
  7. DMA 通道冲突 → 同一通道同一时间只能服务一个外设请求