ADC 学习笔记:从原理到 STM32 实战,一篇讲透
本文是一篇系统的 ADC(模数转换器)学习笔记:前半部分讲透 ADC 的基本原理(采样/保持/量化/编码、各种架构对比),后半部分以 STM32 为例,从模块框图、转换流程一路讲到 CubeMX + HAL 库的四个实战(轮询 → DMA 多通道 → 定时器触发 → 内部传感器校准),最后附精度优化和避坑指南。
适合:单片机/嵌入式入门同学、准备笔试面试需要快速过一遍知识点的同学。
阅读路线
-
赶时间直接上手 → 看 第六章 实战;
-
面试/考试抱佛脚 → 看 第九章 速查表;
-
想真正理解 → 从头读,重点看第三、四章。

一、ADC 是什么:模拟世界与数字世界的桥梁
我们生活的物理世界本质上是模拟的:温度、光照、声音、压力、电池电压……这些量在时间和幅值上都是连续变化的。而单片机(CPU)只认识 0 和 1 组成的离散数字信号。
ADC(Analog-to-Digital Converter,模数转换器)就是这两者之间的桥梁:每隔一段时间取一次模拟电压的"快照",并把它换算成一个整数,交给 CPU 处理。

举几个日常例子感受一下 ADC 无处不在:
| 应用场景 | 被测模拟量 | 典型需求 |
|---|---|---|
| 数字万用表 | 电压、电流 | 高精度、低速 |
| 温控器 | NTC 热敏电阻分压 | 中低速、稳定 |
| 麦克风采集 | 音频信号 | 44.1kHz 等固定采样率 |
| 无人机 IMU | 陀螺仪/加速度模拟输出 | 高速、实时 |
| 电池电量显示 | 锂电池电压 3.0~4.2V | 低速、低功耗 |
💡 一句话总结:ADC 负责"量",CPU 负责"算"。 单片机片内集成的 ADC(如 STM32 的 12 位 ADC)能覆盖 80% 的中低速应用;要求更高精度或更快速度时,才需要外挂专用 ADC 芯片(如 ADS1115、AD7606)。
二、ADC 的四步曲:采样 → 保持 → 量化 → 编码
任何一本教材里,ADC 的转换过程都可以拆成四个阶段。理解这四步,后面所有概念都迎刃而解。
2.1 采样(Sampling)与奈奎斯特定理
采样,就是每隔固定时间 Ts(采样周期)读取一次信号的瞬时值,采样率 fs = 1/Ts。
这步有一个黄金定律——奈奎斯特采样定理:
fs ≥ 2 × fmax(采样率必须大于信号最高频率的 2 倍),否则会发生混叠(Aliasing):高频信号会被错误地"折叠"成一个不存在的低频信号。
图中可以看到:fs 不足时,采样点"看上去"属于另一路假信号,而且事后用任何软件手段都无法还原。所以工程上的做法是:ADC 前端加一个低通滤波器(抗混叠滤波),把高频成分先滤掉。
2.2 保持(Hold)
采样只是一瞬间的事,转换需要时间。采样保持电路(S/H)用一个电容把采样瞬间的电压"冻住"(保持为阶梯状),让 ADC 在这段平坦期内慢慢完成量化。STM32 内部就集成了 S/H 电路,我们能配置的"采样时间"长短,说的就是这段给采样电容充电的时间(第四章细讲)。
2.3 量化(Quantization)与分辨率
保持下来的电压值有无穷多种可能,但输出的数字是有限的——必须"四舍五入"到最近的台阶上,这就是量化。

