STM32 学习 —— 个人学习笔记8(DMA 直接存储器存取 & 数据转运及DMA+AD多通道)
声明
文中内容为观看 BiliBili 视频【STM32入门教程-2023版 细致讲解 中文字幕】后学习并扩展总结。
本文章为个人学习使用,版面观感若有不适请谅解,文中知识仅代表个人观点,若出现错误,欢迎各位批评指正。
一、DMA 直接存储器存取
1.1 简介
直接存储器存取(Direct Memory Access,DMA)是嵌入式微控制器中实现高速、自主数据传输的关键硬件机制。DMA 可在外部设备与片内存储器、存储器与存储器之间建立直接的数据传输通路,整个传输过程由 DMA 控制器独立完成,无需 CPU 介入数据搬运,从而有效解除 CPU 在高频数据交互中的负载压力,提升系统整体运行效率与数据吞吐率。
STM32F1 系列微控制器集成两组 DMA 控制器,共提供 12 个独立可配置的传输通道,其中 DMA1 包含 7 个通道,DMA2 包含 5 个通道。针对 STM32F103C8T6 型号,其片上仅集成 DMA1 控制器,可使用 7 个独立 DMA 通道,各通道相互独立,支持单独配置与并行调度,能够满足多外设、多数据流并发传输的系统需求。
每个 DMA 通道均支持软件触发与硬件触发两种启动方式。硬件触发由片内外设(如 UART、SPI、ADC、定时器等)的专用传输请求信号自动发起,适用于实时性要求较高的周期性数据传输;软件触发则通过 CPU 对相关控制寄存器进行配置与写操作主动启动,适用于存储器间数据批量搬移等非周期性传输场景。通过灵活配置传输源地址、目标地址、数据长度、地址自增模式及传输优先级,DMA 可适配数据采集、高速通信、信号处理等多种典型嵌入式应用场景,是提升嵌入式系统实时性与资源利用率的重要技术。
- 存储器映像
以下为嵌入式微控制器核心存储器空间的地址映射表,明确了各类存储器的起始地址、物理介质及核心用途。
| 类型 | 起始地址 | 存储器 | 用途 |
|---|---|---|---|
| ROM | 0x0800 0000 | 程序存储器 Flash | 存储 C 语言编译后的程序代码 |
| ROM | 0x1FFF F000 | 系统存储器 | 存储 BootLoader,用于串口下载 |
| ROM | 0x1FFF F800 | 选项字节 | 存储一些独立于程序代码的配置参数 |
| RAM | 0x2000 0000 | 运行内存 SRAM | 存储运行过程中的临时变量 |
| RAM | 0x4000 0000 | 外设寄存器 | 存储各个外设的配置参数 |
| RAM | 0xE000 0000 | 内核外设寄存器 | 存储内核各个外设的配置参数 |
1.2 DMA 框图
- 架构与总线拓扑
该系统基于 ARM Cortex‑M3 内核构建,采用多层 AHB(Advanced High‑performance Bus)总线矩阵作为核心互联架构,实现了处理器、存储器与外设间的高效数据传输。总线矩阵支持并发访问,通过仲裁机制协调 Cortex‑M3 内核(I‑Code/D‑Code / 系统总线)、DMA 控制器、以太网 MAC、USB OTG FS 等主控设备对 Flash、SRAM 及外设寄存器的访问,从而缓解总线争用并提升系统吞吐量。
外设与存储器通过两级 APB(Advanced Peripheral Bus)桥接至 AHB 总线:
(1)APB1:挂载低速外设,如 DAC、SPI2/I2S2、SPI3/I2S3、CAN1/CAN2、I2C1/I2C2、UART2‑UART5、TIM2‑TIM7、RTC、WWDG 等。
(2)APB2:挂载高速外设,如 ADC1/ADC2、USART1、SPI1、TIM1、所有 GPIO 端口(A‑E)、EXTI、AFIO 等。
复位与时钟控制模块(RCC)为整个系统提供时钟树管理与复位控制,确保各模块在指定频率下同步工作。 - DMA 控制器架构
系统集成了两个独立的 DMA 控制器(DMA1、DMA2),均采用多通道共享仲裁器的架构:
(1)每个 DMA 控制器包含多个通道(DMA1 含通道 1、2、7;DMA2 含通道 1、2、5),各通道可独立配置为外设到存储器、存储器到外设或存储器到存储器的传输模式。
(2)内置仲裁器基于优先级规则(可编程优先级或硬件固定优先级)调度多通道的 DMA 请求,防止传输冲突。
(3)DMA 控制器通过 AHB 从设备接口连接至总线矩阵,可直接访问 SRAM、Flash 及外设寄存器,实现无 CPU 干预的高速数据搬运。 - DMA 请求与外设映射
各外设(如 ADC、UART、SPI、定时器)通过专用的 DMA 请求线 向 DMA 控制器发送传输触发信号。这些请求线与 DMA 通道存在固定的硬件映射关系,例如:
(1)ADC1/ADC2 的转换完成信号可触发 DMA1/2 的对应通道,自动将采样数据搬运至 SRAM 缓冲区。
(2)UART/USART 的 TXE(发送为空)或 RXNE(接收非空)标志可触发 DMA 传输,实现串口数据的批量收发。
(3)定时器(TIM1‑TIM7)的更新或比较事件可作为 DMA 触发源,用于周期性数据采集或输出。
这种硬件映射机制确保了 DMA 传输与外设事件的精准同步,显著降低了 CPU 中断负载与数据搬运开销。 - 关键性能与应用场景
(1)零 CPU 开销:DMA 控制器独立于 Cortex‑M3 内核执行数据传输,使 CPU 可专注于计算与控制任务,提升系统实时性与能效。
(2)高速数据吞吐:通过 AHB 总线直接访问存储器,DMA 传输速率可接近总线时钟频率,适用于高速外设(如以太网 MAC、USB OTG FS)的大数据量传输。
(3)典型应用:
传感器数据采集:ADC 采样数据通过 DMA 直接搬运至 SRAM,避免 CPU 频繁中断。
通信接口缓冲:UART/SPI/CAN 数据通过 DMA 实现批量收发,提升通信吞吐量。
信号处理:定时器触发 DMA 传输,配合 DAC 实现周期性波形输出。 - 系统级优化考量
在系统设计中,需重点关注以下优化方向:
(1)总线仲裁策略:合理配置 DMA 通道优先级,避免关键外设(如 USB、以太网)的传输被低优先级任务阻塞。
(2)存储器布局:将 DMA 目标缓冲区放置在 SRAM 中,避免访问 Flash 带来的等待状态,提升传输效率。
(3)时钟与功耗:通过 RCC 动态关闭未使用的 DMA 通道与外设时钟,降低系统功耗,尤其适用于电池供电设备。
1.3 DMA 基本结构
在基于该 DMA 控制器结构进行软件开发时,需遵循严格的配置与启动流程,以确保数据传输的正确性与高效性。以下从学术专业角度对代码编写中所需配置与启动信息进行系统性阐述:
(1)传输方向配置:需明确数据传输的方向,即确定数据是从外设到存储器(外设到存储器模式),还是从存储器到外设(存储器到外设模式),或存储器到存储器(M2M 模式)。这一配置决定了数据在 DMA 通道中的流动路径,是所有后续操作的基础。
(2)外设端参数配置:需对外设侧的传输参数进行精确配置,包括外设寄存器的起始地址、单次传输的数据宽度(如 8 位、16 位或 32 位),以及地址是否自增。地址自增功能决定了在连续传输中,外设寄存器地址是否自动步进,这对于批量数据传输至关重要。
(3)存储器端参数配置:与外设端类似,需配置存储器侧的起始地址、数据宽度和地址自增模式。这些参数确保 DMA 能够准确访问目标存储区域(如 Flash 或 SRAM),并在传输过程中正确更新地址指针。
(4)传输计数器与自动重装器配置:传输计数器用于设定单次 DMA 传输的数据总量,其值决定了传输的长度。自动重装器则用于配置传输计数器的重载值,以支持循环传输模式,使 DMA 在完成一次传输后自动恢复初始计数值,实现周期性数据搬运。
(5)触发源选择:DMA 传输的启动可由硬件触发或软件触发。硬件触发通常来自外设的中断信号(如 UART 接收完成、ADC 采样完成),而软件触发则通过 CPU 直接写控制寄存器实现。在代码中,需根据应用场景选择合适的触发源,并配置相应的触发逻辑。
(6)M2M 模式使能:当需要在存储器之间直接传输数据时,需使能存储器到存储器(M2M)模式。此模式下,DMA 控制器绕过外设,直接在两个存储区域之间进行数据搬运,常用于内存块复制等操作。
(7)通道使能与启动控制:在完成所有参数配置后,需通过写控制寄存器使能 DMA 通道,启动数据传输。这一操作将激活 DMA 控制器,使其根据已配置的参数开始执行数据搬运任务。
通过对上述信息的系统性配置与启动,可确保 DMA 控制器按照预期的方式高效、可靠地完成数据传输任务,从而减轻 CPU 负担,提升系统整体性能。
1.4 DMA 请求
该图清晰呈现了 DMA1 控制器的请求映射与优先级架构:DMA1 包含 7 个独立通道,每个通道通过多路选择器支持 ADC1、USART、TIM、SPI、I2C 等多类外设请求源,可配置为硬件触发(由外设事件驱动)或软件触发(MEM2MEM 模式);通道优先级由硬件固定分配,通道 1 至通道 7 依次从高到低,多通道同时请求时按此顺序仲裁服务,各通道独立的 EN 位控制其工作使能状态,详细的通道映射、寄存器配置及时序约束等技术细节可通过查阅对应 MCU 的 DMA 控制器章节手册获取。
1.5 DMA 数据转运
本场景中,DMA 控制器在软件触发信号的驱动下,执行片上 SRAM 内部的数据块传输操作。具体而言,DMA 控制器以 DataA [7] 数组(uint8_t 类型)作为外设地址端数据源,以** DataB [7] 数组作为存储器地址端目标**,将 DataA 中从索引 0 至 6 的 7 个字节数据,按顺序逐一搬运至 DataB 的对应位置。整个传输过程由硬件 DMA 控制器自主完成,无需 CPU 介入,从而实现了高效的片上存储器到存储器的数据搬运。
1.6 DMA + ADC 扫描模式
本系统采用ADC 多通道扫描模式与DMA 直接存储器访问相结合的高速数据采集架构,通过硬件自动化实现多通道模拟信号的无 CPU 干预式数字化存储。系统配置 ADC 规则序列共包含 7 路有效模拟输入通道,扫描顺序固化为通道 2→通道 5→通道 4→通道 1→通道 0→通道 3→通道 6;外部触发信号启动转换后,ADC 按预设序列逐通道完成 A/D 转换,每路通道的转换结果锁存至 ADC 数据寄存器(ADC_DR),并由转换结束信号(EOC)触发 DMA 硬件请求。DMA 控制器以 ADC_DR 为外设数据源地址,以片内 SRAM 中定义的uint16_t ADValue[7]数组为目标存储地址,按扫描序列顺序将各通道转换结果依次搬运至对应数组单元,完成一轮全序列扫描后自动终止或等待下一次触发。整个采集流程中,CPU 无需参与数据传输,仅需在 DMA 完成后读取ADValue数组即可获取完整的 7 路通道采样数据,彻底消除了中断响应与数据拷贝的时间开销,保障了多通道模拟信号采集的实时性与连续性。
二、DMA 数据转运 及 DMA+AD多通道
2.1 DMA 数据转运的实现
- 首先,按下图接线方式,搭建面包板电路连接 OLED 显示屏,并将电位器与 PA0 连接,然后将 DAP-Link / ST-Link 连接到 STM32 最小系统板上,为使 OLED 显示屏的 VCC 和 GND 正确连接正负极,请先连接对应正负极跳线(或直接使用 GPIO 口进行供电)。

