单片机 ADC 电压采集实战:从配置到数据读取
本文介绍了STM32F103单片机ADC电压采集的完整流程。主要内容包括:ADC基本原理与12位分辨率特性(0-4095对应0-3.3V)、硬件连接方式(以电位器为例)、标准库配置步骤(时钟分频、GPIO模拟输入、ADC参数设置等)、采样时间选择建议(推荐55.5周期)以及数据读取方法。重点讲解了电压换算公式(实际电压=采样值×3.3/4095)和初始化代码实现,适用于初学者掌握单通道ADC采集技
单片机 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,一般需要完成以下步骤:
- 开启 GPIOA 和 ADC1 时钟;
- 配置 PA0 为模拟输入;
- 配置 ADC 时钟分频;
- 配置 ADC 工作模式;
- 配置规则通道;
- 使能 ADC;
- 复位 ADC 校准;
- 等待校准完成;
- 启动软件转换;
- 等待转换完成;
- 读取 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 标志判断和电压换算公式,就可以完成大多数基础模拟量采集项目。
更多推荐



所有评论(0)