单片机 ADC 电压采集实战:从配置到数据读取

1. 前言

ADC 是单片机中非常重要的外设之一。
在实际项目中,只要需要采集模拟量,基本都会用到 ADC。

常见应用包括:

  • 采集电位器电压;
  • 采集电池电压;
  • 读取光敏电阻;
  • 读取热敏电阻;
  • 采集电流检测芯片输出;
  • 采集传感器模拟电压。

本文主要基于 STM32 标准外设库,讲解如何使用 STM32F103 的 ADC 完成电压采集。
示例使用:

项目 配置
单片机 STM32F103 系列
外设库 STM32 标准外设库
ADC 外设 ADC1
ADC 通道 ADC_Channel_0
输入引脚 PA0
参考电压 3.3V
转换精度 12 位

本文重点讲解:

  • ADC 的基本原理;
  • STM32 ADC 的关键参数;
  • GPIO 模拟输入配置;
  • ADC 初始化流程;
  • ADC 校准;
  • 软件触发采样;
  • ADC 原始值读取;
  • ADC 数值转换成实际电压;
  • ADC 采样常见问题。

2. ADC 是什么

ADC 全称是:

Analog to Digital Converter

也就是 模数转换器

它的作用是:

把连续变化的模拟电压转换成单片机可以处理的数字量。

单片机本身只能识别数字信号,例如 0 和 1。
但是现实中的电压、温度、光照、电流很多都是连续变化的模拟量。

比如 PA0 引脚输入一个电压:

0V ~ 3.3V

ADC 会把这个电压转换成一个数字值。

STM32F103 的 ADC 通常是 12 位 ADC
12 位 ADC 的数字范围是:

0 ~ 4095

因为:

2^12 = 4096

所以:

输入电压 ADC 理论值
0V 0
1.65V 2048 左右
3.3V 4095

3. ADC 电压换算公式

假设参考电压为 3.3V,ADC 分辨率为 12 位。
ADC 采集值和实际电压之间的关系为:

实际电压 = ADC采样值 × 参考电压 / 4095

对应 C 语言写法:

voltage = adc_value * 3.3f / 4095.0f;

例如 ADC 采样值为 2048,则电压约为:

2048 × 3.3 / 4095 ≈ 1.65V

需要注意:

这里的 3.3V 是 ADC 参考电压,不一定等于理论电源电压。
如果板子的 3.3V 实际只有 3.28V,那么换算时最好使用 3.28V。


4. STM32F103 ADC 关键特性

STM32F103 内部 ADC 常用特点如下:

参数 说明
分辨率 12 位
转换结果范围 0 ~ 4095
输入范围 0 ~ VDDA
参考电压 通常为 VDDA
转换模式 单次转换 / 连续转换
触发方式 软件触发 / 外部触发
数据对齐 左对齐 / 右对齐
通道类型 规则通道 / 注入通道

本文使用最基础、最常用的方式:

ADC1 + PA0 + 单通道 + 单次转换 + 软件触发

这种方式适合初学者理解 ADC 的完整采样流程。


5. ADC 采样硬件连接

这里以电位器采集为例。

电位器三个引脚连接如下:

电位器一端  --->  3.3V
电位器另一端 --->  GND
电位器中间端 --->  PA0

PA0 采集的是电位器中间端输出的模拟电压。

当旋转电位器时,PA0 上的电压会在 0V 到 3.3V 之间变化。
STM32 通过 ADC1 的通道 0 读取 PA0 电压。

需要注意:

ADC 输入电压不能超过 VDDA。
如果 STM32 供电是 3.3V,那么 PA0 输入电压不能超过 3.3V。


6. STM32 ADC 配置流程

使用标准库配置 ADC,一般需要完成以下步骤:

  1. 开启 GPIOA 和 ADC1 时钟;
  2. 配置 PA0 为模拟输入;
  3. 配置 ADC 时钟分频;
  4. 配置 ADC 工作模式;
  5. 配置规则通道;
  6. 使能 ADC;
  7. 复位 ADC 校准;
  8. 等待校准完成;
  9. 启动软件转换;
  10. 等待转换完成;
  11. 读取 ADC 数据寄存器。