- 直接复制先前演示的已有文件目录,重命名并双击后缀名为 .uvprojx 的文件打开工程文件,并对 main.c 进行修改,工程中所使用的全部头文件其详细内容已放于文末。
#include "stm32f10x.h" // Device header
#include <OLED.h>
#include <MyDMA.h>
#include <Delay.h>
//const uint8_t num_a = 0x15;
//uint8_t num_b = 0x66;
// DMA 转运的源端数组
uint8_t DataA[] = {0x01, 0x02, 0x03, 0x04};
// DMA 转运的目的数组
uint8_t DataB[] = {0, 0, 0, 0};
int main(void){
OLED_Init();
// OLED_ShowString(1, 1, "value | address");
//
// OLED_ShowHexNum(2, 4, num_a, 2);
// OLED_ShowString(2, 6, " | ");
// OLED_ShowHexNum(2, 9, (uint32_t)&num_a, 8);
//
// OLED_ShowHexNum(3, 4, num_b, 2);
// OLED_ShowString(3, 6, " | ");
// OLED_ShowHexNum(3, 9, (uint32_t)&num_b, 8);
//
// OLED_ShowString(4, 1, "ADC1_DR:");
// OLED_ShowHexNum(4, 9, (uint32_t)&ADC1->DR, 8);
// OLED_ShowHexNum(1, 1, DataA[0], 2);
// OLED_ShowHexNum(1, 4, DataA[1], 2);
// OLED_ShowHexNum(1, 7, DataA[2], 2);
// OLED_ShowHexNum(1, 10, DataA[3], 2);
//
// OLED_ShowHexNum(2, 1, DataB[0], 2);
// OLED_ShowHexNum(2, 4, DataB[1], 2);
// OLED_ShowHexNum(2, 7, DataB[2], 2);
// OLED_ShowHexNum(2, 10, DataB[3], 2);
//
// MyDMA_Init((uint32_t)DataA, (uint32_t)DataB, 4);
//
// OLED_ShowHexNum(3, 1, DataA[0], 2);
// OLED_ShowHexNum(3, 4, DataA[1], 2);
// OLED_ShowHexNum(3, 7, DataA[2], 2);
// OLED_ShowHexNum(3, 10, DataA[3], 2);
//
// OLED_ShowHexNum(4, 1, DataB[0], 2);
// OLED_ShowHexNum(4, 4, DataB[1], 2);
// OLED_ShowHexNum(4, 7, DataB[2], 2);
// OLED_ShowHexNum(4, 10, DataB[3], 2);
OLED_ShowString(1, 1, "DataA");
OLED_ShowHexNum(1, 8, (uint32_t)DataA, 8);
OLED_ShowString(3, 1, "DataB");
OLED_ShowHexNum(3, 8, (uint32_t)DataB, 8);
MyDMA_Init((uint32_t)DataA, (uint32_t)DataB, 4);
while(1){
DataA[0] ++;
DataA[1] ++;
DataA[2] ++;
DataA[3] ++;
OLED_ShowHexNum(2, 1, DataA[0], 2);
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);
OLED_ShowHexNum(4, 4, DataB[1], 2);
OLED_ShowHexNum(4, 7, DataB[2], 2);
OLED_ShowHexNum(4, 10, DataB[3], 2);
Delay_ms(1000);
MyDMA_Transfer();
OLED_ShowHexNum(2, 1, DataA[0], 2);
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);
OLED_ShowHexNum(4, 4, DataB[1], 2);
OLED_ShowHexNum(4, 7, DataB[2], 2);
OLED_ShowHexNum(4, 10, DataB[3], 2);
Delay_ms(1000);
}
}

