为什么需要 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:勾选 IN0IN1IN2(对应 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 传输:

#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

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 循环模式,读取三路电压值并通过串口打印:

/* 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 循环模式,即可实现稳定可靠的多通道实时采集。此方案在电机控制、传感器采集、音频采样等场景均有广泛应用。掌握这一技术,是嵌入式开发者进阶的重要一步。