STM32 ADC采样详解(标准库版):普通模式与DMA模式,附完整可用代码

前言

ADC(模数转换器)是嵌入式开发中测量模拟信号的核心外设,从简单的电压读取到复杂的传感器数据采集都离不开它。STM32F103 内置 12 位逐次逼近型 ADC,最多支持 18 个通道,在 72MHz 主频下最高采样率达 1Msps,性能非常实用。

在实际项目中,我们最常接触两种采集方式:

  • 普通查询模式:简单直接,适合低频率、单通道的采集。
  • DMA 传输模式:配合 DMA 进行多通道连续采集,CPU 零负担,是工程应用的主流选择。

本文使用 标准外设库(SPL) ,以 STM32F103C8T6 为核心,先讲清 ADC 时基与规则通道原理,再分别给出普通模式和 DMA 模式的 完整可运行代码,所有配置均经过验证,复制到你的工程里即可点亮功能。


一、STM32 ADC 基础原理

1.1 逐次逼近型 ADC 如何工作

STM32 的 ADC 采用逐次逼近架构,内部通过二分法比较输入电压与 DAC 输出,经过 12 个时钟周期(12 位分辨率)逼近真实值,转换结果范围为 0 ~ 4095

1.2 规则通道与注入通道

  • 规则通道组 :可安排 16 个通道的转换序列,按顺序依次转换,结果只存到一个 共用的 16 位数据寄存器 ADC_DR 中。如果 CPU 来不及读取,新的结果会覆盖旧值,这就是多通道采集必须依赖 DMA 的根本原因。
  • 注入通道组 :最多 4 个通道,拥有 独立的数据寄存器,可打断规则通道的转换序列,常用于紧急采样(如电流环控制)。

本文将专注于使用最广泛的 规则通道组

1.3 时钟与采样时间

ADC 挂载在 APB2 总线上,时钟通过分频器供给,最大不超过 14MHz

标准配置:72MHz ÷ 6 = 12MHz

单次转换所需时间 = 采样时间 + 12.5 个 ADC 时钟周期。例如采样时间选 55.5 周期,则总时间 ≈ 68 / 12M ≈ 5.67μs,采样率约 176kHz。

1.4 关键配置参数一览

参数 含义 普通单通道 DMA 多通道
ADC_Mode 工作模式 ADC_Mode_Independent ADC_Mode_Independent
ADC_ScanConvMode 扫描模式 DISABLE ENABLE
ADC_ContinuousConvMode 连续转换 按需 ENABLE
ADC_DataAlign 数据对齐 ADC_DataAlign_Right ADC_DataAlign_Right
ADC_NbrOfChannel 规则通道数量 1 实际通道数

二、普通模式(查询法)

特点:CPU 主动查询 EOC 标志,手动读取 ADC_DR。代码清晰,适用于低频单通道场景。

2.1 单通道单次转换(PA0)

c 复制代码
/* adc_single.c */
#include "adc_single.h"

/**
 * @brief  ADC1 单通道初始化(PA0, 通道0)
 */
void ADC1_Single_Init(void)
{
    ADC_InitTypeDef       ADC_InitStructure;
    GPIO_InitTypeDef      GPIO_InitStructure;

    /* 1. 开启时钟 */
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE);
    RCC_ADCCLKConfig(RCC_PCLK2_Div6);               // ADC 时钟 = 72M/6 = 12MHz

    /* 2. PA0 模拟输入 */
    GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_0;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    /* 3. ADC 基础配置 */
    ADC_InitStructure.ADC_Mode               = ADC_Mode_Independent;
    ADC_InitStructure.ADC_ScanConvMode       = DISABLE;          // 单通道不扫描
    ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;          // 单次转换
    ADC_InitStructure.ADC_ExternalTrigConv   = ADC_ExternalTrigConv_None; // 软件触发
    ADC_InitStructure.ADC_DataAlign          = ADC_DataAlign_Right;
    ADC_InitStructure.ADC_NbrOfChannel       = 1;               // 通道数
    ADC_Init(ADC1, &ADC_InitStructure);

    /* 4. 配置规则通道:通道0,排序第1,采样时间 55.5 周期 */
    ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);

    /* 5. 使能 ADC,并校准(必须先使能再校准) */
    ADC_Cmd(ADC1, ENABLE);

    ADC_ResetCalibration(ADC1);
    while(ADC_GetResetCalibrationStatus(ADC1));
    ADC_StartCalibration(ADC1);
    while(ADC_GetCalibrationStatus(ADC1));
}

/**
 * @brief  触发一次软件转换,返回 ADC 值
 */
uint16_t ADC1_GetValue(void)
{
    ADC_SoftwareStartConvCmd(ADC1, ENABLE);                 // 启动转换
    while(ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET); // 等待转换完成
    return ADC_GetConversionValue(ADC1);                    // 读取结果(同时清EOC)
}
c 复制代码
/* adc_single.h */
#ifndef __ADC_SINGLE_H__
#define __ADC_SINGLE_H__

#include "stm32f10x.h"

void     ADC1_Single_Init(void);
uint16_t ADC1_GetValue(void);

