1. 课程导入

在嵌入式系统中,CPU 的主频提升往往带来功耗和成本的双重压力,而真正决定数据采集系统上限的,常常不是 CPU 算得多快,而是数据能不能"不经过 CPU"高效地搬运。STM32 的 DMA(Direct Memory Access,直接存储器访问)正是解决这一矛盾的关键外设。本课程围绕"DMA 与高性能采集"展开,从寄存器级原理讲到工程级实战,帮助你把 ADC、DMA、定时器、中断与内存组织成一个稳定、高速、低占用的采集链路。

一、学习目标

完成本课程后,你应当能够独立完成以下任务:

  • 理解 DMA 的通道、请求、仲裁、传输方向与地址递增等核心机制,能读懂参考手册中的 DMA 框图与寄存器描述。
  • 掌握 STM32 常见 DMA 控制器(如 DMA1/DMA2、BDMA、MDMA、GPDMA)的差异与选型依据。
  • 能够配置 ADC + 定时器触发 + DMA 循环搬运,实现固定采样率、无 CPU 干预的连续采集。
  • 能够使用双缓冲(Double Buffer)与半传输/传输完成中断,构建"采集—处理"流水线,避免数据覆盖与丢点。
  • 能够定位 DMA 常见故障:不触发、只传一次、数据错位、溢出丢包、Cache 一致性问题等。
  • 能够评估采集系统的吞吐量、延迟与 CPU 占用率,并据此优化缓冲区大小与中断策略。

二、核心概念

DMA 的本质是"外设与存储器之间的数据搬运工"。CPU 只需给出源地址、目的地址、传输数量和触发条件,DMA 控制器便在后台完成搬运,并在完成时通过中断或标志位通知 CPU。理解以下概念,是后续所有实验的基础。

概念 含义 典型取值/说明
通道 / Stream DMA 控制器中独立的传输单元,每个通道绑定一组固定外设请求 STM32F4 的 DMA1 有 8 个 Stream,每个 Stream 可选 8 个 Channel
请求(Request) 外设发出的搬运触发信号,如 ADC 转换完成、USART 发送空 硬件请求优先于软件触发
传输方向 外设→存储器、存储器→外设、存储器→存储器 ADC 采集属于外设→存储器
地址递增 每传一个数据后地址是否自动加一 外设寄存器地址通常固定,内存地址递增
数据宽度 单次搬运的字节数:8/16/32 位 ADC 12 位结果常用 16 位对齐
循环模式 传输计数归零后自动重装,实现连续采集 Circular 模式是连续采集的关键
半传输/传输完成 缓冲区一半或全部填满时产生中断 双缓冲流水线的基础

三、整体框架

本课程按"原理—配置—实战—优化—排障"的路径推进。下图展示了从模拟信号到内存缓冲区的完整采集信号链,以及 DMA 在其中的位置:定时器产生精确触发,ADC 完成采样与转换,DMA 将结果搬入内存,CPU 只在半满/全满时介入处理。

STM32 高性能采集信号链与 DMA 位置 模拟信号源 传感器 / 电压 定时器 TIM TRGO 精确触发 ADC 采样 + 转换 12 位结果 DMA 控制器 通道 / Stream 循环 + 双缓冲 内存缓冲区 Buffer A / Buffer B 半满 / 全满中断 CPU / 应用 滤波 / 计算 / 通信 模拟输入 触发 转换完成请求 搬运数据 中断通知 关键路径:TIM 触发 → ADC 转换 → DMA 请求 → 内存写入 → 半满/全满中断 → CPU 处理 CPU 仅在缓冲区边界介入,采集过程几乎零占用,实现高采样率与低抖动

四、典型场景与配置示例

以最常见的"定时器触发 ADC + DMA 循环采集"为例,初始化代码的核心结构如下。注意 DMA 配置中方向为外设到存储器、外设地址不递增、存储器地址递增、数据宽度 16 位、循环模式使能。

// 1. 定时器触发配置(以 TIM2 TRGO 为例)
htim2.Instance = TIM2;
htim2.Init.Prescaler = 84 - 1;        // 84MHz / 84 = 1MHz
htim2.Init.Period = 1000 - 1;         // 1MHz / 1000 = 1kHz 采样率
HAL_TIM_Base_Init(&htim2);
TIM_MasterConfigTypeDef sMaster = {0};
sMaster.MasterOutputTrigger = TIM_TRGO_UPDATE;
HAL_TIMEx_MasterConfigSynchronization(&htim2, &sMaster);

// 2. ADC 配置为外部触发
hadc1.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T2_TRGO;
hadc1.Init.ContinuousConvMode = DISABLE;   // 由触发驱动
hadc1.Init.DMAContinuousRequests = ENABLE;

// 3. DMA 配置:外设 -> 内存,循环模式
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;            // 循环采集
HAL_DMA_Init(&hdma_adc1);
__HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc1);

// 4. 启动:DMA 先就绪,再启动定时器
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, BUF_LEN);
HAL_TIM_Base_Start(&htim2);
启动顺序很重要:应先启动 ADC 的 DMA 传输,再启动定时器触发。否则前几个触发脉冲到来时 DMA 尚未就绪,会造成数据错位或丢点。

五、常见误区与复盘

  • 误区一:认为 DMA 一定比中断快。DMA 的优势在于"不占用 CPU 搬运",但单次传输延迟未必低于中断。高频率小数据量场景要综合评估。
  • 误区二:忘记使能 DMA 时钟或中断。DMA1、DMA2 挂在不同总线,时钟未开时配置看似成功,实际毫无反应。
  • 误区三:缓冲区太小导致覆盖。循环模式下,若 CPU 处理速度跟不上,旧数据会被新数据覆盖。应使用双缓冲并合理设置半满中断。
  • 误区四:忽略 Cache 一致性。在带 D-Cache 的 H7 等系列上,DMA 写入内存后 CPU 可能读到旧值,需要配置 MPU 或执行 Cache 无效化操作。
  • 误区五:外设地址递增误开。ADC 数据寄存器地址固定,若开启外设地址递增,第二次传输就会访问错误地址。
排障时优先检查三件事:DMA 时钟是否使能、请求映射是否选对通道、传输方向与地址递增是否匹配。这三项覆盖了大多数"DMA 不工作"的问题。