2.2 DMA+AD多通道的实现
- 首先,按下图接线方式,搭建面包板电路连接 OLED 显示屏,并将电位器、热敏传感器、反射式红外传感器分别与 PA0、PA2、PA4 连接,然后将 DAP-Link / ST-Link 连接到 STM32 最小系统板上,为使 OLED 显示屏的 VCC 和 GND 正确连接正负极,请先连接对应正负极跳线(或直接使用 GPIO 口进行供电)。

- 直接复制先前演示的已有文件目录,重命名并双击后缀名为 .uvprojx 的文件打开工程文件,并对 main.c 进行修改,工程中所使用的全部头文件其详细内容已放于文末。
#include "stm32f10x.h" // Device header
#include <OLED.h>
#include <Mult_AD.h>
#include <Delay.h>
float Voltage;
int main(void){
OLED_Init();
AD_Init();
OLED_ShowString(1, 1, "AD0:");
OLED_ShowString(2, 1, "AD1:");
OLED_ShowString(3, 1, "AD2:");
while(1){
// AD_GetValue();
OLED_ShowNum(1, 5, AD_Value[0], 4);
OLED_ShowNum(2, 5, AD_Value[1], 4);
OLED_ShowNum(3, 5, AD_Value[2], 4);
Delay_ms(100);
}
}
- 通过十字螺丝刀旋转电位器调节旋钮、遮挡反射式红外传感器及用手包覆热敏传感器,均可在 OLED 显示屏上实时观察到电压值(Voltage)与 ADC 转换数值(ADValue)的同步动态变化。