#endif

使用示例(main.c)

c 复制代码
int main(void)
{
    uint16_t adc_val;
    float    vol;
    ADC1_Single_Init();

    while(1) {
        adc_val = ADC1_GetValue();
        vol = adc_val * 3.3f / 4095;   // 参考电压 3.3V
        // 可通过串口打印 vol
    }
}

2.2 多通道连续扫描(仅演示,不推荐用于实际项目)

开启扫描 + 连续转换后,ADC 会按顺序循环转换各个通道,但 读取时无法确认当前数据属于哪个通道 ,极易覆盖。此模式仅供学习了解流程,正式项目请直接跳到 DMA 模式

c 复制代码
/* 不推荐!仅作演示 */
void ADC1_Multi_NoDMA_Init(void)
{
    // ... 时钟、GPIO 同前 ...
    ADC_InitStructure.ADC_ScanConvMode       = ENABLE;  // 扫描模式
    ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;  // 连续转换
    ADC_InitStructure.ADC_NbrOfChannel       = 2;
    // ... 其余相同 ...
    ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);
    ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5);
    // 启动一次连续转换
    ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}

警告:这种方式读到的数据顺序不可靠,请不要在项目中采用。


三、DMA 模式(多通道自动采集)

3.1 DMA 如何解决多通道采集难题

DMA(直接存储器访问)可以不经过 CPU,直接将 ADC_DR 中的结果搬运到内存数组里。配合 扫描模式 + 连续转换 + DMA 循环模式,可实现:

  • ADC 自动扫描所有通道 → 转换结果由 DMA 按序存入数组 → 数组满后自动从头覆盖。
  • CPU 完全被解放,只需在需要时读取数组即可。

3.2 双通道 DMA 循环传输(PA0、PA1)

我们以最常见的双通道电压采集为例,数据存放于 ADC_ConvertedValue[2] 中,索引 0 对应通道 0(PA0),索引 1 对应通道 1(PA1)。

c 复制代码
/* adc_dma.c */
#include "adc_dma.h"

#define ADC_CH_NUM  2
uint16_t ADC_ConvertedValue[ADC_CH_NUM] = {0};

/**
 * @brief  ADC1 + DMA 初始化(PA0/通道0, PA1/通道1)
 */
void ADC1_DMA_Init(void)
{
    ADC_InitTypeDef     ADC_InitStructure;
    GPIO_InitTypeDef    GPIO_InitStructure;
    DMA_InitTypeDef     DMA_InitStructure;

    /*========== 1. 使能时钟 ==========*/
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE);
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);      // DMA1 时钟
    RCC_ADCCLKConfig(RCC_PCLK2_Div6);                      // ADC 时钟 12MHz

    /*========== 2. 配置 PA0, PA1 为模拟输入 ==========*/
    GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_0 | GPIO_Pin_1;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    /*========== 3. 配置 DMA1 通道1 ==========*/
    DMA_DeInit(DMA1_Channel1);                              // 复位 DMA 通道

    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;              // 外设地址
    DMA_InitStructure.DMA_MemoryBaseAddr     = (uint32_t)ADC_ConvertedValue;     // 内存地址
    DMA_InitStructure.DMA_DIR                = DMA_DIR_PeripheralSRC;            // 外设到内存
    DMA_InitStructure.DMA_BufferSize         = ADC_CH_NUM;                       // 传输个数(两个通道)
    DMA_InitStructure.DMA_PeripheralInc      = DMA_PeripheralInc_Disable;        // 外设地址固定
    DMA_InitStructure.DMA_MemoryInc          = DMA_MemoryInc_Enable;             // 内存地址递增
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;  // 半字(16位)
    DMA_InitStructure.DMA_MemoryDataSize     = DMA_MemoryDataSize_HalfWord;
    DMA_InitStructure.DMA_Mode               = DMA_Mode_Circular;                // 循环模式
    DMA_InitStructure.DMA_Priority           = DMA_Priority_High;
    DMA_InitStructure.DMA_M2M                = DMA_M2M_Disable;
    DMA_Init(DMA1_Channel1, &DMA_InitStructure);

    DMA_Cmd(DMA1_Channel1, ENABLE);                         // 使能 DMA 通道

    /*========== 4. 配置 ADC1 ==========*/
    ADC_InitStructure.ADC_Mode               = ADC_Mode_Independent;
    ADC_InitStructure.ADC_ScanConvMode       = ENABLE;                     // 多通道必须扫描
    ADC_InitStructure.ADC_ContinuousConvMode = ENABLE;                     // 连续转换
    ADC_InitStructure.ADC_ExternalTrigConv   = ADC_ExternalTrigConv_None;
    ADC_InitStructure.ADC_DataAlign          = ADC_DataAlign_Right;
    ADC_InitStructure.ADC_NbrOfChannel       = ADC_CH_NUM;                 // 通道总数
    ADC_Init(ADC1, &ADC_InitStructure);

    /* 配置规则通道:序列1 - 通道0, 序列2 - 通道1 */
    ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);
    ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5);

    /* 使能 ADC 的 DMA 请求(极易遗漏!) */
    ADC_DMACmd(ADC1, ENABLE);

    /* 使能 ADC 并校准 */
    ADC_Cmd(ADC1, ENABLE);

    ADC_ResetCalibration(ADC1);
    while(ADC_GetResetCalibrationStatus(ADC1));
    ADC_StartCalibration(ADC1);
    while(ADC_GetCalibrationStatus(ADC1));

    /* 启动连续转换(只需触发一次,之后自动循环) */
    ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}