这里引出 ADC 最重要的参数——分辨率 N(位数):把满量程电压 Vref 划分成 2^N 份:
1 LSB = Vref / 2^N
-
1 LSB 就是 ADC 能分辨的最小电压差;
-
例:12 位 ADC、参考电压 3.3V → 1 LSB = 3.3 / 4096 ≈ 0.81 mV;
-
任何时候量化误差都不超过 ±1/2 LSB(理想情况);
-
理论极限信噪比:SNR ≈ 6.02 × N + 1.76 (dB),12 位约 74 dB。每多 1 位分辨率,SNR 提升约 6 dB。
📌 拿到 ADC 读数怎么算回电压? 最常用的一个公式:
电压 Vin = ADC原始值 × Vref / 4095 (12 位、右对齐)
注意除以 4095 还是 4096 的争论:严格地说输出码 4095 对应"接近 Vref",工程上两种写法差异不到 0.03%,CSDN 上两种都能见到,统一即可,不必纠结。
2.4 编码(Encoding)
把量化后的台阶编号写成二进制输出,12 位 ADC 就输出 0 ~ 4095(0x000 ~ 0xFFF)。至此,一个模拟电压变成了一个 CPU 能读的数字。
三、常见 ADC 架构:原理与选型
ADC 的实现方案有很多种,本质上就是在"速度、精度、成本、功耗"之间做不同的取舍。重点理解 SAR 和 Σ-Δ 两种。
3.1 SAR 逐次逼近型(重点!MCU 内置 ADC 几乎都是它)
结构:输入 → 采样保持 → 比较器 ↔ SAR 逻辑 ↔ 内部 DAC → 输出码。

工作方式像用天平二分称量:从最高位(MSB)开始,每个时钟节拍试探一位:
-
先猜一半:输出码设为
100..0,DAC 给出 Vref/2; -
比较器判断 Vin 和 DAC 谁大:Vin 大 → 这一位留 1,否则清 0;
-
把已确定的位锁存,继续试探下一位;
-
N 位 ADC 需要 N 个节拍,逐位逼近出完整结果。
下面这张 4 位 ADC 的例子(Vref=5V,Vin=3.0V)把过程画得很清楚——最终输出 1001:

特点:中高速、低延迟、低功耗、成本低,分辨率和速度互相制约(位数越多节拍越多)。STM32、MSP430、AVR 等单片机内置的 ADC 基本都是 SAR。
3.2 Σ-Δ 型(高精度之王)
思路完全不同:先求和(Σ积分)再取差分(Δ),用远高于信号带宽的频率过采样,把量化噪声"推"到高频段,再用数字滤波器滤掉高频噪声并抽取数据。模拟部分极其简单,复杂的是数字滤波。
特点:分辨率轻松做到 24 位甚至 32 位、超高精度、抗混叠能力强;代价是速度慢(几 SPS ~ 几百 kSPS)、有滤波器延迟。典型应用:电子秤、温度计、心电图等精密慢速测量(如 ADS1115、HX711 都是这类外置 ADC)。
3.3 其他常见架构(了解即可)
| 架构 | 原理一句话 | 分辨率 | 速度 | 典型应用 |
|---|---|---|---|---|
| Flash 并行比较 | 2^N−1 个比较器同时比较,一次出结果 | 低(4~10位) | 最快(GSPS 级) | 示波器、高速采集 |
| 流水线 Pipeline | 多级 Flash 级联,每级算几位再拼起来 | 8~16 位 | 很快(MSPS~GSPS) | 通信、视频采集卡 |
| SAR 逐次逼近 | 二分搜索,一拍一位 | 8~18 位 | 中高速 | MCU 内置、数据采集 |
| Σ-Δ | 过采样 + 噪声整形 + 数字滤波 | 最高(16~32位) | 慢 | 精密测量、音频编解码 |
| 双积分 | 先对信号定时积分,再对基准反向积分计时 | 16~24 位 | 最慢(几 SPS) | 数字万用表 |

