目录
[1. DMA 概述](#1. DMA 概述)
[1.1 DMA的基本概念](#1.1 DMA的基本概念)
[1.2 STM32中的DMA资源配置](#1.2 STM32中的DMA资源配置)
[2. 存储器映射与总线架构](#2. 存储器映射与总线架构)
[2.1 内核与片上外设](#2.1 内核与片上外设)
[2.2 总线矩阵与冲突仲裁](#2.2 总线矩阵与冲突仲裁)
[2.3 存储器映射](#2.3 存储器映射)
[2.3.1 Block 0:程序代码区 (Code)](#2.3.1 Block 0:程序代码区 (Code))
[2.3.2 Block 1:运行内存区 (SRAM)](#2.3.2 Block 1:运行内存区 (SRAM))
[2.3.3 Block 2:片上外设区 (Peripherals)](#2.3.3 Block 2:片上外设区 (Peripherals))
[3. DMA 硬件架构与工作机制](#3. DMA 硬件架构与工作机制)
[3.1 DMA 请求](#3.1 DMA 请求)
[3.2 DMA 通道](#3.2 DMA 通道)
[3.2.2 硬件触发的映射约束](#3.2.2 硬件触发的映射约束)
[3.2.3 资源冲突与分时复用原则](#3.2.3 资源冲突与分时复用原则)
[3.3 仲裁器](#3.3 仲裁器)
[4. DMA 配置参数解析](#4. DMA 配置参数解析)
[4.1 传输方向与基地址](#4.1 传输方向与基地址)
[4.2 地址增量模式](#4.2 地址增量模式)
[4.3 数据位宽与对齐规则](#4.3 数据位宽与对齐规则)
[4.4 传输计数器与循环模式](#4.4 传输计数器与循环模式)
[4.4.1 正常单次模式 (Normal Mode, CIRC = 0)](#4.4.1 正常单次模式 (Normal Mode, CIRC = 0))
[4.4.2 自动循环模式 (Circular Mode, CIRC = 1)](#4.4.2 自动循环模式 (Circular Mode, CIRC = 1))
[5. DMA 配置流程](#5. DMA 配置流程)
[5.1 存储器到存储器(M2M)数据转运](#5.1 存储器到存储器(M2M)数据转运)
[5.1.1 开启 DMA 时钟](#5.1.1 开启 DMA 时钟)
[5.1.2 初始化 DMA 参数(核心配置)](#5.1.2 初始化 DMA 参数(核心配置))
[5.2 外设到存储器(ADC 多通道扫描)](#5.2 外设到存储器(ADC 多通道扫描))
[5.2.1 时钟与 GPIO 配置](#5.2.1 时钟与 GPIO 配置)
[5.2.2 ADC 扫描与连续模式配置](#5.2.2 ADC 扫描与连续模式配置)
[5.2.3 DMA 通道参数配置(非对称联动)](#5.2.3 DMA 通道参数配置(非对称联动))
[5.2.4 链路激活与启动](#5.2.4 链路激活与启动)
[6. 本章节实验](#6. 本章节实验)
[6.1 DMA直接数据转运(存储器到存储器)](#6.1 DMA直接数据转运(存储器到存储器))
[6.1.1 实验目标](#6.1.1 实验目标)
[6.1.2 硬件设计](#6.1.2 硬件设计)
[6.1.3 软件设计](#6.1.3 软件设计)
[6.1.4 实验现象](#6.1.4 实验现象)
[6.2 ADC多通道扫描与DMA连续转运(外设到存储器)](#6.2 ADC多通道扫描与DMA连续转运(外设到存储器))
[6.2.1 实验目标](#6.2.1 实验目标)
[6.2.2 硬件设计](#6.2.2 硬件设计)
[6.2.3 软件设计](#6.2.3 软件设计)
[6.2.4 实验现象](#6.2.4 实验现象)
1. DMA 概述
1.1 DMA的基本概念
**DMA(Direct Memory Access,直接存储器存取)**是微控制器中用于实现高效数据搬运的专用外设。其核心工程意义在于:在无需 CPU 连续干预的情况下,通过硬件逻辑直接接管总线控制权,建立起外设与存储器(Peripheral-to-Memory)、存储器与外设(Memory-to-Peripheral)或存储器与存储器(Memory-to-Memory)之间的高速数据传输通道。
在传统的轮询或中断驱动方式中,CPU 需要参与数据的逐次读写搬运。当数据量较大或传输频繁时,会占用大量 CPU 时间,降低系统的并发处理能力。引入 DMA 后,CPU 仅需完成传输配置与状态管理,具体的数据搬运由 DMA 控制器在总线上自动完成,从而显著降低 CPU 负载并提升系统效率。
1.2 STM32中的DMA资源配置
STM32F103系列微控制器的DMA资源分配与芯片闪存容量(密度等级)直接相关,具体配置如下:
| 设备类型 | 集成控制器数量 | 通道总数 | 适用典型型号 |
|---|---|---|---|
| 低容量/中容量设备 | 仅 DMA1 | 7 个独立通道 | STM32F103C8T6 |
| 大容量/互联型设备 | DMA1 + DMA2 | 12 个独立通道 | STM32F103VET6 |
DMA核心资源特性:
-
DMA1 (全系列标配):具备 7 个独立通道(Channel 1--7)。每个通道支持独立配置源/目的基地址、数据宽度(8/16/32-bit)、地址增量模式及软件优先级。
-
DMA2 (大容量专属):具备 5 个独立通道(Channel 1--5),旨在扩展对 SDIO、FSMC 及多组 ADC 等高性能外设的数据承载能力。
2. 存储器映射与总线架构
为了深刻理解 DMA 在微控制器内部是如何搬运数据的,我们必须首先建立对 STM32 底层硬件运行环境的全局认知。这包括 CPU 内核与片上外设的拓扑关系、负责数据路由的总线矩阵,以及决定数据物理存放位置的存储器映射机制。
2.1 内核与片上外设
从硬件体系结构来看,STM32 微控制器主要由内核(Core)与片上外设(On-chip Peripherals)两大部分构成,如下图 STM32F10xx 系统框图所示:

以 STM32F103 系列为例,其运算与控制核心采用的是由 ARM 公司设计的 Cortex-M3 处理器内核。ARM 公司仅提供内核的微架构 IP 授权,而意法半导体(ST)等芯片制造商则在内核的外围,通过内部总线矩阵挂载了丰富的功能模块(如 GPIO、USART、ADC、定时器以及 DMA 控制器等),这些模块统称为片上外设。
内核与这些片上外设并非孤立存在,如图 STM32F10xx 系统框图所示,它们之间通过高度分布式的总线网络进行高速的数据与指令交互。在 Cortex-M3 架构中,总线被细分为多条专用的物理链路:
- ICode 总线(指令总线):专门连接内核的指令总线接口与内部 Flash。该总线几乎始终处于高频运行状态,是内核执行程序的取指专属通道。
- DCode 总线(数据总线):连接内核的数据总线接口,主要用于内核从 Flash 中读取常量数据(如 const 修饰的变量),或从 SRAM 中读写运行时的全局变量、局部变量与堆栈数据。
- System 总线(系统总线):内核与片上外设交互的桥梁。开发者在代码中进行的所谓寄存器编程(如配置 GPIO 模式、启动 ADC 转换),本质上都是 CPU 内核通过 System 总线对外设的控制寄存器发起读写操作。
- DMA 总线:独立于 CPU 的高速数据通道。它能够绕过内核,直接在存储器(SRAM/Flash)与外设数据寄存器之间建立点对点的批量数据传输链路。
2.2 总线矩阵与冲突仲裁
在高度并发的嵌入式系统中,CPU(通过 DCode/System 总线)与 DMA 控制器(通过 DMA 总线)极有可能在同一时钟周期内尝试访问同一个物理资源(例如,CPU 正在读取 SRAM 中的变量,而 DMA 也恰好要将 ADC 的采集结果写入该 SRAM 区域)。为了解决这种总线竞争,STM32 内部引入了 **多层 AHB 总线矩阵(AHB Bus Matrix)**机制。
总线矩阵本质上是一个复杂的交叉互联开关网络。它将网络一端的 总线主机 (Bus Master,如 CPU 内核、DMA 控制器)与另一端的总线从机(Bus Slave,如 Flash 接口、SRAM 控制器、AHB/APB 桥接器)进行动态路由编排,其核心调度机制如下:
- 多路并发与分时复用:当不同的主机访问不同的从机时(例如 CPU 通过 ICode 访问 Flash 取指,同时 DMA 访问 SRAM 搬运数据),总线矩阵允许这两条物理链路并行不悖地独立工作,极大提升了系统的吞吐量。
- 总线冲突与周期窃取 :当 CPU 与 DMA 同时向同一个目标(如 SRAM 控制器)发起总线请求时,总线矩阵内部的硬件仲裁器将介入。为了保证 DMA 传输(如高速串口接收或 ADC 连续采样)的硬实时性与数据完整性,仲裁器通常会赋予 DMA 更高的总线访问优先级。此时,CPU 对该总线的访问将被强制挂起一至数个时钟周期,这种底层硬件调度策略被称为 周期窃取,它是确保系统外设数据不丢失的核心机制。
2.3 存储器映射
DMA 的数据转运本质是对芯片内部物理地址空间的跨区读写。因此,明确各类资源的绝对物理地址是配置 DMA(设置源地址与目的地址)的先决条件。
STM32 基于 32 位的系统架构,具备统一的线性寻址空间,其理论最大寻址能力为 。物理存储器单元本身并无原生地址,是芯片内部的地址译码逻辑为 Flash、SRAM 及各个外设寄存器分配了固定的逻辑地址范围,这一过程即为存储器映射(Memory Mapping)。如下图存储器映射图所示:

在这 4GB 的地址空间中,ARM 架构规范将其粗线条地平均划分为 8 个核心区块(Block),每个区块大小严格定义为 512MB。具体分类见下表:
| 区块编号 | 存储区类型 | 地址范围 | 工程用途说明 |
|---|---|---|---|
| Block 0 | Code (程序存储区) | 0x0000 0000 ~ 0x1FFF FFFF | 存放启动向量表、固件/引导加载器、只读常量与代码;为上电与复位后 CPU 首次执行的区域(通常映射片上 Flash 或 ROM),写入需擦除/编程操作。 |
| Block 1 | SRAM (片上 SRAM) | 0x2000 0000 ~ 0x3FFF FFFF | 运行时可读写的主内存:堆栈、全局/静态变量、堆与 DMA 数据缓冲区;断电/复位后内容丢失,访问延迟低。 |
| Block 2 | Peripheral (片上外设) | 0x4000 0000 ~ 0x5FFF FFFF | 内部外设的寄存器映射区(GPIO、UART、ADC、定时器等);读/写会触发外设动作或状态变化,应以 volatile 语义访问并注意总线事务顺序。 |
| Block 3 | FSMC Bank 1~2(外部存储) | 0x6000 0000 ~ 0x7FFF FFFF | 通过 FSMC 等外设映射的外部并行存储(外部 SRAM、NOR Flash);访问受时序/总线宽度影响,可能存在更高延迟或需特殊映射/CS 信号。 |
| Block 4 | FSMC Bank 3~4(外部设备) | 0x8000 0000 ~ 0x9FFF FFFF | FSMC 的扩展区域,常用于 NAND、片选更多的外设或异构存储器;通常需要特定时序与命令序列访问。 |
| Block 5 | FSMC Register(FSMC 寄存器) | 0xA000 0000~ 0xBFFF FFFF | FSMC 控制器自身的寄存器与配置区,用于设置外部总线的时序、地址映射、数据宽度和片选行为。 |
| Block 6 | Reserved (保留区域) | 0xC000 0000~ 0xDFFF FFFF | 架构保留,不同芯片/实现可能映射特殊外设或保留为将来扩展;用户代码通常不使用此区。 |
| Block 7 | Cortex-M3 Internal(内核级寄存器) | 0xE000 0000~ 0xFFFF FFFF | Cortex-M3 内核外设(NVIC、SysTick、SCB、MPU 等)的寄存器区;用于中断/异常控制、系统定时与系统配置,访问需谨慎(影响全局行为)。 |
对于 DMA 数据搬运而言,我们最核心关注的是前三个 Block(Block 0 ~ Block 2):
2.3.1 Block 0:程序代码区 (Code)
该区块(0x0000 0000 ~ 0x1FFF FFFF)主要用于映射芯片内部的非易失性存储器(Non-Volatile Memory)。为了支持不同的启动模式和系统配置,该区块内部被精细划分为多个功能子区(这些逻辑区块是架构级别的划分;各子区的实际有效范围与设备可用容量由目标器件决定)。
| 区域 | 用途说明 | 地址范围 |
|---|---|---|
| 启动别名区 (Boot Alias) | 启动映射区域。根据 BOOT 引脚配置,该空间会被映射为 Flash、系统存储器或 SRAM 的别名,用于上电启动向量访问。 | 0x0000 0000 ~ 0x0007 FFFF |
| 预留 (Reserved) | 保留地址空间,不可访问。 | 0x0008 0000 ~ 0x07FF FFFF |
| Flash (程序存储器) | 片上 Flash 存储主区,存放用户应用程序机器指令集及只读常量数据。 | 0x0800 0000 ~ 0x0807 FFFF |
| 预留 (Reserved) | 保留地址空间,不可访问。 | 0x0808 0000 ~ 0x1FFF EFFF |
| 系统存储器 (System Memory) | 存储 ST 官方预置的 Bootloader 程序,专用于提供串口 ISP 下载引导。 | 0x1FFF F000 ~ 0x1FFF F7FF |
| 选项字节 (Option Bytes) | 用于配置 Flash 保护及启动参数(如读保护 RDP、看门狗选择等)。 | 0x1FFF F800 ~ 0x1FFF F80F |
虽然 Block 0 包含众多区域,但在配置 DMA 时,我们仅需关注片内 Flash(0x0800 0000 起始)与系统存储器(0x1FFF F000 起始)。由于这两者通常为只读属性,在 DMA 传输中,它们仅能作为 M2M(存储器到存储器) 模式下的只读数据源。
2.3.2 Block 1:运行内存区 (SRAM)
地址区间 0x2000_0000 ~ 0x3FFF FFFF 对应片内 SRAM(具体可用的物理 SRAM 容量与有效地址子区由具体芯片型号决定),是微控制器运行时的主存储区,用于保存程序执行过程中的易变数据------如全局/局部/静态变量以及堆(heap)与栈(stack)。
| 区域 | 用途说明 | 地址范围 |
|---|---|---|
| Block 1 (SRAM 64KB) | 片内主 SRAM 区,实际物理容量视具体芯片型号而定(如 STM32F103C8T6 的片上 SRAM 为 20 KB,而大容量如 STM32F103RE / STM32F103VET6 通常提供 64 KB SRAM,其映射范围为 0x2000 0000--0x2000 FFF) | 0x2000 0000 ~ 0x2000 FFFF |
| Block 1 (预留) | 保留地址空间,用于更高容量型号的 SRAM 扩展。 | 0x2001 0000 ~ 0x3FFF FFFF |
由于 SRAM 具备零等待周期的高速读写特性,在 DMA 数据搬运任务中,其性能优势至关重要。无论是 ADC 采集结果的实时存储,还是串口发送数据的暂存,软件层定义的各类数据缓冲区均通过地址对齐映射至此物理地址段。因此,Block 1 构成了 DMA 传输中最核心的内存端点,既可作为高速数据源,也可作为数据接收的目的地。
2.3.3 Block 2:片上外设区 (Peripherals)
地址区间 0x4000 0000 ~ 0x5FFF FFFF 是片上外设寄存器的统一映射空间,其中包含各外设的控制、状态及数据寄存器。为了在性能与功耗之间取得平衡,STM32 通过内部总线桥接机制,将外设分别连接到不同层级的系统总线(如 AHB、APB1、APB2),并在该地址空间中形成 3 个对应的地址子区(参考:STM32F10xxx参考手册 - 2.3 存储器映像)。
| 区域 | 用途说明 | 地址范围 |
|---|---|---|
| Block 2 (APB1) | 低速总线外设(如 USART2/3、TIM2-7 等)的寄存器映射区。 | 0x4000 0000 ~ 0x4000 77FF |
| Block 2 (APB2) | 高速总线外设(如 GPIO、USART1、ADC1、TIM1 等)的寄存器映射区。 | 0x4001 0000 ~ 0x4001 3FFF |
| Block 2 (AHB) | 系统级高速外设(如 SDIO、RCC 等)的寄存器映射区。 | 0x4001 8000 ~ 0x5003 FFFF |
在明确了外设所属的总线子区后,配置 DMA 的关键在于精确定位要搬运的外设寄存器的物理地址------即用"外设基地址 + 寄存器偏移量"的方式计算出目标寄存器的绝对地址,并将该地址写入 DMA 的外设地址寄存器(同时正确设置传输方向、数据宽度与对齐)。基址和偏移量必须以器件参考手册/寄存器手册为准,否则会导致读写错误或硬件异常。
计算示例:假设我们需要 DMA 自动搬运 ADC1 的转换结果。查阅手册可知 ADC1 挂载于 APB2 总线,其分配的基地址 ADC1_BASE = 0x4001 2400。,其数据寄存器 ADC_DR 相对于基地址的偏移为 0x4C,则 ADC_DR 的物理地址为:
因此,应将 0x4001 244C 作为 DMA 的外设端地址(source 或 destination,取决于传输方向)填写到 DMA 描述符或寄存器中。
3. DMA 硬件架构与工作机制
在 STM32 的总线拓扑中,DMA 控制器作为独立于 Cortex-M3 内核的总线主机(Bus Master),直接挂载于 AHB 系统总线上。为了深刻理解 DMA 的行为,我们需要解构其内部硬件框图。
从硬件微架构来看,DMA 控制器的核心工作机制可归结为三大功能模块协同运行:DMA 请求接口 、独立数据通道 与总线仲裁器。

3.1 DMA 请求
外设若需通过 DMA 搬运数据,必须依赖底层的硬件请求与应答机制。其标准执行流程如下:
(1)外设触发(请求阶段)
当外设的特定事件被触发(如 ADC 转换完成、USART 发送数据寄存器空)时,其硬件电路会自动向 DMA 控制器发送一个有效的 DMA 请求信号(DMAReq)。该信号代表外设已准备好进行数据交换。
(2)优先级仲裁(竞争阶段)
DMA 控制器在接收到 DMAReq 后,并不会立即执行。内部仲裁器会根据当前各通道的优先级配置进行调度,并向 AHB 总线矩阵申请系统总线的控制权。只有在获得总线使用权后,事务才会进入下一阶段。
(3)应答确认(握手阶段)
DMA 控制器在完成优先级仲裁并获取 AHB 总线控制权后,会向该外设回传一个应答信号(DMAAck)。
(4)数据搬运(执行阶段)
外设在收到应答信号后,即刻撤销当前的请求信号,随后,DMA 控制器接管总线,启动单次(或突发)数据搬运操作,将数据从源地址传输至目的地址。
这种一问一答的交互,确保了数据传输与外设状态的严格同步,防止数据丢失或被覆盖。
3.2 DMA 通道
STM32 的 DMA 控制器通过多通道架构实现高效的数据并发处理。在物理设计上,每个通道都是一个独立的可编程流控制单元。3.2.1 通道架构与配置独立性
STM32 内部集成了多个独立的数据通道(例如,大容量型号中 DMA1 具备 7 个通道,DMA2 具备 5 个通道)。每一个通道本质上是一条逻辑独立的数据传输管道,开发者可以为各通道分别配置以下关键参数:
-
端点物理基地址:源地址与目的地址。
-
传输方向:存储器与外设间的双向调度。
-
传输属性:包括数据位宽、地址增量模式及优先级。
3.2.2 硬件触发的映射约束
在硬件触发模式下,DMA 通道并不是通用且可任意分配的。每个 DMA 通道通过芯片内部的硬件电路,与特定的外设请求源实现了硬连线映射(Hardwired Mapping)。如下图 DMA1 通道请求映射示意图所示:

如上图所示,DMA 的硬件触发架构遵循以下逻辑:
- 多对一映射逻辑:通过硬件选择器(MUX),一个 DMA 通道可以接收来自多个不同外设的请求信号。例如,在 DMA1 控制器中,通道 1 (DMA1_Channel1) 被映射至 ADC1、TIM2_CH3 及 TIM4_CH1。
- 固定分配原则:这种映射关系在芯片出厂时已固化,开发者无法通过软件将 ADC1 的请求重定向至通道 2。因此,在进行系统方案设计与引脚分配时,必须严格查阅参考手册中的《DMA 请求映像表》来锁定特定的硬件通道。
- 产品型号差异:在工程移植时需注意,高性能外设(如 ADC3、SDIO、TIM8)的请求通常仅路由至大容量或互联型产品的 DMA2 控制器中。若在小容量芯片上开发,需核对资源是否存在。在进行通道分配或移植时,须参考目标器件的参考手册。
3.2.3 资源冲突与分时复用原则
尽管一个通道在硬件层面上支持多个外设请求源(如图中通过或门逻辑接入),但在软件配置与实际运行阶段,必须遵循以下约束:
-
单点激活约束:在特定的传输任务中,同一时刻只能有一个硬件触发源被使能并占用该通道。例如,若配置了通道 1 为 ADC1 服务,则同一时间内该通道无法响应 TIM4_CH1 的触发。
-
总线分时复用:当多个通道(如通道 1 至通道 7)同时产生传输请求时,DMA 控制器内部的仲裁器将根据优先级配置,以分时复用的方式依次获取 AHB 总线控制权,确保数据流有序转运。
3.3 仲裁器
当多个外设在同一时钟周期内同时向不同 DMA 通道发出传输请求时,DMA 控制器内部的仲裁单元会立即介入,按照两级梯队机制进行总线抢占权的优先级调度:
- 软件仲裁(配置寄存器):开发者可通过配置 DMA_CCRx 寄存器,为各个通道独立分配 4 个软件优先级等级:非常高(Very High)、高(High)、中(Medium)与低(Low)。
- 硬件仲裁(通道物理编号):若多个通道被配置为相同的软件优先级,仲裁器将根据通道的硬件编号决定绝对优先级。通道编号越低,优先级越高(例如,通道1优先于通道2)。
在大容量与互联型产品中,若 DMA1 与 DMA2 发生 AHB 总线竞争,硬件默认赋予 DMA1 更高的总线访问优先级。
4. DMA 配置参数解析
配置 DMA 的核心逻辑,本质上是为其定义一个完整的数据转运事务。我们需要明确数据的传输方向与位置 、地址增量模式 、位宽对齐规则 以及搬运总量与循环模式。
4.1 传输方向与基地址
DMA 的数据转运模型被抽象为两个端点(Endpoint)之间的点对点传输。这两个端点在寄存器层面被定义为 外设站点 与 存储器站点 ,其物理基地址分别由外设地址寄存器 DMA_CPARx 与 存储器地址寄存器 DMA_CMARx界定。
在配置基地址时,必须严格遵循芯片的内存映射范围。以 STM32F103C8T6 为例,其内部地址空间布局如下:

传输的绝对方向与触发机制由 DMA_CCRx 寄存器中的 DIR(数据传输方向)位与 MEM2MEM(存储器到存储器模式)位联合定义:
- **外设到存储器 (P2M):**配置为 DIR = 0, MEM2MEM = 0。传输受外设硬件信号(DMAReq)驱动。例如在 ADC 连续采样中,每当转换结束,DMA 即将 DMA_CPARx 指向的 ADC 数据寄存器值搬运至 DMA_CMARx 指向的 SRAM 缓冲区。
- **存储器到外设 (M2P):**配置为 DIR = 1, MEM2MEM = 0。同样依赖硬件触发。例如在串口高速发送时,只要 USART 判定发送寄存器为空(TXE),即触发 DMA 从 SRAM 取出数据并写入 USART 数据寄存器。
- **存储器到存储器 (M2M):**配置为 MEM2MEM = 1。该模式采用软件自动触发,一旦通道使能,DMA 将不等待外部硬件请求,直接以最高总线速率执行数据搬运(如 SRAM 内部的数据复制,或将 Flash 中的字库数据拷贝至 SRAM),直至传输计数器递减清零。
**注意:**尽管寄存器命名区分了外设地址(CPAR)与存储器地址(CMAR),但在底层硬件架构中,这两个站点本质上是两条平等的总线访问通道。这种设计在 M2M(存储器到存储器)模式下体现得最为彻底:此时 DMA 不再受限于特定的硬件触发信号,CPAR 与 CMAR 彻底回归为两个通用的 32 位地址指针。开发者可以完全忽略其字面含义,将 CPAR 指向 Flash 或 SRAM 空间作为源地址,将 CMAR 指向另一块 SRAM 空间作为目的地址。寄存器的命名仅代表其在典型硬件流控场景中的默认角色,而物理地址的映射才是决定访问对象的真实准则。
4.2 地址增量模式
当涉及多笔数据的连续传输时,必须确定单次搬运完成后,端点的地址指针是否自动步进。地址步进的偏移量由配置的数据宽度决定。
- **外设地址增量 (PINC):**控制 DMA_CPARx 的指针行为。
- **存储器地址增量 (MINC):**控制 DMA_CMARx 的指针行为。
工程实战准则:
- **处理连续内存块:**当端点映射为 SRAM 中的数组或缓冲区时,必须使能增量模式(如 MINC = 1),以确保数据依次排列并防止旧数据被覆盖。
- **对接固定寄存器:**当端点映射为 USART_DR 或 ADC_DR 等固定外设数据寄存器时,必须禁用地址增量(如 PINC = 0),确保 DMA 始终访问同一个物理地址。
4.3 数据位宽与对齐规则
为了保证总线访问的对齐与数据完整性,开发者需通过 PSIZE[1:0] 与 MSIZE[1:0] 分别配置两侧单次读写的位宽。可选配置包括:8位(Byte)、16位(HalfWord)或 32位(Word)。
当源端与目的端的数据宽度不一致时,硬件电路会自动执行对齐或截断操作,其底层规则如下表所示:
| 源端数据位宽 | 目的端数据位宽 | 硬件底层操作逻辑 |
|---|---|---|
| 8位 | 16位 / 32位 | 读取源端 8 位数据后,在写入目的地址时,低 8 位填充有效数据,高位硬件自动填充 0。 |
| 16位 | 32位 | 读取源端 16 位数据后,在写入目的地址时,低 16 位填充有效数据,高位硬件自动填充 0。 |
| 16位 / 32位 | 8位 | 读取源端数据后,直接截取最低 8 位有效位写入目的地址,高位数据被硬件丢弃(可能导致精度丢失)。 |
| 32位 | 16位 | 读取源端数据后,直接截取最低 16 位有效位写入目的地址,高 16 位被硬件丢弃。 |
4.4 传输计数器与循环模式
DMA 内部集成的 16 位可编程自减计数器 DMA_CNDTRx 决定了单次事务的数据总量(最大为 65535)。每搬运完成一个数据单位,计数器自动减 1。当计数器递减至 0 时,根据 DMA_CCRx 寄存器中 CIRC(循环模式) 位的配置,DMA 将呈现出两种完全不同的生存周期行为:
4.4.1 正常单次模式 (Normal Mode, CIRC = 0)
(1)行为描述
在正常单次模式下,DMA 完成预先设定的数据量传输后会终止当前事务:硬件自动清除通道使能位(即 DMA_CCRx 中的 EN 位清零),通道进入挂起状态,等待软件重新配置与再次使能。此过程是一次性、确定性的------传输结束后需要软件显式干预才能发起下一次传输。
(2)典型应用场景
适用于单次或间断性的搬运任务,例如一次性内存镜像、单帧数据采集或文件块写入等场景。
(3)重新发起传输的推荐安全流程
若需发起新一轮传输,必须严格遵循以下序列以确保逻辑闭环:
- **状态确认:**轮询或通过中断确认 EN 位已被硬件清零,确保前一事务已彻底终止。
- **清理标志位:**手动清除该通道的相关状态 / 中断标志(如 TC、HT、TE),防止旧的完成标志导致中断逻辑误触发。
- **配置计数器:**向 DMA_CNDTRx 重新写入新的传输计数值 N。
- **更新基地址(可选):**若数据源或目的地需变更地址,需更新 CPARx 或 CMARx 寄存器。
- **激活通道:**重新将 DMA_CCRx.EN 位置 1,触发新的单次传输事务。
(4)注意事项
- 切勿在通道仍然被使能(EN = 1)时直接写 DMA_CNDTRx 或地址寄存器,应先禁能再写。
- 在重新启用前务必清除旧的完成/错误标志,避免引起误中断或错误处理。
- 若传输与外设操作紧耦合(例如外设 DMA 请求由外设中断触发),确保外设在通道配置好且使能后才开始产生请求,或在外设端实现短暂屏蔽以避免竞态。
4.4.2 自动循环模式 (Circular Mode, CIRC = 1)
(1)行为描述
在循环模式下,DMA 在每次计数器归零(CNDTR 清零)时不关闭通道,而是由硬件自动完成以下事项:
- 将内部的内存地址指针(MemAddr)恢复到初始设置的基地址;
- 将计数器 CNDTR 自动重载为先前设定的初值;
- 立即启动下一轮搬运,从而实现连续、无间断的数据流动。
(2)典型应用场景
非常适合需要持续采样或循环存取的场景,例如 ADC 连续扫描、音频流处理、以及基于环形缓冲区(Ring Buffer)的数据流收发。
(3)运行/调整的操作要点(步骤与建议)
- 配置好初始 PeriphAddr、MemAddr 与 DMA_CNDTRx(初值),并设置 CIRC = 1。
- 启用通道(DMA_CCRx.EN = 1)后,DMA 将在无需软件干预的情况下循环搬运数据。
- 在循环模式运行期间,若需调整传输数量(CNDTR)或目标地址,必须先通过软件将 DMA_CCRx 的 EN 位清零(即禁能通道)。待通道停止工作后,再修改相关寄存器,最后重新置位 EN(使能通道)以生效。
(4)注意事项
- 不要在通道运行时直接修改 DMA_CNDTRx 或地址寄存器;必须先禁能再改写,否则会引起不确定行为或数据错位。
- 使用半搬运(Half-Transfer, HT)中断时,处理中要小心维护与 DMA 的并发读写关系:中断中应尽量只读取已经稳态写入的缓冲区部分,避免与 DMA 写操作发生竞态。
- 确认外设与 DMA 的触发/时序一致性(例如 ADC 的触发频率与 DMA 的循环节拍),防止数据错位或采样丢失。
- 当系统需要短暂停止数据流(例如切换缓冲区或更改采样参数)时,应按"禁能 → 更新寄存器 → 清标志 → 重新使能"的顺序执行,以保证状态一致性。
5. DMA 配置流程
在理解了 DMA 的内部架构与硬件特性后,本章将通过两个典型工程实验------存储器到存储器转运 与外设到存储器转运,演示如何利用标准外设库将底层逻辑转化为严谨的 C 语言驱动配置。
5.1 存储器到存储器(M2M)数据转运
本实验旨在将一段 SRAM 连续内存块(数组 DataA)的数据完整复制至另一内存块(数组 DataB)。由于数据源与目的均位于系统内部存储器,无需外部硬件外设的触发信号,故采用**存储器到存储器模式(M2M)**配合软件触发机制实现。
实现 M2M 模式的数据转运,需遵循以下四个核心步骤:
- **开启总线时钟:**使能 DMA1 所挂载的 AHB 总线时钟。
- **初始化 DMA 参数:**填充 DMA_InitTypeDef 初始化结构体,配置地址映射、传输方向、总线位宽、自增模式及 M2M 使能。
- **封装传输控制函数:**针对正常模式(Normal),编写符合芯片手册"禁能 ---> 重载 ---> 使能"规范的触发函数。
- **主循环调用与验证:**在应用层调用控制函数并通过状态寄存器验证转运结果。
5.1.1 开启 DMA 时钟
在进行任何外设寄存器配置前,解除总线时钟门控是必要前提。DMA 控制器作为总线矩阵上的主设备,挂载在高速的 AHB 总线上。若未提前使能其时钟,CPU 发出的所有针对 DMA 寄存器映射区域的写指令均会被总线忽略,导致初始化失败。
cpp
/* 开启 DMA1 的 AHB 总线时钟 */
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
5.1.2初始化 DMA 参数(核心配置)
此步骤通过填充 DMA_InitTypeDef 结构体来确定 DMA 的工作逻辑。通过代码段逐步解析各个参数的含义:
首先,指定数据的来源、去向以及传输的方向。在 STM32 的 DMA 架构中,每个通道的控制逻辑内部都包含一个二选一选择器(Multiplexer),用于决定当前的传输由谁驱动。

-
硬件请求关断与软件触发接入:如上图所示,当 DMA_M2M 设置为 DMA_M2M_Enable 时,本质上是置位了该通道控制寄存器(DMA_CCRx)中的 MEM2MEM 位。此时,该通道会切断与左侧外设硬件请求信号(如 ADC1、TIMx 等)的逻辑关联,转而接入内部的"软件触发"路径。
-
地址代号化:DMA 寄存器组抽象出了外设地址(DMA_PeripheralBaseAddr)与存储器地址(DMA_MemoryBaseAddr)两个独立指针。在存储器到存储器(M2M)模式下,这两者仅作为总线访问的地址代号。根据 STM32 的总线矩阵架构,这两个指针均可指向 SRAM 区域。
-
传输方向控制:由于两端均为存储器,数据的流向由 DMA_DIR 参数决定。在本实验中,我们将源数组地址赋予"外设"指针,目的数组地址赋予"存储器"指针,并设置方向为 PeripheralSRC(即外设指针作为源端)。
cpp
DMA_InitTypeDef DMA_InitStructure;
/* 1. 配置地址与方向 */
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)AddrA; // 源基地址:映射为 DataA 数组首地址(在 M2M 模式下此处可为任意内存地址,API 名称为 PeripheralBaseAddr 为历史命名)
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)AddrB; // 目的基地址:映射为 DataB 数组首地址
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; // 传输方向:源为 Peripheral 指针(本例代表 AddrA),目的为 Memory(AddrB)
DMA_InitStructure.DMA_M2M = DMA_M2M_Enable; // 触发机制:开启存储器到存储器模式(内部时钟驱动)
接着,配置数据位宽与地址自增模式。由于是对整型数组进行逐个元素的批量拷贝,必须确保源端与目的端的地址指针在每次传输一个元素后,自动向后偏移。本例中配置位宽为 8 位(字节),这意味着内部硬件指针每次自动递增 1 个字节地址单元,严格防止内存踩踏与数据覆盖。
cpp
/* 2. 配置位宽与自增模式 */
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; // 源端位宽:8位(字节)
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable; // 源端地址自增:使能,指向数组下一个元素
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; // 目的端位宽:8位(字节)
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; // 目的端地址自增:使能,实现连续写入
最后,设置单次事务传输的数据总量工作模式。DMA_BufferSize 最终会被写入内部的传输计数寄存器 DMA_CNDTRx。配置为正常模式(Normal)意味着当该计数器递减至 0 时,硬件会自动清除通道使能位(EN),总线访问立即挂起,从而安全地结束本次批量传输。
cpp
/* 3. 配置总量与工作模式 */
DMA_InitStructure.DMA_BufferSize = Size; // 传输计数:设定单次事务搬运的数据总量
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; // 工作模式:正常单次模式(计数清零后停止)
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; // 通道仲裁优先级:中等
DMA_Init(DMA1_Channel1, &DMA_InitStructure); // 将结构体参数写入 DMA1 通道 1 物理寄存器
/* 规范要求:初始化完成后保持禁能,由应用层按需触发 */
DMA_Cmd(DMA1_Channel1, DISABLE);
5.1.3:封装软件触发与等待逻辑
由于采用了正常模式(Normal),DMA 通道在完成一轮指定 Size 的转运后会自动关闭。若需发起下一次转运,必须重置环境。
依据 STM32 参考手册的约束:在对传输计数器 DMA_CNDTRx 进行重新赋值前,必须确保通道控制寄存器中的使能位(EN)为 0。因此,标准的代码范式必须严格遵循 禁能 ---> 赋值重载 ---> 使能的安全时序。
cpp
void MyDMA_Transfer(void)
{
/* 1. 禁能通道:只有在 Disable 状态下,传输计数器才能被重新写入 */
DMA_Cmd(DMA1_Channel1, DISABLE);
/* 2. 重载计数器:写入本次需要转运的次数 */
DMA_SetCurrDataCounter(DMA1_Channel1, MyDMA_Size);
/* 3. 使能通道:DMA 开始工作 */
DMA_Cmd(DMA1_Channel1, ENABLE);
/* 4. 同步等待:轮询传输完成标志位 (TC),确保数据转运彻底结束 */
while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);
/* 5. 清除标志位:硬件标志位需手动清除,以便下次判断 */
DMA_ClearFlag(DMA1_FLAG_TC1);
}
5.2 外设到存储器(ADC 多通道扫描)
本实验旨在利用 DMA 实现 ADC 多通道数据的自动转运。ADC1 将工作在扫描模式下采集 PA0 - PA3 通道,DMA 负责将每次转换后的结果实时存入 AD_Value 数组中,有效解决手动读取时可能发生的数据覆盖(Overrun)问题。
实现 ADC-DMA 联动采集,需遵循以下五个核心步骤:
- 开启相关时钟:使能 ADC1、GPIOA 以及 DMA1 的时钟,并配置 ADC 总线时钟分频器。
- 配置 ADC 与 GPIO:将对应引脚设为模拟输入,配置规则组序列,并使能 ADC 的连续转换与扫描模式。
- 配置 DMA 参数:基地址固定为 ADC 数据寄存器,目标地址为 SRAM 数组,配置非对称的地址自增模式(外设不增,存储器增),开启循环模式,禁用 M2M 模式。
- 打通联动信号链:使能 DMA 通道,并调用 ADC_DMACmd 开启外设向 DMA 发送请求的开关,最后使能 ADC 外设。
- 校准与软件触发:完成 ADC 自校准后,启动软件触发。
5.2.1时钟与 GPIO 配置
ADC 内部包含极其敏感的模拟采样与量化电路,对工作频率有严格的电气约束。STM32F103 的 ADC 挂载在 APB2 总线上,其最大允许时钟频率为 14 MHz。在系统主频为 72 MHz 的标准配置下,必须通过 RCC_ADCCLKConfig 调用专用的预分频器进行 6 分频,得到 72 MHz / 6 = 12 MHz 的安全工作时钟。
同时,需将参与采样的物理引脚配置为模拟输入模式,以断开内部的施密特触发器和数字接口。
cpp
uint16_t AD_Value[4]; // 定义全局数组,用作 DMA 传输的目的地缓存
/* 1. 开启外设总线时钟 */
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); // 开启 ADC1 所在 APB2 总线时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); // 开启 GPIOA 时钟,供模拟输入引脚使用
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 开启 DMA1 所在 AHB 总线时钟
/* 2. 配置 ADC 时钟分频:$72\text{ MHz} / 6 = 12\text{ MHz}$(约束条件:不超过 14MHz) */
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
/* 3. GPIO 模拟输入初始化:配置 PA0-PA3 */
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; // 设置为模拟输入模式,防止数字电路引入噪声干扰
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
5.2.2 ADC 扫描与连续模式配置
此部分配置定义了底层数据源的触发频率与生成逻辑。当 ADC 配置为扫描模式(Scan)与连续转换模式(Continuous)后,其内部硬件状态机将按照既定序列(CH0 -> CH1 -> CH2 -> CH3)循环执行模拟信号的采样与量化。
每当单通道转换完成并更新结果寄存器(ADC_DR)时,ADC 硬件会自动向 DMA 控制器发送一个 DMAReq(DMA 请求信号)。该信号作为总线事务的触发源,驱动 DMA 搬运当前 ADC_DR 中的量化数据,从而实现外设转换速度与总线传输速率的硬件级同步。
cpp
/* 1. 配置规则组扫描序列:定义 ADC 扫描的物理通道与顺序 */
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); // 序列位置 1,映射至通道 0
ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5); // 序列位置 2,映射至通道 1
ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_55Cycles5); // 序列位置 3,映射至通道 2
ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_55Cycles5); // 序列位置 4,映射至通道 3
/* 2. ADC 运行模式初始化 */
ADC_InitTypeDef ADC_InitStructure;
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; // 独立模式:仅使用 ADC1,不涉及双 ADC 同步
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; // 数据对齐:12 位量化结果右对齐于 16 位寄存器
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; // 触发源:禁用外部事件触发,采用软件触发
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; // 连续转换模式:当前序列扫描完毕后自动重启下一轮
ADC_InitStructure.ADC_ScanConvMode = ENABLE; // 扫描模式:允许多通道按序列轮转
ADC_InitStructure.ADC_NbrOfChannel = 4; // 序列总长度:定义本次扫描包含 4 个通道
ADC_Init(ADC1, &ADC_InitStructure);
5.2.3 DMA 通道参数配置(非对称联动)
此步骤是实验的核心。ADC 的多个通道转换结果共用一个 DR 寄存器,因此 DMA 读取时地址不能自增;而目标数组必须自增,以便依次存放不同通道的数据。
- **非对称地址增量:**由于 ADC 4 个通道的转换结果均分时复用地输出至唯一物理寄存器 ADC1->DR,因此 DMA 的读取源地址必须绝对固定(外设地址不增)。而目标 SRAM 数组需要存放 4 个独立的通道值,故写入地址必须随之向后偏移(存储器地址自增)。
- **循环模式:**由于 ADC 处于连续转换状态,为了防止数组越界,DMA 必须开启循环模式。当传输计数器搬运完 4 个数据(即一轮完整扫描)归零时,DMA 硬件会自动将 DMA_CNDTRx 重载为 4,并将存储器指针自动返回 AD_Value 数组首地址,实现数据的连续循环刷新。
cpp
DMA_InitTypeDef DMA_InitStructure;
/* 1. 地址映射:从单一 ADC 数据寄存器映射至 SRAM 内存数组 */
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; // 源地址:固定为 ADC1 数据寄存器地址
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)AD_Value; // 目的地址:映射为全局数组首地址
/* 2. 核心增量逻辑:外设地址静态固定,存储器地址动态步进 */
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 外设端禁能自增,始终读取 DR 寄存器
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; // 存储器端使能自增,按通道顺序填充数组
/* 3. 数据位宽匹配:12 位 ADC 结果需占用 16 位寄存器空间 */
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; // 源端按半字(16位)读取
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; // 目的端按半字(16位)写入
/* 4. 生命周期控制:开启循环模式,承接 ADC 的连续转换节拍 */
DMA_InitStructure.DMA_BufferSize = 4; // 单周期搬运次数:与序列中定义的 4 个通道对应
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; // 传输模式:循环模式,计数器归零后自动重载恢复指针
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; // 触发源配置:禁用软件直接触发,等待外设 ADC 硬件请求
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; // 仲裁优先级配置
DMA_Init(DMA1_Channel1, &DMA_InitStructure);
5.2.4 链路激活与启动
在外设参数与 DMA 通道初始化完成后,必须建立两者的硬件控制链路。调用 ADC_DMACmd 函数本质上是置位了 ADC 控制寄存器 2(ADC_CR2)中的 DMA 位。
该位在硬件层级决定了 ADC 转换结束信号(EOC)是否会被转发至 DMA 控制器作为传输请求。只有当此位逻辑使能时,ADC 内部的触发脉冲才能通过硬件选通电路抵达 DMA 请求映射逻辑。最后通过软件指令启动 ADC 的首轮转换,系统即可在无 CPU 轮询参与的情况下,进入由硬件信号驱动的自动化采样与数据搬运闭环。
cpp
/* 1. 激活 DMA 通道:使能 DMA1_Channel1 状态机,进入就绪等待模式 */
DMA_Cmd(DMA1_Channel1, ENABLE);
/* 2. 建立请求连接:置位 ADC_CR2 寄存器的 DMA 控制位,使能 ADC 传输请求信号发生器 */
ADC_DMACmd(ADC1, ENABLE);
/* 3. 激活 ADC 外设:接通模拟电路供电及工作时钟 */
ADC_Cmd(ADC1, ENABLE);
/* 4. ADC 硬件自校准流程:消除内部电容制造误差,确保量化精度 */
ADC_ResetCalibration(ADC1); // 复位校准寄存器
while (ADC_GetResetCalibrationStatus(ADC1) == SET); // 阻塞等待复位完成
ADC_StartCalibration(ADC1); // 启动 A/D 校准进程
while (ADC_GetCalibrationStatus(ADC1) == SET); // 阻塞等待校准结束
/* 5. 启动采样闭环:触发 ADC 执行初次转换,后续转换将由连续模式及 DMA 循环模式硬件维持 */
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
6. 本章节实验
6.1 DMA直接数据转运(存储器到存储器)
6.1.1 实验目标
-
掌握 DMA 存储器到存储器转运原理:理解如何利用 DMA 的软件触发模式(M2M)建立独立于 CPU 的高速数据复制通道。
-
熟悉 DMA 核心参数配置:掌握 DMA_Init() 中关于源/目的地址、数据宽度(Byte)、地址增量模式及传输方向的底层设定规则。
-
理解传输状态机与重装载机制:学习在正常传输模式(Normal Mode)下,单次传输完成后如何通过标准的复位时序(失能 -> 重写计数器 -> 使能)重新激活 DMA 通道。
6.1.2 硬件设计

6.1.3 软件设计
本实验采用软件触发模式实现 SRAM 内部数组之间的数据搬运,具体流程如下:
(1)数据源与目标定义模块
- 内存分配:在 SRAM 中定义源测试数组 DataA 与目的数组 DataB。
- 外围初始化:初始化 OLED 显示屏,用于直观映射两个数组的内存物理首地址及内部数据变化。
(2)DMA 底层配置模块(基于 MyDMA_Init)
- 时钟与通道配置:开启 DMA1 时钟,任意选择一个通道(如 Channel1)。
- 传输规则设定:配置外设站点与存储器站点的数据宽度均为 8 位字节(Byte),两端地址均开启自增(Inc_Enable)。
- 触发模式选择:传输模式设定为正常模式(Normal),开启存储器到存储器触发位(M2M_Enable),并在初始化末尾保持 DMA 失能,等待手动调用。
(3)转运控制与显示调度逻辑(主程序)
-
数据变更模拟:在主循环中不断对 DataA 的元素执行自增运算,模拟不断刷新的数据源。
-
封装传输触发器:调用自定义封装的 MyDMA_Transfer() 函数,其内部执行关闭通道、重写 DMA_CNDTR 寄存器并重新使能通道的操作,最后阻塞等待 TC(传输完成)标志位置位并清零。
-
状态观测:在调用触发函数的前后,分别在 OLED 上打印 DataA 和 DataB 的数据,验证转运结果。
具体代码如下:
main.c文件:
cpp
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "MyDMA.h"
uint8_t DataA[] = {0x01, 0x02, 0x03, 0x04}; //定义测试数组DataA,为数据源
uint8_t DataB[] = {0, 0, 0, 0}; //定义测试数组DataB,为数据目的地
int main(void)
{
/*模块初始化*/
OLED_Init(); //OLED初始化
MyDMA_Init((uint32_t)DataA, (uint32_t)DataB, 4); //DMA初始化,把源数组和目的数组的地址传入
/*显示静态字符串*/
OLED_ShowString(1, 1, "DataA");
OLED_ShowString(3, 1, "DataB");
/*显示数组的首地址*/
OLED_ShowHexNum(1, 8, (uint32_t)DataA, 8);
OLED_ShowHexNum(3, 8, (uint32_t)DataB, 8);
while (1)
{
DataA[0] ++; //变换测试数据
DataA[1] ++;
DataA[2] ++;
DataA[3] ++;
OLED_ShowHexNum(2, 1, DataA[0], 2); //显示数组DataA
OLED_ShowHexNum(2, 4, DataA[1], 2);
OLED_ShowHexNum(2, 7, DataA[2], 2);
OLED_ShowHexNum(2, 10, DataA[3], 2);
OLED_ShowHexNum(4, 1, DataB[0], 2); //显示数组DataB
OLED_ShowHexNum(4, 4, DataB[1], 2);
OLED_ShowHexNum(4, 7, DataB[2], 2);
OLED_ShowHexNum(4, 10, DataB[3], 2);
Delay_ms(1000); //延时1s,观察转运前的现象
MyDMA_Transfer(); //使用DMA转运数组,从DataA转运到DataB
OLED_ShowHexNum(2, 1, DataA[0], 2); //显示数组DataA
OLED_ShowHexNum(2, 4, DataA[1], 2);
OLED_ShowHexNum(2, 7, DataA[2], 2);
OLED_ShowHexNum(2, 10, DataA[3], 2);
OLED_ShowHexNum(4, 1, DataB[0], 2); //显示数组DataB
OLED_ShowHexNum(4, 4, DataB[1], 2);
OLED_ShowHexNum(4, 7, DataB[2], 2);
OLED_ShowHexNum(4, 10, DataB[3], 2);
Delay_ms(1000); //延时1s,观察转运后的现象
}
}
MyDMA.c文件:
cpp
#include "stm32f10x.h" // Device header
uint16_t MyDMA_Size; //定义全局变量,用于记住Init函数的Size,供Transfer函数使用
/**
* 函 数:DMA初始化
* 参 数:AddrA 原数组的首地址
* 参 数:AddrB 目的数组的首地址
* 参 数:Size 转运的数据大小(转运次数)
* 返 回 值:无
*/
void MyDMA_Init(uint32_t AddrA, uint32_t AddrB, uint16_t Size)
{
MyDMA_Size = Size; //将Size写入到全局变量,记住参数Size
/*开启时钟*/
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //开启DMA的时钟
/*DMA初始化*/
DMA_InitTypeDef DMA_InitStructure; //定义结构体变量
DMA_InitStructure.DMA_PeripheralBaseAddr = AddrA; //外设基地址,给定形参AddrA
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; //外设数据宽度,选择字节
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable; //外设地址自增,选择使能
DMA_InitStructure.DMA_MemoryBaseAddr = AddrB; //存储器基地址,给定形参AddrB
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; //存储器数据宽度,选择字节
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //存储器地址自增,选择使能
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //数据传输方向,选择由外设到存储器
DMA_InitStructure.DMA_BufferSize = Size; //转运的数据大小(转运次数)
DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; //模式,选择正常模式
DMA_InitStructure.DMA_M2M = DMA_M2M_Enable; //存储器到存储器,选择使能
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; //优先级,选择中等
DMA_Init(DMA1_Channel1, &DMA_InitStructure); //将结构体变量交给DMA_Init,配置DMA1的通道1
/*DMA使能*/
DMA_Cmd(DMA1_Channel1, DISABLE); //这里先不给使能,初始化后不会立刻工作,等后续调用Transfer后,再开始
}
/**
* 函 数:启动DMA数据转运
* 参 数:无
* 返 回 值:无
*/
void MyDMA_Transfer(void)
{
DMA_Cmd(DMA1_Channel1, DISABLE); //DMA失能,在写入传输计数器之前,需要DMA暂停工作
DMA_SetCurrDataCounter(DMA1_Channel1, MyDMA_Size); //写入传输计数器,指定将要转运的次数
DMA_Cmd(DMA1_Channel1, ENABLE); //DMA使能,开始工作
while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET); //等待DMA工作完成
DMA_ClearFlag(DMA1_FLAG_TC1); //清除工作完成标志位
}
6.1.4 实验现象
启动程序后,OLED 屏幕分别显示 DataA 与 DataB 的内存物理首地址。在主循环中,DataA 的数据不断发生变化,而 DataB 初始全为 0。当调用 DMA 转运函数后,无需任何 CPU 赋值循环指令,DataB 的数据瞬间被覆盖并变得与 DataA 完全一致,证实了 DMA 存储器到存储器转运的高效性。
6.2 ADC多通道扫描与DMA连续转运(外设到存储器)
6.2.1 实验目标
- 掌握硬件触发与外设级联原理:理解 ADC 外设转换完成信号如何作为 DMA 的硬件请求源(DMAReq),实现外设到存储器的自动数据搬运。
- 熟练配置扫描与循环转运模式:学习 ADC 连续扫描模式与 DMA 循环模式(Circular Mode)的协同配合,解决多通道高速采集下的数据覆盖(Overrun)问题。
- 实现硬件自动化采集架构:构建一条完全独立于 CPU 时钟调度的实时采集与搬运总线链路,极大释放系统算力。
6.2.2 硬件设计

6.2.3 软件设计
本实验采用 ADC 连续扫描配合 DMA 循环转运的双重硬件自动化架构,将多通道电压采集与数组写入工作完全交由底层硬件处理,具体流程如下:
(1)外设时钟与 GPIO 配置模块
- 时钟域开启:同步开启 ADC1、GPIOA 以及 DMA1 的总线时钟。
- 模拟通道接入:将 PA0 至 PA3 引脚配置为模拟输入模式(AIN),接入外部传感器(如电位器、光敏、热敏等)。
(2)ADC 连续扫描路由模块
- 序列映射:将 4 个物理引脚对应的 ADC 通道(Channel 0~3)依次填入规则组的转换序列(SQ1~SQ4)中。
- 转换引擎配置:配置 ADC1 为独立模式,同时开启扫描模式(ScanConvMode)与连续转换模式(ContinuousConvMode)。
(3)DMA 硬件触发通道配置模块
- 地址与指针策略:配置 DMA 外设基地址为 ADC 数据寄存器(固定不自增),目标地址为 SRAM 中的 AD_Value 数组(地址自增)。
- 数据宽度匹配:外设与存储器的数据宽度均严格匹配为半字(16位)。
- 工作模式设定:关闭 M2M 软件触发,设定为外设到存储器方向;开启 DMA 循环模式(Circular),确保与 ADC 的连续转换节奏同步。
(4)硬件链路打通与启动逻辑(主程序)
- 级联门控开启:通过 ADC_DMACmd 显式开启 ADC 向 DMA 发送触发信号的硬件输出通道。
- 状态机启动:依次使能 DMA 通道与 ADC 外设,执行标准的 ADC 内部校准时序,最后仅需调用一次软件触发指令(ADC_SoftwareStartConvCmd),整个采集与转运总线即可永久全自动运行。
- 数据读取解耦:主循环逻辑中彻底剥离 ADC 转换读取函数,仅需直接访问 AD_Value 数组即可获取最新的传感器实时数据并推送到 OLED 屏。
具体代码如下:
main.c文件:
cpp
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "AD.h"
int main(void)
{
/*模块初始化*/
OLED_Init(); //OLED初始化
AD_Init(); //AD初始化
/*显示静态字符串*/
OLED_ShowString(1, 1, "AD0:");
OLED_ShowString(2, 1, "AD1:");
OLED_ShowString(3, 1, "AD2:");
OLED_ShowString(4, 1, "AD3:");
while (1)
{
OLED_ShowNum(1, 5, AD_Value[0], 4); //显示转换结果第0个数据
OLED_ShowNum(2, 5, AD_Value[1], 4); //显示转换结果第1个数据
OLED_ShowNum(3, 5, AD_Value[2], 4); //显示转换结果第2个数据
OLED_ShowNum(4, 5, AD_Value[3], 4); //显示转换结果第3个数据
Delay_ms(100); //延时100ms,手动增加一些转换的间隔时间
}
}
AD.c文件:
cpp
#include "stm32f10x.h" // Device header
uint16_t AD_Value[4]; //定义用于存放AD转换结果的全局数组
/**
* 函 数:AD初始化
* 参 数:无
* 返 回 值:无
*/
void AD_Init(void)
{
/*开启时钟*/
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE); //开启ADC1的时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); //开启GPIOA的时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); //开启DMA1的时钟
/*设置ADC时钟*/
RCC_ADCCLKConfig(RCC_PCLK2_Div6); //选择时钟6分频,ADCCLK = 72MHz / 6 = 12MHz
/*GPIO初始化*/
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_3;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure); //将PA0、PA1、PA2和PA3引脚初始化为模拟输入
/*规则组通道配置*/
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); //规则组序列1的位置,配置为通道0
ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5); //规则组序列2的位置,配置为通道1
ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_55Cycles5); //规则组序列3的位置,配置为通道2
ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 4, ADC_SampleTime_55Cycles5); //规则组序列4的位置,配置为通道3
/*ADC初始化*/
ADC_InitTypeDef ADC_InitStructure; //定义结构体变量
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; //模式,选择独立模式,即单独使用ADC1
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; //数据对齐,选择右对齐
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; //外部触发,使用软件触发,不需要外部触发
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; //连续转换,使能,每转换一次规则组序列后立刻开始下一次转换
ADC_InitStructure.ADC_ScanConvMode = ENABLE; //扫描模式,使能,扫描规则组的序列,扫描数量由ADC_NbrOfChannel确定
ADC_InitStructure.ADC_NbrOfChannel = 4; //通道数,为4,扫描规则组的前4个通道
ADC_Init(ADC1, &ADC_InitStructure); //将结构体变量交给ADC_Init,配置ADC1
/*DMA初始化*/
DMA_InitTypeDef DMA_InitStructure; //定义结构体变量
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; //外设基地址,给定形参AddrA
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; //外设数据宽度,选择半字,对应16为的ADC数据寄存器
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; //外设地址自增,选择失能,始终以ADC数据寄存器为源
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)AD_Value; //存储器基地址,给定存放AD转换结果的全局数组AD_Value
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; //存储器数据宽度,选择半字,与源数据宽度对应
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; //存储器地址自增,选择使能,每次转运后,数组移到下一个位置
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; //数据传输方向,选择由外设到存储器,ADC数据寄存器转到数组
DMA_InitStructure.DMA_BufferSize = 4; //转运的数据大小(转运次数),与ADC通道数一致
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; //模式,选择循环模式,与ADC的连续转换一致
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; //存储器到存储器,选择失能,数据由ADC外设触发转运到存储器
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium; //优先级,选择中等
DMA_Init(DMA1_Channel1, &DMA_InitStructure); //将结构体变量交给DMA_Init,配置DMA1的通道1
/*DMA和ADC使能*/
DMA_Cmd(DMA1_Channel1, ENABLE); //DMA1的通道1使能
ADC_DMACmd(ADC1, ENABLE); //ADC1触发DMA1的信号使能
ADC_Cmd(ADC1, ENABLE); //ADC1使能
/*ADC校准*/
ADC_ResetCalibration(ADC1); //固定流程,内部有电路会自动执行校准
while (ADC_GetResetCalibrationStatus(ADC1) == SET);
ADC_StartCalibration(ADC1);
while (ADC_GetCalibrationStatus(ADC1) == SET);
/*ADC触发*/
ADC_SoftwareStartConvCmd(ADC1, ENABLE); //软件触发ADC开始工作,由于ADC处于连续转换模式,故触发一次后ADC就可以一直连续不断地工作
}
6.2.4 实验现象
程序运行后,OLED 屏幕实时显示四个外设通道(PA0~PA3)的 ADC 采集原始数值。当人为调节电位器旋钮或改变光敏/热敏传感器的物理环境状态时,对应通道的数值会迅速且平滑地作出响应。整个数据采集与内存刷新过程完全由 ADC 与 DMA 在后台硬件自动闭环完成,主程序无任何查询或等待延时,系统响应极快。