c 复制代码
/* adc_dma.h */
#ifndef __ADC_DMA_H__
#define __ADC_DMA_H__

#include "stm32f10x.h"

extern uint16_t ADC_ConvertedValue[2];

void ADC1_DMA_Init(void);

#endif

主程序使用示例(main.c)

c 复制代码
#include "adc_dma.h"

int main(void)
{
    float vol_ch0, vol_ch1;
    ADC1_DMA_Init();   // 启动后 DMA 自动循环搬运

    while(1) {
        /* ADC_ConvertedValue[0] 始终存放通道0(PA0)的最新值 */
        /* ADC_ConvertedValue[1] 始终存放通道1(PA1)的最新值 */
        vol_ch0 = ADC_ConvertedValue[0] * 3.3f / 4095;
        vol_ch1 = ADC_ConvertedValue[1] * 3.3f / 4095;
        // 延时或处理数据......
    }
}

3.3 代码要点批注

  • DMA_Mode_Circular:搬运完两个通道后自动跳回数组开头,与 ADC 连续转换完美配合,永不停止。
  • DMA_MemoryInc = Enable :每搬运一次地址递增,保证 [0] 存通道 0,[1] 存通道 1。
  • ADC_DMACmd(ADC1, ENABLE):老手也偶尔忘记这行,没有它 DMA 不会收到任何请求。
  • 校准顺序 :必须 ADC_Cmd(ENABLE) 再校准,否则校准无效,结果会有偏差且很难排查。

四、常见避坑指南

  1. 校准顺序错误
    ADC_Cmd 一定要在校准函数之前调用,这是标准库用户最容易踩的坑。

  2. 忘记设置 ADC 时钟分频

    不调用 RCC_ADCCLKConfig(RCC_PCLK2_Div6) 的话,ADC 时钟默认为 72MHz,远超 14MHz 上限,会导致 ADC 工作异常。

  3. DMA 通道对应错误

    ADC1 固定使用 DMA1_Channel1,其他外设不同通道,切勿混淆。

  4. 数据错位(通道对应不上)

    检查 ADC_RegularChannelConfig 的排序参数:第 1 个通道对应数组 [0],第 2 个对应 [1],以此类推。

  5. 查询模式下忘记等待 EOC

    若不等转换完成直接读 ADC_DR,得到的是旧值或无效值。使用 ADC_GetConversionValue 会同时清除 EOC 标志。


五、总结

模式 适用场景 优缺点
普通单次查询 低频单通道(如温度巡检) 简单,但阻塞 CPU
多通道查询 不建议实际使用 数据极易覆盖,仅作学习
DMA 循环 多通道连续采集(推荐) CPU 零负担,数据准确可靠

掌握了普通模式和 DMA 模式的标准库写法后,应对绝大多数 STM32 模拟信号采集需求都将游刃有余。建议先在普通模式下调通单通道,确认硬件没问题,再切换到 DMA 模式,这样可以排除很多基础配置错误。

文中代码均可直接复制到 KEIL 标准库工程中编译运行,若遇到问题,欢迎在评论区讨论交流。


参考资料

  • STM32F103x8/xB 数据手册
  • RM0008 Reference Manual
  • STM32F10x Standard Peripherals Library 使用手册
相关推荐
XTIOT6661 小时前
工业数据采集设备选型 —— 实体键盘 PDA 的技术优势与场景适配(基于 XT8001D 实践)摘要
大数据·嵌入式硬件·物联网·计算机外设
你疯了抱抱我2 小时前
【自用】Kicad 导入嘉立创元器件封装(NLBN插件)
嵌入式硬件·嵌入式·pcb·电路·电子
a83331962 小时前
C语言嵌入汇编详解
汇编·单片机·语言
三佛科技-134163842122 小时前
LED化妆镜方案开发, LED化妆镜MCU主控芯片如何选择?(FT60F011、FT60F021、FT61FC4F、FT62FC33、FT32F103)
单片机·嵌入式硬件·物联网·智能家居·pcb工艺
周周记笔记2 小时前
【问题答疑】三极管的饱和道导通压降在工程上一般是多少?
嵌入式硬件·硬件工程
踏着七彩祥云的小丑2 小时前
嵌入式测试学习第 14 天:数字电路基础:高低电平、0和1、逻辑电平
单片机·嵌入式硬件
拾知_H2 小时前
STM32/PWM占空比配置
stm32·单片机·嵌入式·定时器·pwm
星华云2 小时前
[STM32] 硬件I2C主模式时序
stm32·单片机·嵌入式硬件
木子单片机2 小时前
基于51单片机汽车智能灯光控制系统
stm32·单片机·嵌入式硬件·汽车·51单片机·keil