选型思路:要快 → Flash/流水线;要高精度慢信号 → Σ-Δ;速度精度均衡 + 集成在单片机里白用 → SAR。本文后面的实战全部围绕单片机内置的 SAR ADC。
四、STM32 ADC 深度解析:架构与转换流程
铺垫结束,进入实战部分的主角——STM32 片上 ADC。以最经典的 STM32F103(12 位 SAR ADC)为例讲解,F4/G4/H7 等系列原理完全相通,只是时钟上限、分辨率位数等参数不同。
4.1 特性速览
-
12 位逐次逼近型 ADC,读数范围 0 ~ 4095;
-
最多 18 个通道:16 个外部通道 + 内部温度传感器 + Vrefint 内部基准;
-
ADCCLK 最高 14MHz,最快转换 1µs(1 Msps);
-
支持规则组(16 通道序列)和注入组(4 通道,可插队优先转换);
-
单次/连续/扫描/间断多种转换模式,软件/定时器/外部引脚多种触发;
-
结果可左/右对齐存入 16 位寄存器;支持 DMA、多个中断和模拟看门狗。
4.2 总体框图:一张图看懂数据走向

跟着这张图从左到右走一遍:模拟信号从 GPIO(需配置为模拟输入模式)进来 → 多路开关选定本次转换的通道 → 按规则组/注入组的队列安排进入转换核心 → SAR 在 ADCCLK 驱动下逐位逼近 → 结果写入 DR / JDR 数据寄存器 → 触发 EOC 中断或 DMA 请求把数搬走。期间触发源决定"什么时候开始转",模拟看门狗则充当电压越界的"报警器"。
4.3 时钟(ADCCLK):越快不一定越好
ADCCLK 由 APB2 总线时钟(PCLK2,F103 最大 72MHz)分频得到,分频系数只能选 /2 /4 /6 /8,并且有个硬性上限:
⚠️ F1 系列 ADCCLK ≤ 14MHz。72MHz 主频下常用 72/6=12MHz;如果系统时钟比较低(如 48MHz),直接选 /4=12MHz 或更低即可。其他系列上限不同,如 F4 可到 36MHz,G4/H7 更高,原则一样:按手册不超过上限。
很多人问"为什么不直接拉到 14MHz?"——ADCCLK 越快转换越快,但采样电容需要足够时间充满,信号源阻抗大时盲目拉高时钟反而降低精度。所以时钟和采样时间要配合着调(4.7 节讲计算)。
4.4 输入通道
-
外部通道 ADC_IN0 ~ IN15:连接到具体 GPIO,CubeMX 里点选引脚的
ADCx_INy复用功能即可,该引脚自动设为模拟模式(无上下拉,直达 ADC); -
内部温度传感器:占用 ADC1 的通道 16(IN16),要测芯片/环境温度不用外围器件;
-
Vrefint 内部基准:通道 17(IN17),出厂电压约 1.20V,常用来反推 VDDA 实际值以校准读数(实战四详解)。
4.5 规则组 vs 注入组:普通队列与 VIP 通道
-
规则组(Regular):日常使用的主力,最多 16 个通道排成一条转换序列,按 Rank1→Rank16 顺序依次转换;
-
注入组(Injected):最多 4 个通道的"高优先级小队",可以打断正在进行的规则组转换,转完再把规则组"续上"——典型用法是电机控制里,PWM 波形特定点触发电流采样这类"一刻也不能晚"的任务。
入门阶段只用规则组即可,知道注入组是"插队"的就够了。
4.6 四种转换模式:搞清楚这节很重要
模式由 连续开关(CONT) 和 扫描开关(SCAN) 组合出来的:

| 组合 | 行为 | 典型场景 |
|---|---|---|
| 单次转换 | 触发一次,转一个通道一次然后停 | 按键采样、偶尔读一次电量 |
| 连续转换 | 对同一通道不停重复转换 | 实时监控单一模拟量 |
| 扫描模式 | 一次触发,把规则组所有通道按序扫一遍 | 一次采集多路传感器(必须配 DMA) |
| 扫描 + 连续 | 多通道一轮接一轮循环扫 | 最常用的多路采集方式 |
⚠️ 一个经典坑:扫描模式下每个通道转换完都想往同一个 DR 寄存器里写,后面的数据会覆盖前面的,用轮询只能读到最后一个通道的值。所以多通道扫描几乎一定要配 DMA,每个通道 EOC 时 DMA 立刻把数据搬到数组对应位置。
4.7 转换时间怎么算(面试高频)
STM32(F1)的一次转换时间:
Tconv = 采样时间 + 12.5 个 ADCCLK 周期
-
"采样时间"可以在 1.5 / 7.5 / 13.5 / 28.5 / 41.5 / 55.5 / 71.5 / 239.5 周期中选;
-
"12.5"是 12 位逐位比较 + 收尾固定的开销。

