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 使用手册
相关推荐
清风6666664 小时前
基于单片机的锅炉压力与温度监测报警系统设计
单片机·嵌入式硬件·毕业设计·课程设计·期末大作业
ACP广源盛139246256736 小时前
GSV2221 显示转换芯片@ACP#赋能 RTX Spark 端侧 AI 设备,构建多屏全模态视觉交互新生态
大数据·人工智能·嵌入式硬件·gpt·spark·电脑·音视频
Szime6 小时前
TJA1044T/1现货查询与汽车CAN通信应用采购注意事项
嵌入式硬件·汽车
rhythm-ring6 小时前
《汽车智能高边开关PROFET:电流检测与标定实战》
嵌入式硬件·汽车
点灯小铭9 小时前
基于单片机的鱼缸监测与远程管理系统设计
数据库·单片机·嵌入式硬件·毕业设计·课程设计·期末大作业
Szime9 小时前
DS90UB924TRHSRQ1现货交期与车载视频SerDes项目采购说明
嵌入式硬件·汽车
清风6666669 小时前
基于单片机的罐体压力控制器设计与实现
单片机·嵌入式硬件·毕业设计·51单片机·课程设计·期末大作业
嵌入式-老费10 小时前
esp32开发与应用(题外话之j-link调试)
嵌入式硬件
点灯小铭10 小时前
基于单片机的热量计测量系统设计
单片机·嵌入式硬件·毕业设计·课程设计·期末大作业
wengqidaifeng10 小时前
嵌入式小白第三站:UART、I2C、SPI、ADC 怎么学?从传感器读数到完整小项目
stm32·单片机·嵌入式硬件·51单片机