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 使用手册
相关推荐
FreakStudio6 天前
W55MH32L-EVB 上手测评:硬件 TCP/IP 加持的以太网单片机,MicroPython 零门槛开发
python·单片机·嵌入式·大学生·面向对象·并行计算·电子diy·电子计算机
✎ ﹏梦醒͜ღ҉繁华落℘12 天前
单片机基础知识---stm32单片机的优先级
stm32·单片机·mongodb
u1521096484912 天前
S.S.Audio PRO A2音频隔离器
嵌入式硬件·音视频·实时音视频·视频编解码·视频
zd84510150012 天前
RS485 总线详解
单片机·嵌入式硬件
半条-咸鱼12 天前
【STM32】I2C协议原理、HAL读写与OLED显示操作
嵌入式硬件·c·信息与通信
牛根生同志12 天前
SPI数据收发的时候 TXE与RXNE标志位置位的时机
stm32·spi·transfer
wohoo_wangzi12 天前
苏州晟雅泰电子:关于W25Q128JVSIQ这个芯片物料的参数,规格及应用领域
嵌入式硬件
goldenrolan12 天前
学习型红外控制系统稳定性挂测工装专项总结
软件测试·python·stm32·嵌入式·红外
✎ ﹏梦醒͜ღ҉繁华落℘12 天前
编程基础 --高内聚,低耦合
c语言·单片机
科芯创展12 天前
1A,1MHz,30VIN,XZ4115,降压恒流LED驱动芯片
单片机·嵌入式硬件