完整流程可以理解为:

GPIO 模拟输入配置
↓
ADC 外设参数配置
↓
ADC 通道选择
↓
ADC 校准
↓
启动转换
↓
等待转换结束
↓
读取转换结果

7. ADC1 初始化代码

下面是 ADC1 通道 0 的初始化代码。

#include "stm32f10x.h"

void ADC1_Init_Config(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    ADC_InitTypeDef ADC_InitStructure;

    /* 1. 开启 GPIOA 和 ADC1 时钟 */
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);

    /* 2. 配置 ADC 时钟
     * STM32F103 的 ADC 时钟不能超过 14MHz。
     * 如果 PCLK2 = 72MHz,6 分频后 ADC 时钟为 12MHz。
     */
    RCC_ADCCLKConfig(RCC_PCLK2_Div6);

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

    /* 4. 配置 ADC1 参数 */
    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);

    /* 5. 配置 ADC1 规则通道
     * ADC_Channel_0 对应 PA0。
     * Rank = 1 表示规则序列中的第 1 个转换。
     * 采样时间选择 55.5 个周期,比较稳妥。
     */
    ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);

    /* 6. 使能 ADC1 */
    ADC_Cmd(ADC1, ENABLE);

    /* 7. 复位校准 */
    ADC_ResetCalibration(ADC1);
    while (ADC_GetResetCalibrationStatus(ADC1) == SET);

    /* 8. 开始校准 */
    ADC_StartCalibration(ADC1);
    while (ADC_GetCalibrationStatus(ADC1) == SET);
}

这段代码完成了 ADC1 的基本初始化。
其中最关键的配置有三个:

GPIO_Mode_AIN
ADC_RegularChannelConfig()
ADC_StartCalibration()

8. ADC 时钟为什么要分频

STM32F103 的 ADC 时钟来自 PCLK2 分频。
如果系统时钟是 72MHz,PCLK2 通常也是 72MHz。

但是 ADC 时钟不能太高。
对于 STM32F103,ADC 时钟一般要求不超过 14MHz。

所以常见配置是:

RCC_ADCCLKConfig(RCC_PCLK2_Div6);

这样:

72MHz / 6 = 12MHz

12MHz 在允许范围内,比较常用。

如果 ADC 时钟过高,可能会导致:

  • 转换结果不稳定;
  • 采样误差增大;
  • 不同通道之间数据异常;
  • 高速采样时可靠性变差。

9. ADC 采样时间怎么选

ADC 采样时间决定了 ADC 内部采样电容对外部信号充电的时间。

标准库中常见采样时间有:

ADC_SampleTime_1Cycles5
ADC_SampleTime_7Cycles5
ADC_SampleTime_13Cycles5
ADC_SampleTime_28Cycles5
ADC_SampleTime_41Cycles5
ADC_SampleTime_55Cycles5
ADC_SampleTime_71Cycles5
ADC_SampleTime_239Cycles5

采样时间越短,转换速度越快,但对信号源要求越高。
采样时间越长,采样更稳定,但速度会下降。

如果采集的是电位器、热敏电阻、光敏电阻这类阻抗较高的信号,建议使用较长采样时间,例如:

ADC_SampleTime_55Cycles5
ADC_SampleTime_71Cycles5
ADC_SampleTime_239Cycles5

本文使用:

ADC_SampleTime_55Cycles5

这是一个比较稳妥的选择。


10. 读取 ADC 原始值

ADC 初始化完成后,可以通过软件触发启动一次转换。

读取函数如下:

uint16_t ADC1_GetValue(void)
{
    uint16_t value;

    /* 1. 启动软件转换 */
    ADC_SoftwareStartConvCmd(ADC1, ENABLE);

    /* 2. 等待转换完成 */
    while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET);

    /* 3. 读取转换结果 */
    value = ADC_GetConversionValue(ADC1);

    return value;
}

这里用到一个重要标志位:

标志位 含义
ADC_FLAG_EOC End Of Conversion,转换完成