三、演示代码关联的头文件与源文件说明
- Delay 相关头文件请从 STM32 学习 —— 个人学习笔记3-1(GPIO 输出) 文末查看,此处不重复展示。
- OLED 相关头文件请从 STM32 学习 —— 个人学习笔记4(OLED 显示屏及调试工具) 文末查看,此处不重复展示。
- MyDMA.c
#include "stm32f10x.h" // Device header
uint16_t MyDMA_Size;
void MyDMA_Init(uint32_t AddrA, uint32_t AddrB, uint16_t Size){
// 提前声明需要用到的结构体
DMA_InitTypeDef DMA_InitStructure;
// RCC 开启 DMA 时钟
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
// 初始化 DMA 各个参数
// 外设站点的参数
DMA_InitStructure.DMA_PeripheralBaseAddr = AddrA;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable;
// 存储器站点的参数
DMA_InitStructure.DMA_MemoryBaseAddr = 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);
MyDMA_Size = Size;
// DMA 使能
DMA_Cmd(DMA1_Channel1, DISABLE);
}
void MyDMA_Transfer(void){
// DMA 失能
DMA_Cmd(DMA1_Channel1, DISABLE);
// 给传输计数器赋值
DMA_SetCurrDataCounter(DMA1_Channel1, MyDMA_Size);
// DMA 使能
DMA_Cmd(DMA1_Channel1, ENABLE);
// 等待转运完成
while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);
DMA_ClearFlag(DMA1_FLAG_TC1);
}
- MyDMA.h
#ifndef __MYDMA_H
#define __MYDMA_H
void MyDMA_Init(uint32_t AddrA, uint32_t AddrB, uint16_t Size);
void MyDMA_Transfer(void);
#endif
- Mult_AD.c
#include "stm32f10x.h" // Device header
uint16_t AD_Value[4];
void AD_Init(void){
// 提前声明需要使用的结构体
GPIO_InitTypeDef GPIO_InitStructure;
ADC_InitTypeDef ADC_InitStructure;
DMA_InitTypeDef DMA_InitStructure;
// 开启 RCC 时钟
RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
// 配置 ADCCLK 分频器, ADCCLK = 72MHz / 6 = 12MHz
RCC_ADCCLKConfig(RCC_PCLK2_Div6);
// 配置 GPIO
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_2 | GPIO_Pin_4;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 2, ADC_SampleTime_55Cycles5);
ADC_RegularChannelConfig(ADC1, ADC_Channel_4, 3, ADC_SampleTime_55Cycles5);
// 配置 ADC
// ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right;
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
ADC_InitStructure.ADC_NbrOfChannel = 3;
ADC_InitStructure.ADC_ScanConvMode = ENABLE;
ADC_Init(ADC1, &ADC_InitStructure);
// 初始化 DMA 各个参数
// 外设站点的参数
DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;
DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
// 存储器站点的参数
DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)AD_Value;
DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
DMA_InitStructure.DMA_BufferSize = 3;
// DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;
DMA_Init(DMA1_Channel1, &DMA_InitStructure);
// DMA 使能
DMA_Cmd(DMA1_Channel1, ENABLE);
// 开启 ADC 到 DMA 的输出
ADC_DMACmd(ADC1, ENABLE);
// 开启 ADC
ADC_Cmd(ADC1, ENABLE);
// 校准 ADC
ADC_ResetCalibration(ADC1);
while (ADC_GetResetCalibrationStatus(ADC1) == SET);
ADC_StartCalibration(ADC1);
while (ADC_GetCalibrationStatus(ADC1) == SET);
// 开启循环时取消注释,关闭循环时注释此行
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}
void AD_GetValue(void){
// DMA 失能
DMA_Cmd(DMA1_Channel1, DISABLE);
// 给传输计数器赋值
DMA_SetCurrDataCounter(DMA1_Channel1, 3);
// DMA 使能
DMA_Cmd(DMA1_Channel1, ENABLE);
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
// 等待转运完成
while (DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET);
DMA_ClearFlag(DMA1_FLAG_TC1);
}
- Mult_AD.h
#include "stm32f10x.h" // Device header
#ifndef __MULT_AD_H
#define __MULT_AD_H
extern uint16_t AD_Value[4];
void AD_Init(void);
void AD_GetValue(void);
#endif
文中部分知识参考:B 站 —— 江协科技;百度百科
更多推荐
所有评论(0)