算例 1:ADCCLK=12MHz(72/6),采样时间 55.5 周期 → Tconv = (55.5+12.5)/12MHz ≈ 5.67µs,即该通道每秒最多转约 176k 次。
算例 2:采样时间选最短的 1.5 周期、ADCCLK 拉满 14MHz → Tconv = 14/14MHz = 1µs = 1 Msps,这就是"STM32 ADC 最高 1M 采样率"的出处。
📌 采样时间的选择原则:信号源阻抗越高,采样时间要越长,因为内部采样电容要在这段时间里被充到足够接近真实电压。电位器/运放输出等低阻源可以用短采样时间;分压电阻大、NTC 这类高阻源要拉长采样时间(实在不行在引脚加运放缓冲)。
4.8 数据对齐
12 位结果存放在 16 位的 DR 寄存器里:
-
右对齐(强烈推荐):高 4 位补 0,读出来直接就是 0~4095;
-
左对齐:把结果顶到高位,取高 8 位 ≈ 直接得到 8 位精度——极少数想省事降精度的场景用。
4.9 中断、DMA 与模拟看门狗
-
EOC 中断:规则组一次转换完成;JEOC:注入组完成;
-
DMA 请求:每通道 EOC 时都可产生,多通道/连续采集的标配;
-
模拟看门狗(AWD):设定上/下两个阈值电压,转换结果越界立刻触发中断——"电池过压报警""温度失控报警"这类需求不用 CPU 频繁比较,硬件自动盯梢。
五、三种数据读取方式对比
| 轮询(阻塞) | 中断 | DMA | |
|---|---|---|---|
| CPU 占用 | 高(干等) | 中(转完打断一次) | 几乎为 0 |
| 代码难度 | ★ | ★★ | ★★ |
| 适用场景 | 偶尔读一次、demo 验证 | 单通道、需要及时处理每个值 | 多通道、连续/高速采集 |
| 关键 API | HAL_ADC_PollForConversion |
HAL_ADC_Start_IT + 回调 |
HAL_ADC_Start_DMA |
经验法则:教学最先学轮询,项目里差不多都用 DMA。中断方式介于两者之间,但要注意转换很快时(比如 1Msps)中断都来不及处理。
六、上手实战(STM32CubeMX + HAL 库)
硬件准备:任意一块 STM32F103 开发板(如最白菜的 BluePill)、一个电位器(中间脚接 PA0,两边接 3.3V 和 GND)、USB 转串口模块。
6.0 通用配置流程
不论用哪种方式,CubeMX 里的配置流程都是这七步:

这里把 ADC 参数配置界面里每个选项的意思列成表(CSDN 上被问得最多的就是这些):
| 配置项 | 推荐值 | 说明 |
|---|---|---|
| Resolution(分辨率) | 12 位(F1 固定) | 部分系列可选 6/8/10/12 位 |
| Data Alignment | Right 右对齐 | 读数直接为 0~4095 |
| Scan Conversion Mode | 多通道时 Enabled | 单通道 Disable |
| Continuous Conversion Mode | 按需 | 连续自动转换开关 |
| Discontinuous Conversion Mode | 一般 Disabled | 间断分组模式,入门先不用 |
| DMA Continuous Requests | 用 DMA 时 Enabled | 扫描结束后 DMA 继续请求 |
| End Of Conversion Selection | 默认 | 每个通道转完置 EOC 标志 |
| Number Of Conversion | = 通道数 | 规则组序列长度 |
| Rank | 1,2,3… | 序列中每个通道的顺序和采样时间 |
| External Trigger Conversion | 软件/定时器/EXTI | 触发源,实战三用定时器 |
| Sampling Time | 1.5 ~ 239.5 cycles | 阻抗高则拉长(见 4.7) |
另外两件事每次都要做:
-
时钟树里确认 ADCCLK:F1 不要超过 14MHz;72MHz 主频常用 /6 = 12MHz;
-
代码里(F1 系列)上电后先做一次自校准
HAL_ADCEx_Calibration_Start(&hadc1),能明显减小偏移误差。
6.1 实战一:单通道轮询读取电位器电压
目标:转电位器,每 500ms 把电压值通过串口打印出来。
CubeMX 配置:PA0 → ADC1_IN0;扫描关、连续关、触发源选软件触发;采样时间 55.5 周期;开 USART1(PA9/PA10)。
用户代码(写在 USER CODE 区):
uint32_t adc_raw;
float voltage;
char buf[64];
/* USER CODE BEGIN 2 */
hadc1.Init.NbrOfConversion = 1;
HAL_ADCEx_Calibration_Start(&hadc1); // F1系列:上电先自校准!
/* USER CODE END 2 */
while (1)
{
HAL_ADC_Start(&hadc1); // ① 软件触发一次转换
if (HAL_ADC_PollForConversion(&hadc1, 10) == HAL_OK) // ② 阻塞等待转换完(超时10ms)
{
adc_raw = HAL_ADC_GetValue(&hadc1); // ③ 读原始值 0~4095
voltage = adc_raw * 3.3f / 4095.0f; // ④ 换算成实际电压
sprintf(buf, "raw=%lu, voltage=%.3f V\r\n", adc_raw, voltage);
HAL_UART_Transmit(&huart1, (uint8_t *)buf, strlen(buf), 100);
}
HAL_ADC_Stop(&hadc1);
HAL_Delay(500);
}
要点解析:
-
"启动 → 等待 EOC → 读 DR" 就是轮询三板斧,期间 CPU 在
PollForConversion里干等,所以只适合低频、偶尔读取; -
HAL_ADC_GetValue只是读寄存器,必须先确认转换完成(PollForConversion 返回 OK)再读,否则拿到的是旧值或 0; -
若读数全是 0:检查 ADC 时钟使能、引脚是否为 Analog 模式、是否忘记
HAL_ADC_Start。
6.2 实战二:DMA 多通道采集(强烈推荐)
目标:同时采集电位器(PA0)、光敏电阻(PA1)、温度探头(PA2)三路,CPU 不参与搬运。

CubeMX 配置变化(相对实战一):
-
ADC1 勾选 IN0、IN1、IN2 三个通道;
-
Scan Conversion Mode → Enabled,Continuous → Enabled;
-
Number Of Conversion = 3,Rank1~3 分别绑定通道 0/1/2;
-
左侧 DMA Settings → Add,方向 ADC1→Memory,模式选 Circular(循环),数据宽度 Half Word;
-
注意生成工程顺序:
MX_DMA_Init()必须在MX_ADC1_Init()之前(CubeMX 会自动排好)。
用户代码:
uint16_t adc_buf[3]; // [0]=电位器 [1]=光敏 [2]=温度,与Rank顺序一一对应
/* USER CODE BEGIN 2 */
HAL_ADCEx_Calibration_Start(&hadc1);
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buf, 3); // 启动后就不用管了
/* USER CODE END 2 */
while (1)
{
// 随时读 adc_buf,DMA 已在后台默默刷新
float v0 = adc_buf[0] * 3.3f / 4095.0f;
float v1 = adc_buf[1] * 3.3f / 4095.0f;
float v2 = adc_buf[2] * 3.3f / 4095.0f;
char buf[80];
sprintf(buf, "CH0=%.2fV CH1=%.2fV CH2=%.2fV\r\n", v0, v1, v2);
HAL_UART_Transmit(&huart1, (uint8_t *)buf, strlen(buf), 100);
HAL_Delay(500); // 打印快慢你自己定,采样始终在自动进行
}
要点解析:
-
DMA 循环模式:ADC 每转完一个通道,DMA 立即把 DR 里的值搬到
adc_buf对应下标,一轮结束后自动从头再来,CPU 零参与; -
adc_buf与 Rank 序列的一一对应关系不能乱,数据"张冠李戴"都是这里排错序; -
常见误区:
HAL_ADC_GetValue在 DMA 模式下不要用——值早被 DMA 搬走了,直接读数组就行。
6.3 实战三:定时器触发 + DMA,实现"精确采样率"
轮询 + HAL_Delay 的采样间隔误差很大(代码执行时间、中断抖动都会影响),做音频、震动分析、波形显示时不可接受。正确做法:用定时器的更新事件(TRGO)精准触发 ADC,采样率完全由硬件决定。