EOC 置位时,说明本次 ADC 转换已经完成,可以读取数据。

读取数据使用:

ADC_GetConversionValue(ADC1);

这个函数返回的是 ADC 数据寄存器中的转换结果,范围通常是:

0 ~ 4095

11. ADC 数值转换成电压

读取到 ADC 原始值后,还需要转换成实际电压。

函数如下:

float ADC1_GetVoltage(void)
{
    uint16_t adc_value;
    float voltage;

    adc_value = ADC1_GetValue();

    voltage = adc_value * 3.3f / 4095.0f;

    return voltage;
}

如果 ADC 读取值为 2048,计算结果约为:

1.65V

如果 ADC 读取值为 4095,计算结果约为:

3.3V

12. 完整示例代码

下面给出一个完整的 ADC 电压采集示例。
程序通过 ADC1 通道 0 采集 PA0 电压,并把原始值和电压值保存在变量中。

#include "stm32f10x.h"

void ADC1_Init_Config(void);
uint16_t ADC1_GetValue(void);
float ADC1_GetVoltage(void);

uint16_t adc_raw;
float adc_voltage;

int main(void)
{
    ADC1_Init_Config();

    while (1)
    {
        adc_raw = ADC1_GetValue();
        adc_voltage = adc_raw * 3.3f / 4095.0f;

        /*
         * 这里可以配合串口打印 adc_raw 和 adc_voltage。
         * 也可以把 adc_voltage 用于电池电压检测、传感器数据处理等。
         */
    }
}

void ADC1_Init_Config(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;
    ADC_InitTypeDef ADC_InitStructure;

    RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);

    RCC_ADCCLKConfig(RCC_PCLK2_Div6);

    GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    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);

    ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5);

    ADC_Cmd(ADC1, ENABLE);

    ADC_ResetCalibration(ADC1);
    while (ADC_GetResetCalibrationStatus(ADC1) == SET);

    ADC_StartCalibration(ADC1);
    while (ADC_GetCalibrationStatus(ADC1) == SET);
}

uint16_t ADC1_GetValue(void)
{
    uint16_t value;

    ADC_SoftwareStartConvCmd(ADC1, ENABLE);

    while (ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET);

    value = ADC_GetConversionValue(ADC1);

    return value;
}

float ADC1_GetVoltage(void)
{
    uint16_t adc_value;
    float voltage;

    adc_value = ADC1_GetValue();

    voltage = adc_value * 3.3f / 4095.0f;

    return voltage;
}

13. 如果要用串口打印 ADC 数据

实际调试时,通常会把 ADC 采集结果通过串口打印到电脑。

伪代码流程如下:

while (1)
{
    adc_raw = ADC1_GetValue();
    adc_voltage = adc_raw * 3.3f / 4095.0f;

    printf("ADC Raw = %d, Voltage = %.2f V\r\n", adc_raw, adc_voltage);

    Delay_ms(500);
}

如果使用 printf,需要完成串口重定向。
也可以使用自己写的 USART_SendString() 函数进行发送。


14. 多次采样求平均

ADC 采样值可能会有轻微抖动。
这在实际项目中很正常。

为了让数据更稳定,可以使用多次采样求平均。

uint16_t ADC1_GetAverageValue(uint8_t times)
{
    uint32_t sum = 0;
    uint8_t i;

    for (i = 0; i < times; i++)
    {
        sum += ADC1_GetValue();
    }

    return sum / times;
}

使用示例:

adc_raw = ADC1_GetAverageValue(10);
adc_voltage = adc_raw * 3.3f / 4095.0f;

这种方法适合电池电压、温度、电位器这类变化较慢的信号。
如果采集的是高速波形,就不适合简单平均。


15. 电池电压采集注意事项

如果要采集电池电压,经常会遇到一个问题:

电池电压可能大于 3.3V,不能直接接 ADC 引脚。

例如采集 12V 电池电压,就必须先用电阻分压。

分压电路如下:

电池正极
  |
 R1
  |
  +----> ADC 输入
  |
 R2
  |
 GND

