STM32 ADC 基础应用:从初始化到模拟量采集与电压换算
ADC(模数转换器)是嵌入式系统中连接模拟信号与数字世界的核心桥梁。本文以 STM32 为例,从硬件配置、代码实现到实际应用,详细讲解 ADC 的基本使用方法,帮助初学者快速掌握 “模拟量采集→数字量转换→电压计算” 的完整流程。
一、ADC 应用场景与核心目标
在嵌入式开发中,ADC 常用于采集传感器输出的模拟信号(如温度、光照、压力等物理量通常转换为 0~3.3V 电压),并将其转换为数字量供 CPU 处理。本文以 “采集 PA0 引脚的 0~3.3V 电压” 为例,实现以下目标:
- 配置 STM32 ADC1,以 PA0 为输入通道;
- 完成单次 ADC 转换,获取数字量;
- 将数字量换算为实际电压值(精确到 mV 级)。
二、硬件电路与原理
- 硬件连接:模拟信号源(如电位器、传感器)输出端接 PA0 引脚,信号范围需限制在 0~3.3V(避免超出 ADC 参考电压范围);
- 参考电压:STM32 内置 ADC 默认使用 3.3V 作为参考电压(VREF+),因此输入电压需满足 0≤Vin≤3.3V;
- 核心原理:12 位 ADC 将 0~3.3V 电压分为 4096 个量化级(0~4095),数字量与电压的关系为:
实际电压(V)= 数字量 × 3.3V / 4096
三、代码实现与优化(分模块解析)
1. ADC 初始化函数(ADC1_Init)
初始化是 ADC 使用的基础,需完成时钟配置、GPIO 配置、ADC 模式配置及校准,以下是优化后的代码(增加注释可读性,补充关键参数说明):
void ADC1_Init(void)
{
ADC_InitTypeDef ADC_InitStructure;
GPIO_InitTypeDef GPIO_InitStructure;
// 1. 使能时钟(ADC1和GPIOA均挂载在APB2总线)
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_ADC1, ENABLE);
// 2. 配置ADC时钟(关键!需≤芯片最大限制,STM32F1系列不超过14MHz)
// APB2时钟为72MHz,分频8 → 72/8=9MHz(安全且常用)
RCC_ADCCLKConfig(RCC_PCLK2_Div8);
// 3. 配置PA0为模拟输入(禁用数字功能,避免干扰模拟信号)
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AIN; // 模拟输入模式(必须)
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_10MHz; // 模拟模式下速度无效,仅占位
GPIO_Init(GPIOA, &GPIO_InitStructure);
// 4. 配置ADC工作模式
ADC_InitStructure.ADC_Mode = ADC_Mode_Independent; // 独立模式(仅使用ADC1)
ADC_InitStructure.ADC_ScanConvMode = DISABLE; // 单通道模式(非扫描)
ADC_InitStructure.ADC_ContinuousConvMode = DISABLE; // 单次转换(转换一次后停止)
ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; // 软件触发
ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; // 数据右对齐(12位有效)
ADC_InitStructure.ADC_NbrOfChannel = 1; // 转换通道数:1
ADC_Init(ADC1, &ADC_InitStructure);
// 5. 使能ADC1
ADC_Cmd(ADC1, ENABLE);
// 6. ADC校准(必须执行,否则精度会偏差)
ADC_ResetCalibration(ADC1); // 复位校准寄存器
while(ADC_GetResetCalibrationStatus(ADC1)); // 等待复位完成
ADC_StartCalibration(ADC1); // 启动校准
while(ADC_GetCalibrationStatus(ADC1)); // 等待校准完成
}
2. 主函数中的初始化与转换流程
#include "stm32f10x.h"
#include "ADC.h"
#include "usart.h" // 假设你有串口初始化,用于打印结果(没有则可以注释printf)
// 定义全局变量:存储ADC转换结果和实际电压
unsigned short ADC1Value ; // 存储ADC原始数字量(12位,范围0~4095)
float voltage ; // 存储换算后的实际电压(单位:V)
int main(void)
{
// 1. 初始化外设:先初始化ADC1,再配置ADC转换通道
ADC1_Init() ; // 调用ADC.c里的初始化函数,配置PA0和ADC1
// 配置ADC1的转换通道:指定采集“通道0”(对应PA0)
// 参数1:ADC1(用哪个ADC); 参数2:ADC_Channel_0(PA0对应的通道号)
// 参数3:1(通道优先级,单通道不用管); 参数4:采样时间(71.5个时钟周期,采集更稳定)
ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_71Cycles5 );
// 2. 主循环:反复采集电压、换算、打印
while(1)
{
// 第一步:启动ADC转换(因为配置的是“软件触发”,必须手动启动)
ADC_SoftwareStartConvCmd(ADC1, ENABLE);
// 第二步:等待转换完成(查询“转换结束标志EOC”,没完成就一直等)
while(ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET);
// 第三步:读取转换结果(把ADC1的数字量存到ADC1Value里)
ADC1Value = ADC_GetConversionValue(ADC1) ;
// 第四步:换算成实际电压(用核心公式,3.3是参考电压,4096是12位最大数字量)
voltage = (float)ADC1Value * 3.3 / 4096 ;
// (可选)第五步:串口打印结果,方便查看(如果没有串口,这行可以注释)
printf("ADC数字量:%d → 实际电压:%.2fV\r\n", ADC1Value, voltage);
// (可选)延时500ms:不用采集太快,减少CPU占用(需要自己实现delay_ms函数)
delay_ms(500);
}
}
四、关键问题:帮你搞懂 “为什么要这么写”
1.为什么 PA0 要配置成 GPIO_Mode_AIN?
因为 ADC 采集的是 “模拟信号”,而 AIN(模拟输入)模式会完全禁用 PA0 的数字电路(如上拉 / 下拉电阻、数字输入缓冲)。若使用其他模式(如浮空输入、推挽输出),数字电路的电流或信号会干扰微弱的模拟电压,导致 ADC 采集结果偏差极大,只有 AIN 模式能保证模拟信号的纯净性。
2.为什么要做 ADC 校准?
ADC 硬件存在微小固有误差(如电路元件的温度漂移、电压偏移),未校准前转换误差可能达几十 mV。校准的核心是让 ADC 自动计算误差补偿值并存储到校准寄存器,后续每次转换时,硬件会用补偿值修正结果,最终将误差控制在 1mV 左右,确保采集精度符合需求。
3.为什么要等 ADC_FLAG_EOC 标志?
ADC 完成一次模拟→数字转换需要时间(如 9MHz 时钟下,71.5 个采样周期 + 转换逻辑耗时约 8μs),并非启动后立即出结果。ADC_FLAG_EOC 是 “转换结束标志位”:转换未完成时为 0,完成后硬件自动置 1。若不等该标志直接读结果,要么读到上一次转换的旧值,要么读到未更新的乱码,只有等 EOC=1 才能确保结果是当前转换的准确值。
4.电压换算为什么要加 (float)?
ADC1Value 是 unsigned short 类型(整数),3.3(参考电压)和 4096(12 位 ADC 总量化级)若按整数运算,会丢失小数部分(如 1000×3.3/4096 按整数算得 0,实际应为≈0.806V)。加 (float) 是将整数强制转换为浮点数,让运算保留小数位,最终得到与实际电压匹配的精确结果。
补充解析:ADC 中的 “EOC” 到底是什么?
在 ADC 转换流程中,ADC_FLAG_EOC是一个关键 “信号”,很多初学者会疑惑它的作用 —— 其实它就是 ADC 给 CPU 的 “完工通知”,专门用来告诉 CPU“模拟→数字的转换已经做完了,现在可以读结果了”。结合你的代码和硬件逻辑,具体拆解如下:
一、EOC 的全称与本质:“转换结束” 的标志位
- 全称:EOC = End Of Conversion(转换结束);
- 本质:它是 STM32 ADC 外设寄存器里的一个 “bit 位”(二进制位,只有 0 和 1 两种状态),是 ADC 硬件自动控制的 “状态标志”——
- 当 ADC 开始转换时,硬件会自动把 EOC 位设为
0(表示 “正在忙,没做完”); - 当 ADC 完成一次转换(把模拟电压变成数字量,并存到数据寄存器里),硬件会自动把 EOC 位设为
1(表示 “完工了,结果准备好了”)。
- 当 ADC 开始转换时,硬件会自动把 EOC 位设为
二、为什么必须等 EOC=1?—— 避免读 “无效结果”
你代码里的while(ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET);,本质是 “等 EOC 从 0 变成 1”,核心原因是ADC 转换需要时间,不是 “一启动就出结果”:
- 以你的配置为例:ADC 时钟是 9MHz(72MHz/8),采样时间是 71.5 个时钟周期,加上转换逻辑的时间,一次完整转换大约需要 80 个时钟周期,换算成时间就是
80 / 9MHz ≈ 8.9μs(不到 10 微秒); - 如果不等 EOC=1,直接去读
ADC_GetConversionValue(ADC1),可能出现两种问题:- 读的是 “上一次转换的旧值”:比如上一次转换结果是 1000,这次刚启动转换就去读,读的还是 1000,不是当前的新值;
- 读的是 “错误的乱码值”:转换还没做完,数据寄存器里的数字量还没更新,读出来的是随机的二进制数,没有意义。
简单说:等 EOC=1,就是等 ADC “把活干完”,确保读出来的数字量是 “当前这次转换的准确结果”。
三、EOC 的两种用法:查询(你代码的方式) vs 中断
在实际开发中,EOC 有两种常见用法,你的代码用的是 “查询法”,适合简单场景:
1. 查询法(你的代码逻辑)
- 流程:启动转换 → 循环判断 EOC 是否为 1 → 是则读结果;
- 优点:代码简单,容易理解,适合低频采集(比如每秒采集几次);
- 缺点:等待期间 CPU 不能干别的(比如循环判断时,CPU 一直在 “空等”),如果采集频率高,会浪费 CPU 资源。
典型查询法:
ADC_SoftwareStartConvCmd(ADC1, ENABLE); // 启动转换(EOC自动变0) while(ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC) == RESET); // 等EOC变1(转换完成) ADC1Value = ADC_GetConversionValue(ADC1); // 读准确结果
2. 中断法(进阶用法,适合高频采集)
- 原理:提前配置 “ADC 转换完成中断”,当 EOC 变 1 时,硬件会自动触发中断,CPU 暂停当前工作,去执行中断服务函数;
- 流程:配置中断 → 启动转换 → CPU 干别的活 → EOC 变 1 触发中断 → 中断服务函数里读结果;
- 优点:CPU 不用空等,能同时处理其他任务(比如采集的同时还能控制 LED、串口打印),适合高频采集(比如每秒采集上千次)。
如果用中断法,核心代码会多两步:
// 1. 使能ADC转换完成中断 ADC_ITConfig(ADC1, ADC_IT_EOC, ENABLE); // 2. 中断服务函数:EOC变1时自动执行 void ADC1_2_IRQHandler(void) { if(ADC_GetITStatus(ADC1, ADC_IT_EOC) == SET) // 判断是不是EOC中断 { ADC1Value = ADC_GetConversionValue(ADC1); // 读结果 ADC_ClearITPendingBit(ADC1, ADC_IT_EOC); // 清除中断标志(必须) } }
四、关键提醒:EOC 标志要 “及时处理”
不管用哪种方法,EOC 变 1 后,都要注意:
- 如果用查询法,读结果后不需要手动清 EOC(下次启动转换时,硬件会自动把 EOC 设为 0);
- 如果用中断法,必须在中断服务函数里用
ADC_ClearITPendingBit清除 EOC 的中断标志 —— 否则硬件会认为 “中断还没处理完”,一直触发中断,导致程序卡死。
总结
EOC 就是 ADC 的 “完工通知”,核心作用是确保我们读结果时 “转换已经完成”:
- 你的代码用查询法等 EOC=1,是最简单直接的方式,适合入门场景;
- 理解 EOC 的本质后,后续遇到高频采集需求,就能轻松切换到中断法,让代码更高效。
更多推荐

所有评论(0)