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 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
            }
        }
    }
}

实验:DMA + USART 自动发送

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

项目路径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 库版 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 通道冲突 → 同一通道同一时间只能服务一个外设请求