ADC 输入电压为:

Vadc = Vin × R2 / (R1 + R2)

所以原始电池电压为:

Vin = Vadc × (R1 + R2) / R2

例如:

R1 = 30kΩ
R2 = 10kΩ

那么:

Vadc = Vin × 10 / 40 = Vin / 4

如果 ADC 采到 3V,说明实际电池电压约为:

3V × 4 = 12V

需要注意:

分压后的电压必须小于 STM32 的 ADC 参考电压,一般不能超过 3.3V。


16. ADC 常见问题分析

16.1 ADC 采样值一直为 0

常见原因:

  • PA0 没有配置成模拟输入;
  • 没有开启 GPIOA 时钟;
  • 没有开启 ADC1 时钟;
  • 输入引脚实际电压为 0V;
  • ADC 通道选择错误;
  • 没有启动软件转换。

16.2 ADC 采样值一直为 4095

常见原因:

  • 输入电压接近或超过 3.3V;
  • ADC 引脚悬空;
  • 外部电路连接错误;
  • 参考电压异常;
  • 通道选择错误。

ADC 引脚不能悬空。
如果引脚悬空,采样值可能乱跳,也可能接近满量程。


16.3 ADC 数值抖动明显

常见原因:

  • 电源纹波较大;
  • 输入信号源阻抗较高;
  • 采样时间太短;
  • ADC 参考电压不稳定;
  • 模拟地和数字地干扰;
  • 没有做滤波处理;
  • 走线靠近高频信号或电机驱动。

解决思路:

  • 适当增加采样时间;
  • 多次采样求平均;
  • ADC 输入端加小电容滤波;
  • 保证 VDDA 稳定;
  • 模拟信号线尽量短;
  • 减少电机、继电器等干扰源影响。

16.4 电压换算不准确

常见原因:

  • 把 3.3V 当作固定值,但实际板子不是 3.3V;
  • 分压电阻有误差;
  • ADC 没有校准;
  • 输入信号源阻抗过大;
  • 采样时间过短;
  • 参考电压不稳定。

建议用万用表测量实际 VDDA,再代入公式:

voltage = adc_value * vdda_real / 4095.0f;

比如实测 VDDA 是 3.28V,就可以写成:

voltage = adc_value * 3.28f / 4095.0f;

17. ADC 发送和读取的技术总结

虽然 ADC 不是通信外设,但它的数据采集流程也可以理解成一次完整的数据读取过程。

核心流程如下:

模拟电压输入
↓
GPIO 模拟输入
↓
ADC 采样保持
↓
ADC 量化转换
↓
EOC 转换完成
↓
读取 ADC_DR 数据寄存器
↓
换算成实际电压

标准库中最关键的几个函数如下:

函数 作用
RCC_ADCCLKConfig() 配置 ADC 时钟
GPIO_Init() 配置 ADC 引脚为模拟输入
ADC_Init() 初始化 ADC 工作模式
ADC_RegularChannelConfig() 配置规则通道
ADC_Cmd() 使能 ADC
ADC_ResetCalibration() 复位校准
ADC_StartCalibration() 启动校准
ADC_SoftwareStartConvCmd() 软件启动转换
ADC_GetFlagStatus() 判断转换完成标志
ADC_GetConversionValue() 读取 ADC 转换结果

18. 小结

本文基于 STM32 标准外设库,实现了 ADC1 通道 0 的电压采集。

整个过程可以总结为:

PA0 配置为模拟输入
↓
ADC1 初始化
↓
配置 ADC_Channel_0
↓
ADC 校准
↓
软件触发转换
↓
等待 EOC 标志
↓
读取 ADC 值
↓
换算成电压

对于初学者来说,学习 ADC 可以按照下面的顺序:

单通道单次采样
↓
电压换算
↓
多次采样平均
↓
多通道扫描采样
↓
ADC + DMA 连续采样
↓
ADC + 定时器触发采样

只要掌握了 ADC 初始化、通道配置、EOC 标志判断和电压换算公式,就可以完成大多数基础模拟量采集项目。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