CubeMX 配置:
-
TIM3:Clock Source = Internal Clock;Trigger Event Selection = Update Event;PSC=71、ARR=99(72MHz/72/100 = 10kHz);
-
ADC1:External Trigger Conversion Source 选 Timer 3 Capture Compare/Trigger Out;触发沿 Rising Edge;Continuous Disabled(节奏已由定时器掌控);DMA 保持 Circular。
用户代码(并采用"乒乓双半缓冲",采 200 个点一组处理):
#define BUF_LEN 200
uint16_t buf[BUF_LEN];
/* USER CODE BEGIN 2 */
HAL_ADCEx_Calibration_Start(&hadc1);
HAL_TIM_Base_Start(&htim3); // ① 先开定时器
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)buf, BUF_LEN); // ② ADC外部触发
/* USER CODE END 2 */
/* 前半缓冲(buf[0:100))传满:趁后台采集后半,处理这 100 个点 */
void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc)
{
// 取出 buf[0..99] 做滤波 / FFT / 串口发送
}
/* 全部传满:处理 buf[100:200) */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
// 取出 buf[100..199] 做滤波 / FFT / 串口发送
}
说明:两个回调函数让"采集"与"处理"交替进行——半满时处理前 100 点的同时,DMA 继续往后 100 点写,全满时反过来,循环无缝。这是示波器、音频采集类项目的标准写法,受限于篇幅这里只给框架,核心 API 都在这了。
要点解析:
-
采样率公式:fs = f_TIM / ((PSC+1)×(ARR+1)),改 ARR 即可改采样率;
-
定时器触发时必须关掉 Continuous,否则 ADC 会脱离触发自己狂转;
-
定时器 TRGO 触发源的可选范围与具体型号有关,在 CubeMX 的 External Trigger 下拉框里挑。
6.4 实战四:内部温度传感器 + Vrefint 校准
两个内部通道(IN16/IN17)非常实用:一个免费测温,一个能校准 VDDA(当你的 3.3V 供电并不准的时候——比如 USB 口供电常常只有 3.2V,ADC 读数就会整体偏移)。
CubeMX:勾选 ADC1 的 Temperature Sensor Channel 和 Vrefint Channel(会自动占用 IN16/IN17)。注意温度传感器需要的采样时间较长,建议采样时间选最长的 239.5 周期(F1 手册要求 T_S_temp ≥ 17.1µs)。
uint16_t adc_buf[2]; // [0]=温度传感器 [1]=Vrefint
HAL_ADCEx_Calibration_Start(&hadc1);
HAL_ADC_Start_DMA(&hadc1, (uint32_t *)adc_buf, 2);
while (1)
{
/* 第一步:用 Vrefint(约1.20V) 反推 VDDA 真实值 */
float vdda = 1.20f * 4095.0f / adc_buf[1]; // 电源不是精确3.3V时很关键
/* 第二步:换算温度电压并代入F103手册公式 */
float vsense = adc_buf[0] * vdda / 4095.0f;
float temp = (1.43f - vsense) / 0.0043f + 25.0f; // V25=1.43V, 斜率4.3mV/℃
char buf[64];
sprintf(buf, "VDDA=%.3fV, chip temp=%.1f C\r\n", vdda, temp);
HAL_UART_Transmit(&huart1, (uint8_t *)buf, strlen(buf), 100);
HAL_Delay(1000);
}
要点解析:
-
F103 温度传感器参数:V25(25℃ 时电压)≈ 1.43V,平均斜率 ≈ 4.3mV/℃——不同系列数值不同,以你手头芯片的数据手册为准(例如 F4 是 0.76V / 2.5mV/℃);
-
内部温度传感器测的是芯片结温,受芯片自身发热影响,只能当"芯片温度参考",测环境温度还是要外接传感器;
-
部分系列(F0/F4/L4 等)出厂在芯片里存了 Vrefint 校准值(
VREFINT_CAL),校准公式与本文不同,思路一致:由已知基准反推 VDDA。
七、精度优化与避坑指南
同一个 ADC,别人能测稳到 0.01V,你的读数却乱跳 ±50 个码?问题多半在下面这张表里:
7.1 硬件层面
| 措施 | 说明 |
|---|---|
| 稳定干净的 VDDA/Vref+ | ADC 的天花板是参考电压。Vref+ 上加磁珠+去耦电容(100nF + 1µF),有独立 LDO 更好 |
| 模拟输入加 RC 滤波 | 引脚串 100Ω~1kΩ 电阻 + 对地 100nF 电容,即一阶低通兼抗混叠 |
| 模拟地/数字地单点相连 | 大电流回路、开关电源远离模拟走线 |
| 注意信号源内阻 | 内阻大 → 拉长采样时间或加运放跟随(见 4.7) |
| 输入保护 | 别让输入超 Vref+ 或为负压,需要时加钳位二极管 |
7.2 软件层面
-
多次平均(过采样):采 N 次求平均,噪声下降 √N 倍。例:连采 16 次取平均,简单有效;
-
中值滤波:剔除偶发尖刺(脉冲干扰场景首选);
-
滑动平均/一阶 IIR:
y = y*0.9 + x*0.1,慢变信号平滑好用; -
软件校准:F1 系列开机
HAL_ADCEx_Calibration_Start;用 Vrefint 修正 VDDA(实战四)。 -
丢掉头尾:切换通道后第一次转换可能受前一通道电压残留影响,多通道快速扫描时可舍弃每组首个样本。
/* 最常用的均值滤波示例:32次采样取平均 */
uint32_t ADC_ReadAvg(ADC_HandleTypeDef *hadc, uint8_t times)
{
uint32_t sum = 0;
for (uint8_t i = 0; i < times; i++)
{
HAL_ADC_Start(hadc);
HAL_ADC_PollForConversion(hadc, 10);
sum += HAL_ADC_GetValue(hadc);
}
HAL_ADC_Stop(hadc);
return sum / times;
}
八、常见问题 FAQ
Q1:读数总是 0? A:逐步检查:ADC 时钟使能了吗?GPIO 是 Analog 模式吗?忘写 HAL_ADC_Start / HAL_ADCEx_Calibration_Start 了吗?DMA 模式下却在用 HAL_ADC_GetValue?
Q2:读数上下跳动几十个码? A:正常现象(哪怕理想的 12 位 ADC 也有 ±1 LSB 起步的噪声)。依次试:加大采样时间 → 输入端加 100nF 电容 → 软件平均 → 检查 Vref 是否干净。
Q3:多通道数据总是串/错位? A:99% 是没开 DMA,或 Rank 顺序与 adc_buf 下标对应错了;也可能是采样时间太短,上一通道的电荷还没泄放(拉长采样时间试试)。
Q4:DMA 模式下 adc_buf 一直不更新? A:检查 DMA 是否是 Circular 模式、MX_DMA_Init() 是否在 MX_ADC1_Init() 之前、NDTR 长度与数组长度是否一致。
Q5:内部温度测得比室温高、还飘? A:正常。它测的是芯片结温,芯片一上电就自热,且个体差异大(±10℃ 量级),只适合做相对变化参考和过温保护。
Q6:非要更快速度(>1Msps)或更高精度(>12位)怎么办? A:F1 可以用"双 ADC 交叉模式"等玩法提倍采样率;或者上外置 ADC 芯片:想要精度选 Σ-Δ 型(如 ADS1115,16 位),想要更高速度选专用 SAR 或并行 ADC(如 AD9226 等),通过 SPI / I2C / 并口接入。
九、知识点速查表(背完敢去面试)
-
ADC 四步:采样 → 保持 → 量化 → 编码;
-
奈奎斯特:fs ≥ 2×fmax,不足则混叠,混叠不可恢复;
-
1 LSB = Vref / 2^N;量化误差 ≤ ±1/2 LSB;理想 SNR ≈ 6.02N + 1.76 dB;
-
电压换算:Vin = ADC值 × Vref / 4095(12 位右对齐);
-
SAR = 二分/逐位逼近,一拍一位;MCU 内置 ADC 的主流架构;Σ-Δ = 过采样+噪声整形,精度最高;
-
STM32F1 ADC:12 位、18 通道(16 外 + 温度 IN16 + Vrefint IN17)、ADCCLK ≤ 14MHz(PCLK2 分频 /2/4/6/8);
-
Tconv = 采样时间 + 12.5 周期,最快 1.5+12.5=14 周期,14MHz 时 1µs(1Msps);
-
规则组 16 通道排队转换;注入组 4 通道可"插队"打断;
-
模式:单次 / 连续 / 扫描 / 扫描+连续;扫描必须配 DMA(DR 会被覆盖);
-
三种读数方式:轮询(简单)→ 中断(及时)→ DMA(常用,零 CPU 开销);
-
F1 上电要自校准:
HAL_ADCEx_Calibration_Start; -
精确采样率:定时器 TRGO 触发,fs = fTIM/((PSC+1)(ARR+1)),关连续模式;
-
提精度三板斧:稳 Vref、加滤波/拉长采样时间、软件平均;修正 VDDA 用 Vrefint。
十、进阶阅读方向
-
双 ADC 交叉模式(双倍采样率)、注入组在电机控制(FOC 电流环)中的应用;
-
STM32G4/H7 的 16 位 ADC、硬件过采样单元(无需 CPU 直接提高有效位数);
-
外置 ADC 芯片:ADS1115(I2C 16 位 Σ-Δ)、AD7606(8 通道 16 位同步采样)、HX711(称重专用);
-
DMA 双缓冲 + FFT(CMSIS-DSP 库)做频谱分析;
-
RTOS 任务中的 ADC 采集架构设计(信号量 + 队列传递数据)。
参考资料
-
ST《STM32F10x 参考手册 RM0008》第 11 章:Analog-to-digital converter (ADC)
-
ST 应用笔记 AN2834《How to get the best ADC accuracy in STM32》
-
【CSDN】使用HAL库开发STM32:ADC基础使用:使用HAL库开发STM32:ADC基础使用_stm32 hal adc-CSDN博客
-
【CSDN】SD ADC(Σ-Δ)与 SAR ADC 核心区别:SD ADC(Σ-Δ ADC)与 SAR ADC(逐次逼近型 ADC)核心区别_sdadc-CSDN博客
-
【CSDN】STM32 ADC原理详解及应用编程:【STM32】单片机ADC原理详解及应用编程_stm32adc工作原理-CSDN博客
-
【知乎】什么是 SAR ADC?:https://www.zhihu.com/question/471760740
-
onlycalm 单片机ADC采样笔记:https://www.onlycalm.cn/docs/
📝 写这篇文章的目的是帮助自己理解 ADC"从模拟到数字"的全流程,并结合 STM32 实战把知识落地。文中如有疏漏,欢迎评论区指正,共同进步!
更多推荐




所有评论(0)