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 只在半满/全满时介入处理。
四、典型场景与配置示例
以最常见的"定时器触发 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 不工作"的问题。