目录

1. DMA 概述

1.1 DMA的基本概念

1.2 STM32中的DMA资源配置

2. 存储器映射与总线架构

2.1 内核与片上外设

2.2 总线矩阵与冲突仲裁

2.3 存储器映射

2.3.1 Block 0:程序代码区 (Code)

2.3.2 Block 1:运行内存区 (SRAM)

2.3.3 Block 2:片上外设区 (Peripherals)

3. DMA 硬件架构与工作机制

3.1 DMA 请求

3.2 DMA 通道

3.2.2 硬件触发的映射约束

3.2.3 资源冲突与分时复用原则

3.3 仲裁器

4. DMA 配置参数解析

4.1 传输方向与基地址

4.2 地址增量模式

4.3 数据位宽与对齐规则

4.4 传输计数器与循环模式

4.4.1 正常单次模式 (Normal Mode, CIRC = 0)

4.4.2 自动循环模式 (Circular Mode, CIRC = 1)

5. DMA 配置流程

5.1 存储器到存储器(M2M)数据转运

5.1.1 开启 DMA 时钟

5.1.2 初始化 DMA 参数(核心配置)

5.1.3:封装软件触发与等待逻辑

5.2 外设到存储器(ADC 多通道扫描)

5.2.1 时钟与 GPIO 配置

5.2.2 ADC 扫描与连续模式配置

5.2.3 DMA 通道参数配置(非对称联动)

5.2.4 链路激活与启动

6. 本章节实验

6.1 DMA直接数据转运(存储器到存储器)

6.1.1 实验目标

6.1.2 硬件设计

6.1.3 软件设计

6.1.4 实验现象

6.2 ADC多通道扫描与DMA连续转运(外设到存储器)

6.2.1 实验目标

6.2.2 硬件设计

6.2.3 软件设计

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 位的系统架构,具备统一的线性寻址空间,其理论最大寻址能力为 $2^{32} Bytes = 4 \text{ GB}$。物理存储器单元本身并无原生地址,是芯片内部的地址译码逻辑为 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 2400+0x4C=0x4001 244C$

          因此,应将 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)重新发起传输的推荐安全流程

          若需发起新一轮传输,必须严格遵循以下序列以确保逻辑闭环:

  1. 状态确认:轮询或通过中断确认 EN 位已被硬件清零,确保前一事务已彻底终止。
  2. 清理标志位:手动清除该通道的相关状态 / 中断标志(如 TC、HT、TE),防止旧的完成标志导致中断逻辑误触发。
  3. 配置计数器:向 DMA_CNDTRx 重新写入新的传输计数值 N。
  4. 更新基地址(可选):若数据源或目的地需变更地址,需更新 CPARx 或 CMARx 寄存器。
  5. 激活通道:重新将 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)运行/调整的操作要点(步骤与建议)

  1. 配置好初始 PeriphAddr、MemAddr 与 DMA_CNDTRx(初值),并设置 CIRC = 1。
  2. 启用通道(DMA_CCRx.EN = 1)后,DMA 将在无需软件干预的情况下循环搬运数据。
  3. 在循环模式运行期间,若需调整传输数量(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 模式的数据转运,需遵循以下四个核心步骤:

  1. 开启总线时钟:使能 DMA1 所挂载的 AHB 总线时钟。
  2. 初始化 DMA 参数:填充 DMA_InitTypeDef 初始化结构体,配置地址映射、传输方向、总线位宽、自增模式及 M2M 使能。
  3. 封装传输控制函数:针对正常模式(Normal),编写符合芯片手册“禁能 —> 重载 —> 使能”规范的触发函数。
  4. 主循环调用与验证:在应用层调用控制函数并通过状态寄存器验证转运结果。

5.1.1 开启 DMA 时钟

          在进行任何外设寄存器配置前,解除总线时钟门控是必要前提。DMA 控制器作为总线矩阵上的主设备,挂载在高速的 AHB 总线上。若未提前使能其时钟,CPU 发出的所有针对 DMA 寄存器映射区域的写指令均会被总线忽略,导致初始化失败。

/* 开启 DMA1 的 AHB 总线时钟 */
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);

5.1.初始化 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(即外设指针作为源端)。

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 个字节地址单元,严格防止内存踩踏与数据覆盖。

    /* 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),总线访问立即挂起,从而安全地结束本次批量传输。

      /* 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。因此,标准的代码范式必须严格遵循 禁能 —> 赋值重载 —> 使能 的安全时序。

      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 联动采集,需遵循以下五个核心步骤:

      1. 开启相关时钟:使能 ADC1、GPIOA 以及 DMA1 的时钟,并配置 ADC 总线时钟分频器。
      2. 配置 ADC 与 GPIO:将对应引脚设为模拟输入,配置规则组序列,并使能 ADC 的连续转换与扫描模式。
      3. 配置 DMA 参数:基地址固定为 ADC 数据寄存器,目标地址为 SRAM 数组,配置非对称的地址自增模式(外设不增,存储器增),开启循环模式,禁用 M2M 模式。
      4. 打通联动信号链:使能 DMA 通道,并调用 ADC_DMACmd 开启外设向 DMA 发送请求的开关,最后使能 ADC 外设。
      5. 校准与软件触发:完成 ADC 自校准后,启动软件触发。

      5.2.1 时钟与 GPIO 配置

                ADC 内部包含极其敏感的模拟采样与量化电路,对工作频率有严格的电气约束。STM32F103 的 ADC 挂载在 APB2 总线上,其最大允许时钟频率为 14 MHz。在系统主频为 72 MHz 的标准配置下,必须通过 RCC_ADCCLKConfig 调用专用的预分频器进行 6 分频,得到 72 MHz / 6 = 12 MHz 的安全工作时钟。

                同时,需将参与采样的物理引脚配置为模拟输入模式,以断开内部的施密特触发器和数字接口。

      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 中的量化数据,从而实现外设转换速度与总线传输速率的硬件级同步。

      /* 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 数组首地址,实现数据的连续循环刷新。
      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 轮询参与的情况下,进入由硬件信号驱动的自动化采样与数据搬运闭环。

      /* 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文件:

      #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文件:

      #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文件:

      #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文件:

      #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 在后台硬件自动闭环完成,主程序无任何查询或等待延时,系统响应极快。

      Logo

      智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

      更多推荐