“ STM32F4xx 的 DMA 控制器通过硬件直接实现存储器与外设间的数据传输,显著提升系统效率。本文深入解析其工作原理、传输模式及配置要点,结合 ADC 采样案例,详细阐述外设到存储器、循环搬运模式的实现流程,为高速数据采集应用提供实践指导。”
01
—
stm32 DMA简介
STM32F4xx 系列微控制器中的 DMA (Direct Memory Access) 控制器是一种特殊的硬件机制,允许外设与存储器之间或者存储器与存储器之间直接进行数据传输,无需 CPU 干预。这大大提高了系统的性能和效率,尤其在处理大量数据传输时优势明显。
1.传输方向:支持三种传输方向:
外设到存储器;
存储器到外设;
存储器到存储器(某些 STM32F4 型号支持)。
2.通道 / 数据流
STM32F4xx 系列通常配备 2 个 DMA 控制器(DMA1 和 DMA2)。
DMA1有 8 个通道,DMA2有 5 个通道(早期型号)或 8 个通道(某些高级型号)。
较新的 STM32F4 型号(如 STM32F42xxx/43xxx)采用数据流(Stream)架构替代传统通道,每个 DMA 控制器有 8 个数据流,每个数据流支持 8 个通道选择器。
3.仲裁机制
支持软件优先级(4 个级别)和硬件优先级(取决于通道编号或数据流编号),确保高优先级传输优先处理。
4. 电气特性
工作电压:通常为 1.8V 至 3.6V(取决于具体型号)。
时钟要求:DMA 控制器由 AHB 总线提供时钟(通常为系统时钟 HCLK)。
数据宽度:支持 8 位、16 位或 32 位数据传输,可根据外设和存储器要求灵活配置。
最大传输速率:取决于 AHB 总线速度,通常可达 180MHz(在 STM32F407 等型号中)。
功耗:DMA 操作的功耗远低于 CPU 干预的数据传输,有助于降低系统整体功耗。
5. 数量与架构
DMA 控制器数量:大多数 STM32F4xx 型号配备 2 个 DMA 控制器(DMA1 和 DMA2)。
通道 / 数据流数量:
传统通道架构:DMA1 有 8 个通道,DMA2 有 5 个或 8 个通道。
数据流架构(如 STM32F429):每个 DMA 控制器有 8 个数据流,每个数据流支持 8 个通道选择器,共 64 个可能的通道映射。
外设映射:每个 DMA 通道 / 数据流对应特定的外设请求(如 ADC、SPI、I2C、USART 等),需查阅数据手册确认具体映射关系。
6. 应用场景
DMA 控制器在以下场景中尤为适用:
ADC 连续采样数据到内存缓冲区,无需 CPU 频繁干预。
UART、SPI、I2C 等接口的大量数据收发。
SPI 与外部 Flash 或传感器的高速数据交换。
以太网或 USB 数据传输(如 USB 大容量存储设备)。
音频编解码数据的实时传输(如 I2S 接口)。
Flash 或 SRAM 之间的数据传输。
LCD 控制器的帧缓冲区更新,实现流畅的图形显示。
在需要低延迟的应用中,DMA 可减少 CPU 负载,提高系统响应速度。
02
—
DMA原理
1.DMA框图
DMA1 与 DMA2 各自通过一条 AHB 主总线挂到总线矩阵上,每个控制器内部拥有 8 条数据流(Stream0-7)和 8 条请求通道(CH0-7);数据流经两级仲裁器(流级 + 通道级)后,再经 16 B FIFO,最终由外设端口或存储器端口完成外设↔存储器或存储器↔存储器搬运,其中 DMA2 额外支持 SRAM↔SRAM 全速搬运,而 DMA1 仅支持外设↔存储器方向”。
2.请求映射表
STM32F4xx 的 DMA1和DMA2 请求映射表 是一张 “外设事件 → 数据流/通道” 的固定对照表,每个外设请求(ADC、SPI、USART、TIM 等)在表中对应 一条 DMA1 数据流 (Stream0~7) 和一个通道号 (CH0~7),通过设置 DMA_SxCR.CHSEL 即可绑定。多数外设提供 两条可选通道,共 64 个映射点(8×8),实现 64 种外设触发场景(部分为空映射),由 流/通道仲裁器 按优先级实时选择,确保 DMA 无需 CPU 介入即可搬运数据。
DMA1请求映射表
DMA2 请求映射表
3.封装/ 解封和字节序行为
在 STM32F4xx 中,外设、存储器与 DMA 三者可能使用不同的数据宽度(8/16/32 bit)。为了让 DMA 能在任何宽度组合下正确搬运数据,手册给出一张 “可编程数据宽度、封装/解封、字节序” 速查表,用来说明 DMA 如何自动完成宽度对齐、数据打包与拆包。可编程数据宽度、封装/ 解封、字节序表如下:
由于DMA 内部带 16Byte 的FIFO,当源宽度 ≠ 目标宽度时:
窄→宽:FIFO 缓存足够字节后,一次性拼成目标宽度后输出;
宽→窄:FIFO 把源数据按目标宽度拆成多次节拍,依次输出高位→低位;
字节序固定为小端,顺序不变。
举例说明:
|
组合 |
连续源数据(十六进制) |
源宽度 |
目标宽度 |
最终写入 RAM 的完整小端数据 |
说明 |
|
① 8-bit → 8-bit |
0xA5 |
1 B |
1 B |
0xA5 |
直接搬运 |
|
② 8-bit → 16-bit |
0x34, 0x12 |
1 B×2 |
2 B |
0x1234 |
拼成 16-bit |
|
③ 8-bit → 32-bit |
0x44, 0x33, 0x22, 0x11 |
1 B×4 |
4 B |
0x11223344 |
拼成 32-bit |
|
④ 16-bit → 8-bit |
0x1234 |
2 B |
1 B×2 |
0x34→ **0x12 |
拆为两次 8-bit |
|
⑤ 16-bit → 16-bit |
0x1234 |
2 B |
2 B |
0x1234 |
原样 |
|
⑥ 16-bit → 32-bit |
0x5678, 0x1234 |
2 B×2 |
4 B |
0x12345678 |
拼成 32-bit |
|
⑦ 32-bit → 8-bit |
0x11223344 |
4 B |
1 B×4 |
0x44 → 0x33 → 0x22 → 0x11 |
拆为四次 8-bit |
|
⑧ 32-bit → 16-bit |
0x11223344 |
4 B |
2 B×2 |
0x3344 → 0x1122 |
拆为两次 16-bit |
|
⑨ 32-bit → 32-bit |
0x11223344 |
4 B |
4 B |
0x11223344 |
原样 |
4.可能的 DMA 配置汇总
①外设到存储器模式
DMA 外设到存储器模式数据收发流程:
-
外设发起请求:外设源(如 ADC、UART 等)产生数据传输需求时,通过 “外设 DMA 请求” 信号线,向 DMA 控制器发送 REQ_STREAMx(x 为数据流编号 )请求,触发 DMA 传输流程。
-
仲裁器调度:DMA 控制器内的仲裁器接收 REQ_STREAMx 请求,依据配置的优先级(软件设置 + 硬件隐含优先级 ),决定当前数据流的传输优先级,确保高优先级传输优先执行。
-
外设数据获取:DMA 控制器通过 AHB 外设端口 访问外设,从 DMA_SxPAR(外设地址寄存器 )指定的外设地址(如外设数据寄存器地址 )中,读取待传输数据。
-
FIFO 缓冲(可选):数据进入 DMA 内部 FIFO 缓冲区暂存,FIFO 可适配外设与存储器的速率差异。可配置 FIFO 阈值(如半满、全满触发 ),当数据达到阈值,启动向存储器的批量传输,减少总线交互次数。
-
存储器地址配置:DMA_SxM0AR(或双缓冲模式下的 DMA_SxM1AR )存储数据要写入的存储器目标地址(如 SRAM 缓冲区首地址 ),作为数据最终存储位置的 “目的地”。
-
数据写入存储器:DMA 控制器通过 AHB 存储器端口,将 FIFO 中(或直接从外设)的数据,写入 DMA_SxM0AR 指向的存储器地址,完成 “外设 → 存储器” 的数据传输。
-
传输计数与结束:DMA 控制器内置计数器,每传输 1 个数据单元(字节 / 半字 / 字,由配置决定 ),计数器递减。当计数器归 0,标志本次传输完成,可触发中断 / 事件通知 CPU 处理后续任务(如数据处理、新一轮传输准备 )。
②存储器到外设模式
DMA 存储器到外设模式数据收发流程:
-
外设发起请求:外设目标(如 DAC、UART 等)需要数据时,通过 “外设 DMA 请求” 信号线,向 DMA 控制器发送 REQ_STREAMx(x 为数据流编号 )请求,触发 DMA 传输流程 。
-
仲裁器调度:DMA 控制器内的仲裁器接收 REQ_STREAMx 请求,依据配置的优先级(软件设置 + 硬件隐含优先级 ),决定当前数据流的传输优先级,确保高优先级传输优先执行 。
-
存储器数据获取:DMA 控制器通过 AHB 存储器端口 访问存储器,从 DMA_SxM0AR(或双缓冲模式下的 DMA_SxM1AR ,存储存储器源地址 )指定的存储器地址中,读取待传输数据 。
-
FIFO 缓冲(可选):数据进入 DMA 内部 FIFO 缓冲区暂存,适配存储器与外设的速率差异。可配置 FIFO 阈值(如半满、全满触发 ),当数据达到阈值,启动向外设的批量传输,减少总线交互次数 。
-
外设地址配置:DMA_SxPAR(外设地址寄存器 )存储数据要写入的外设目标地址(如 DAC 数据寄存器地址 ),作为数据最终输出位置的 “目的地” 。
-
数据写入外设:DMA 控制器通过 AHB 外设端口,将 FIFO 中(或直接从存储器)的数据,写入 DMA_SxPAR 指向的外设地址,完成 “存储器 → 外设” 的数据传输 。
-
传输计数与结束:DMA 控制器内置计数器,每传输 1 个数据单元(字节 / 半字 / 字,由配置决定 ),计数器递减。当计数器归 0,标志本次传输完成,可触发中断 / 事件通知 CPU 处理后续任务(如新一轮传输准备、状态反馈 ) 。
③存储器到存储器模式
-
触发传输:通过 “流使能” 信号激活 DMA 数据流,仲裁器识别到有效请求后,启动存储器到存储器的数据搬运流程。
-
源地址读取:DMA 控制器通过 AHB 外设端口,访问 DMA_SxPAR 寄存器指定的存储器 1 源地址,读取待传输的数据。
-
FIFO 缓冲(可选):读取的数据先进入 DMA 内部 FIFO 缓冲区暂存,可配置 FIFO 阈值(如半满 / 全满触发传输 ),适配源、目标存储器的速率差异,减少总线竞争。
-
目标地址写入:DMA 控制器通过 AHB 存储器端口,将 FIFO 中的数据,写入 DMA_SxM0AR(或双缓冲模式下的 DMA_SxM1AR )指定的存储器 2 目标地址。
-
传输计数与结束:DMA 内置计数器随数据传输递减(每传 1 个单元,计数 -1 ),计数归 0 时传输完成,可触发中断 / 事件通知 CPU 处理后续任务(如校验数据、释放资源 )。
④从是否循环传输维度,可分为循环模式和普通模式
循环模式:当一次 DMA 传输完成后,DMA 控制器不会停止,而是自动重新装载传输计数器(NDTR),并立即开始下一轮传输,从而形成“循环不断”的数据流,直到软件关 DMA 或等错误中断才会停止传输。循环模式最多一次可传 65535 个数据项(一个数据项大小由PSIZE/MSIZE 设置,如 1 Byte、2 Bytes、4 Bytes)。
普通模式:与循环模式相反,普通模式是一次性数据传输,自动停止,需软件重启。普通模式可传 65535 个数据项(一个数据项大小由PSIZE/MSIZE 设置,如 1 Byte、2 Bytes、4 Bytes)。
⑤从是否突发传输维度,可分为单独传输和突发传输
单独传输:每收到一次 DMA 请求,仅传输 一个数据项,一个数据项大小由PSIZE/MSIZE 设置,如 1 Byte、2 Bytes、4 Bytes;
突发传输:每收到一次 DMA 请求,连续传输多个节拍的数据,由PBURST/MBURST 设置,如4、8 或 16 个节拍,1个节拍的数据等于1个数据项大小,一次突发模式最大传输64 字节(16 节拍 × 4 Bytes/节拍 )。
例如:设置突发传输为 4 节拍,MSIZE = Word(4 Bytes),则一次突发传输的数据量为:4 beats × 4 Bytes/节拍 = 16 Bytes。
⑥从数据是否使用FIFO缓冲,可分为FIFO模式和直接模式
FIFO模式:启用 FIFO,数据先进入 FIFO,达到设定阈值后再一次性传输,支持突发模式。FIFO容量固定 16 Byte,不可调整。FIFO阈值可软件配置为 1/4(4 Bytes)、1/2(8 Bytes)、3/4(12 Bytes)或满(16 Bytes)。
直接模式:禁用 FIFO,每个 DMA 请求立即触发一次单次传输,支持单次传输。直接模式一次仅传输 一个数据项,一个数据项大小由PSIZE/MSIZE 设置,如 1 Byte、2 Bytes、4 Bytes。
⑦从缓冲区数量,可分为双缓冲模式和单缓冲模式
双缓冲模式:具备两个独立存储区(DMA_SxM0AR 和 DMA_SxM1AR)。当 DMA 把当前缓冲区写/读完后,硬件自动把指针切换到另一缓冲区,同时继续下一次传输。
例如:“外设→存储器” 双缓冲模式下,外设(ADC、UART、SPI …)不停产生数据,DMA 把它们搬进两块交替使用的 RAM 缓冲区(SRAM 区域定义两个普通数组即可),每完成一整帧(NDTR 减到 0),硬件自动切换 CT 位并继续下一帧,无需 CPU 重启 DMA,实现零拷贝、高吞吐的连续数据流。
单缓冲模式:只有一个存储区(由 DMA_SxM0AR 指向),DMA 完成一次设定的数据量(NDTR→0)后,要么停止要么回到起点继续。
例如:“外设→存储器” 单缓冲模式下,“单缓冲”就是只使用一块内存区来接收外设数据,DMA 完成一次设定长度的搬运后就会停止(Normal),或回到起点继续下一轮(Circular)。
03
—
硬件说明
PA1(ADC1_IN1)接 0~3.3 V 信号(电位器)。
04
—
程序设计案例
连续 256 个采样值,使用外设到存储器、循环搬运、单次传输、直接模式、单缓冲区模式,将256 个采样值传输到adcBuf数组中。
步骤:
1.开时钟
先把 ADC1、GPIOA 和 DMA2 的时钟全部拉起来:
__HAL_RCC_ADC1_CLK_ENABLE()、__HAL_RCC_GPIOA_CLK_ENABLE()、
__HAL_RCC_DMA2_CLK_ENABLE(),保证外设和 DMA 都能跑起来。
2.配 GPIOA 的 PA1
把 PA1 设置成 模拟输入:GPIO_PIN_1、GPIO_MODE_ANALOG,这样它就能直接挂在 ADC1_IN1 上,不会被数字电路干扰。
3.设 ADC 基本工作模式
用 HAL_ADC_Init() 一次性把 ADC1 设成:12 位分辨率、右对齐、连续转换打开、规则组 1 个通道、软件触发、并 打开 DMAContinuousRequests——这一步决定了 ADC 每转换完一次就自动向 DMA 请求搬运。
4.选通道 + 采样时间
用 ADC_ChannelConfTypeDef 告诉 ADC:我要用 通道 1(PA1),序列号 1,采样时间 15Cycles;HAL 会自动把这些信息写进 SQR 和 SMPR 寄存器。
5.配 DMA 流
针对 DMA2_Stream0 CH0 填结构体:方向 外设到存储器,外设地址固定(&ADC1->DR),内存地址递增,数据宽度 半字,循环模式 让 256 个半字搬完后自动从头再来,这样程序就不用反复启动 DMA。
6.把 DMA 句柄挂到 ADC
__HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc1) 把 DMA 句柄 绑定到 ADC 句柄;HAL 内部会在启动 ADC 时顺带启动这条 DMA 流,省去手动
HAL_DMA_Start()。
7.启动转换
最后一步在 main() 里调用 HAL_ADC_Start_DMA(&hadc1, adcBuf, 256): ADC 开始连续转换,DMA 每收到一次请求就把 ADC1_DR 里的 16 位结果搬到 adcBuf[] 中,256 个半字搬完后自动循环。
uint16_t adcBuf[BUF_LEN] __attribute__((section(".noinit"))); // 避免被初始化成 0,单缓冲模式 ADC_HandleTypeDef hadc1;DMA_HandleTypeDef hdma_adc1; /* 系统时钟 168 MHz(HSE 25 MHz) */void SystemClock_Config(void){ RCC_OscInitTypeDef osc = {0}; RCC_ClkInitTypeDef clk = {0}; osc.OscillatorType = RCC_OSCILLATORTYPE_HSE; osc.HSEState = RCC_HSE_ON; osc.PLL.PLLState = RCC_PLL_ON; osc.PLL.PLLSource = RCC_PLLSOURCE_HSE; osc.PLL.PLLM = 25; osc.PLL.PLLN = 336; osc.PLL.PLLP = RCC_PLLP_DIV2; // 168 MHz HAL_RCC_OscConfig(&osc); clk.ClockType = RCC_CLOCKTYPE_SYSCLK | RCC_CLOCKTYPE_HCLK | RCC_CLOCKTYPE_PCLK1 | RCC_CLOCKTYPE_PCLK2; clk.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK; clk.AHBCLKDivider = RCC_SYSCLK_DIV1; clk.APB1CLKDivider = RCC_HCLK_DIV4; clk.APB2CLKDivider = RCC_HCLK_DIV2; HAL_RCC_ClockConfig(&clk, FLASH_LATENCY_5);} /* ADC1 + DMA 初始化 */void MX_ADC1_Init(void){ ADC_ChannelConfTypeDef sConfig = {0}; __HAL_RCC_ADC1_CLK_ENABLE(); __HAL_RCC_GPIOA_CLK_ENABLE(); GPIO_InitTypeDef gpio = {0}; gpio.Pin = GPIO_PIN_1; gpio.Mode = GPIO_MODE_ANALOG; HAL_GPIO_Init(GPIOA, &gpio); hadc1.Instance = ADC1; hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4; hadc1.Init.Resolution = ADC_RESOLUTION_12B; hadc1.Init.ScanConvMode = ADC_SCAN_DISABLE; hadc1.Init.ContinuousConvMode = ENABLE; // 连续转换 hadc1.Init.DiscontinuousConvMode = DISABLE; hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE; hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion = 1; hadc1.Init.DMAContinuousRequests = ENABLE; // 关键:允许 DMA 连续请求 HAL_ADC_Init(&hadc1); sConfig.Channel = ADC_CHANNEL_1; // PA1 sConfig.Rank = 1; sConfig.SamplingTime = ADC_SAMPLETIME_15CYCLES; HAL_ADC_ConfigChannel(&hadc1, &sConfig); /* DMA2_Stream0 CH0 用于 ADC1 */ __HAL_RCC_DMA2_CLK_ENABLE(); hdma_adc1.Instance = DMA2_Stream0; hdma_adc1.Init.Channel = DMA_CHANNEL_0; hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY; // ← 外设→存储器 hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE; hdma_adc1.Init.MemInc = DMA_MINC_ENABLE; hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD; hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD; hdma_adc1.Init.Mode = DMA_CIRCULAR; // 循环模式 hdma_adc1.Init.Priority = DMA_PRIORITY_HIGH; hdma_adc1.Init.FIFOMode = DMA_FIFOMODE_DISABLE; //禁用FIFO,直接模式 HAL_DMA_Init(&hdma_adc1); //未设置MBURST或PBURST,默认单独传输 __HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc1); // 把 DMA 句柄挂到 ADC} int main(void){ HAL_Init(); SystemClock_Config(); MX_ADC1_Init(); /* 启动 ADC + DMA:一次性搬 256 个半字,循环进行 */ HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adcBuf, BUF_LEN); while (1) { /* 在这里可以安全读取 adcBuf(双缓冲/循环模式) */ HAL_Delay(500); }}
05
—
代码链接
工程代码链接:
https://gitee.com/ylm1101111/stm32_basic7.git
0