## 为什么需要 ADC + DMA? STM32 内置 ADC 支持多通道扫描模式,但若在中断中读取数据,每次转换完成都会触发 CPU 中断。当采样率较高或通道较多时,频繁的中断会严重占用 CPU 时间,甚至导致数据丢失。DMA(Direct Memory Access)可以在 ADC 转换完成后直接硬件搬运结果到内存缓冲区,全程无需 CPU 参与。这样 CPU 只需在缓冲区满时处理数据,或直接使用循环缓冲区的实时数据,极大提升系统效率。 ## 原理:ADC 连续转换 + DMA 循环模式 **关键机制**: - **连续转换模式(Continuous Conversion Mode)**:ADC 自动重复扫描所有配置的通道,无需软件触发。 - **DMA 循环模式(Circular Mode)**:DMA 持续将 ADC 数据寄存器(DR)的值搬运到用户缓冲区,当缓冲区满时自动回卷。 - **数据对齐**:ADC 分辨率通常为 12 位,右对齐存储在 16 位半字中,DMA 传输单位设置为 Half Word(16 位)。 当启动 ADC 后,硬件会按照通道顺序逐个转换,每次转换结束触发 DMA 请求,DMA 将 DR 中的数据按序写入内存。整个过程不产生任何中断,CPU 可以执行其他任务,只需在需要时读取缓冲区。 ## 配置步骤(以 STM32F103 为例) ### 1. 使用 STM32CubeMX 初始化 - **ADC1**:勾选 `IN0`、`IN1`、`IN2`(对应 PA0、PA1、PA2),设置扫描模式启用,连续转换启用,通道采样时间可调(如 55.5 Cycles)。 - **DMA**:在 ADC1 设置中启用 DMA 请求,选择 `Circular` 模式,数据宽度为 `Half Word`,内存地址自增。 - **外部时钟**:ADC 时钟通常由 APB2 分频得到,建议设为 12 MHz 左右(PCLK2 6 分频)。 - **生成代码**:生成工程后,ADC 和 DMA 句柄已自动初始化。 ### 2. 关键代码说明 在 `main.c` 中,用户只需调用 HAL 库函数启动 ADC 的 DMA 传输: ```c #define ADC_CHANNELS 3 uint16_t adc_buf[ADC_CHANNELS]; // 缓冲区,存放三个通道的采样值 int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_DMA_Init(); MX_ADC1_Init(); // 启动 ADC 并开启 DMA 传输,连续采样自动填充 adc_buf HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, ADC_CHANNELS); while (1) { // 主循环中可以随时读取 adc_buf[0] ~ adc_buf[2] // 无需任何中断或轮询 ADC 状态 float v0 = adc_buf[0] * 3.3f / 4095.0f; float v1 = adc_buf[1] * 3.3f / 4095.0f; float v2 = adc_buf[2] * 3.3f / 4095.0f; printf("V0=%.2f V1=%.2f V2=%.2f\r\n", v0, v1, v2); HAL_Delay(500); } } ``` ### 3. 自定义回调处理(可选) 若需要在每轮采样完成时做数据处理,可以重写 `HAL_ADC_ConvCpltCallback`: ```c void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc->Instance == ADC1) { // 这一轮三个通道数据已全部更新到 adc_buf // 可以在此处做滤波、阈值判断等操作,但注意保持回调短小 adc_average = (adc_buf[0] + adc_buf[1] + adc_buf[2]) / 3; } } ``` 在循环模式下,每传输完一轮(DMA 半传输或全传输)都会触发回调,具体取决于 DMA 中断配置。如果不需要回调,可以只依赖主循环读取缓冲区。 ## 完整代码示例(HAL 库) 下面是一个完整的可运行示例,使用 ADC1 的通道 0~2,DMA 循环模式,读取三路电压值并通过串口打印: ```c /* Includes */ #include "main.h" #include "stdio.h" ADC_HandleTypeDef hadc1; DMA_HandleTypeDef hdma_adc1; uint16_t adc_buf[3]; uint8_t dma_ready = 0; void SystemClock_Config(void); static void MX_GPIO_Init(void); static void MX_DMA_Init(void); static void MX_ADC1_Init(void); int fputc(int ch, FILE *f) { ITM_SendChar(ch); return ch; } int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_DMA_Init(); MX_ADC1_Init(); HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf, 3); while (1) { if (dma_ready) // 在回调中置位 { dma_ready = 0; printf("ADC0: %d, ADC1: %d, ADC2: %d\r\n", adc_buf[0], adc_buf[1], adc_buf[2]); } } } void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if (hadc->Instance == ADC1) { dma_ready = 1; // 通知主循环处理数据 } } static void MX_ADC1_Init(void) { hadc1.Instance = ADC1; hadc1.Init.ScanConvMode = ADC_SCAN_ENABLE; // 扫描模式 hadc1.Init.ContinuousConvMode = ENABLE; // 连续转换 hadc1.Init.DiscontinuousConvMode = DISABLE; hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START; // 软件触发 hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion = 3; // 3个通道 HAL_ADC_Init(&hadc1); ADC_ChannelConfTypeDef sConfig = {0}; sConfig.Channel = ADC_CHANNEL_0; sConfig.Rank = 1; // 采样顺序1 sConfig.SamplingTime = ADC_SAMPLETIME_55CYCLES_5; HAL_ADC_ConfigChannel(&hadc1, &sConfig); sConfig.Channel = ADC_CHANNEL_1; sConfig.Rank = 2; HAL_ADC_ConfigChannel(&hadc1, &sConfig); sConfig.Channel = ADC_CHANNEL_2; sConfig.Rank = 3; HAL_ADC_ConfigChannel(&hadc1, &sConfig); } ``` 注意:`MX_DMA_Init` 中需要将 DMA 请求绑定到 ADC1,示例略去该函数细节,实际由 CubeMX 生成。 ## 注意事项与常见问题 - **缓冲区大小**:DMA 缓冲区大小必须等于扫描通道数(或不小于通道数 * 传输次数),否则会越界或数据错位。 - **DMA 模式**:一定要选择 `Circular` 模式。若用 `Normal` 模式,则只能采集一轮,之后必须重新启动 DMA。 - **数据访问冲突**:主循环和回调都可能访问 `adc_buf`,建议加 `volatile` 修饰,或使用双缓冲机制避免读写撕裂。 - **函数耗时**:`HAL_ADC_ConvCpltCallback` 是在 DMA 中断中执行的,不要在里面做耗时操作(如打印、延时),最好只置标志位。 - **ADC 时钟**:ADC 时钟过大可能导致采样不准确,尤其在高阻抗信号源时,应适当降低 ADC 时钟或增加采样周期。 - **多 ADC 同步**:若需要精确同步采样多个通道,可以考虑使用 ADC 的注入组或双 ADC 模式,而非简单扫描。 - **参考电压**:默认使用 VREF+ 作为参考,如果要高精度采集,建议使用外部基准电压源。 ## 总结 STM32 ADC + DMA 多通道连续采样方案,充分利用了硬件自动化的能力,将 CPU 从繁重的数据搬运中解放出来。通过合理配置扫描模式、连续转换和 DMA 循环模式,即可实现稳定可靠的多通道实时采集。此方案在电机控制、传感器采集、音频采样等场景均有广泛应用。掌握这一技术,是嵌入式开发者进阶的重要一步。