ADC采集结果为什么不稳定?硬件与软件原因及STM32排查方法详解

前言

在使用 STM32、GD32、ESP32 或其他 MCU 采集模拟电压时,经常会遇到下面这些问题:

输入电压明明没有变化,ADC数值却一直跳动;
万用表测量是1.65V,ADC换算后却只有1.58V;
同一个通道偶尔出现很大的异常值;
切换ADC通道后,第一次采样结果明显不正确;
电机、风扇或继电器启动后,ADC结果突然剧烈波动;
使用DMA采集后,数据比轮询采集更加混乱;
增加平均滤波后数值稳定了,但响应速度明显变慢。

很多人遇到 ADC 不稳定时,第一反应是增加平均滤波:

adc_value = (value1 + value2 + ... + value100) / 100;

平均滤波确实能减小部分随机噪声,但如果问题来自参考电压、输入阻抗、采样时间、PCB布局或者通道配置,单纯增加平均次数只能掩盖问题,无法真正解决问题。

ADC 采集问题需要从以下几个层面分析:

模拟信号源
   ↓
输入调理电路
   ↓
ADC引脚和参考电压
   ↓
采样保持电路
   ↓
ADC配置
   ↓
软件读取
   ↓
数字滤波
   ↓
电压换算

本文将从 ADC 基本原理开始,详细分析 ADC 采集结果不稳定的硬件原因、软件原因、滤波方法和完整排查流程。


一、ADC是什么

ADC 的英文全称是:

Analog-to-Digital Converter

中文称为:

模数转换器

它的作用是将连续变化的模拟电压转换为 MCU 可以处理的数字量。

例如,一个 12 位 ADC 使用 3.3V 参考电压:

输入0V       → ADC结果接近0
输入1.65V    → ADC结果接近2048
输入3.3V     → ADC结果接近4095

对于 N 位 ADC,一共可以表示:

2^N

个离散等级。

12 位 ADC 可以表示:

2^12 = 4096

个等级,输出代码通常为:

0~4095

理想情况下,一个最低有效位所对应的电压约为:

1 LSB = VREF / 2^N

当参考电压为 3.3V、分辨率为 12 位时:

1 LSB = 3.3V / 4096
      ≈ 0.0008057V
      ≈ 0.806mV

这意味着输入电压变化不到约 0.806mV 时,数字结果不一定发生变化。

反过来,即使输入完全稳定,ADC 结果在相邻的几个代码之间轻微变化,也不一定代表系统存在严重故障。


二、ADC“稳定”和“准确”不是一回事

这是分析 ADC 问题前必须区分的两个概念。

1. 稳定性

稳定性表示连续采样结果的波动程度。

例如:

2046
2048
2047
2049
2047
2048

这些结果平均值比较固定,波动范围也比较小,可以认为稳定性较好。


2. 准确性

准确性表示 ADC 测量结果与真实输入值之间的差距。

例如,真实输入为:

1.650V

ADC连续输出:

1.600V
1.601V
1.600V
1.601V

结果非常稳定,但存在约 50mV 的系统误差。

因此:

稳定但不准确:
通常与参考电压、增益误差、偏置误差和换算公式有关。

准确但波动:
通常与随机噪声、量化误差、采样干扰和滤波不足有关。

既不稳定也不准确:
需要同时检查硬件和软件配置。

STM32 的 ADC 精度不仅受到量化误差影响,还可能受到偏置误差、增益误差、线性误差、参考电压以及模拟输入电路等因素影响。ST 的 ADC 精度应用笔记也将这些误差和外部设计条件列为主要优化对象。


三、ADC内部是如何采样的

很多 ADC 不稳定问题,根本原因是没有理解 ADC 引脚内部并不是一个理想的无限大阻抗电压表。

典型 SAR ADC 输入端可以简化为:

模拟信号源
    │
    ├── 信号源电阻 RSOURCE
    │
    ▼
ADC输入开关 RSW
    │
    ▼
采样保持电容 CSH

ADC采样过程分为两个阶段:

采样阶段:
开关闭合,外部信号给内部采样电容充电。

保持和转换阶段:
开关断开,ADC根据采样电容上的电压进行数字转换。

如果信号源输出阻抗过大,或者采样时间太短,内部采样电容还没有充到接近真实输入电压,ADC 就结束了采样。

最终结果可能表现为:

  • ADC结果偏低;
  • 连续采样值不一致;
  • 通道切换后第一笔数据错误;
  • 输入电压变化后ADC响应缓慢;
  • 采样频率越高,误差越大。

SAR ADC 的采样保持电容需要在有限采集时间内充电并完成稳定,因此高输入阻抗通常需要更长采样时间、缓冲器或适当的输入电容。


四、ADC结果轻微跳动一定是故障吗

不一定。

例如12位ADC连续读取一个稳定电压:

ADC = 2047
ADC = 2048
ADC = 2047
ADC = 2049
ADC = 2048

这种相邻几个 LSB 的轻微跳动可能来自:

  • ADC量化误差;
  • MCU内部数字电路噪声;
  • 电源微小波动;
  • 热噪声;
  • 参考电压噪声;
  • 时钟耦合;
  • 环境电磁干扰。

理想 ADC 本身也存在量化过程,通常可以将理想量化误差理解为约 ±0.5 LSB。

真正需要重点排查的是:

波动达到几十甚至几百个ADC码;
数值周期性大幅变化;
切换通道后结果明显串扰;
电机启动后采样值突然异常;
静态输入下采样结果缓慢漂移;
ADC结果经常突然变成0或满量程。

五、ADC结果不稳定的硬件原因

1. 参考电压不稳定

ADC测量的本质不是直接测量绝对电压,而是测量:

输入电压与参考电压之间的比例

理想情况下:

ADC_CODE ≈ VIN / VREF × (2^N - 1)

因此,即使 VIN 完全不变,只要 VREF 发生变化,ADC结果也会变化。

例如输入电压固定为1.65V。

当参考电压为3.30V时:

ADC ≈ 1.65 / 3.30 × 4095
    ≈ 2048

当参考电压下降到3.20V时:

ADC ≈ 1.65 / 3.20 × 4095
    ≈ 2112

输入电压没有变化,但ADC结果已经明显改变。

常见问题

  • VDDA直接使用噪声较大的数字3.3V;
  • VREF引脚附近没有去耦电容;
  • 电机、继电器或风扇与ADC共用电源;
  • LDO输出纹波较大;
  • 大电流器件启停引起电压跌落;
  • 模拟地和数字地回流路径不合理;
  • VREF通过过长走线连接;
  • 参考源驱动能力不足。

改进方法

  • 按照芯片数据手册配置 VDDA、VREF+ 去耦;
  • 将去耦电容尽量靠近芯片引脚;
  • 将模拟供电与强干扰负载适当隔离;
  • 优化地平面和电流回流路径;
  • 必要时使用更稳定的外部基准源;
  • 通过 MCU 内部 VREFINT 估算实际 VDDA。

需要注意:

ADC换算时不能永远假设参考电压精确等于3.300V。

更可靠的方法是测量实际参考电压,或者利用芯片内部校准过的参考通道进行补偿。


2. ADC输入引脚悬空

如果 ADC 引脚没有连接到确定电压,输入端就处于悬空状态。

悬空引脚很容易耦合:

  • 工频干扰;
  • 人体静电;
  • 相邻数字信号;
  • 时钟信号;
  • 空间电磁噪声。

此时ADC结果可能从0到满量程之间大幅跳动。

验证方法

将ADC引脚通过一个较小电阻连接到地:

ADC输入 → 1kΩ~10kΩ → GND

如果结果立即稳定在接近0,说明ADC本身可能正常,之前的问题是输入悬空或者信号源连接不可靠。

也可以将ADC输入连接到稳定基准电压进行测试。


3. 信号源输出阻抗过大

这是 MCU 内部 ADC 最常见、也最容易忽略的问题之一。

例如使用两个大电阻分压:

24V
 │
 R1 = 1MΩ
 │──── ADC
 R2 = 100kΩ
 │
GND

虽然分压比例可能正确,但等效输出阻抗非常大。

ADC内部采样电容在较短采样时间内无法充满,就会导致转换值偏差或者波动。

ST 的 ADC 精度说明明确指出,高信号源阻抗会影响内部采样保持电容的充电过程,增加采样时间可以减小由此产生的附加误差。

改进方法一:降低分压电阻

例如将:

1MΩ和100kΩ

适当降低为:

100kΩ和10kΩ

但需要同时考虑:

  • 分压器功耗;
  • 被测信号源负载;
  • 输入保护电流;
  • 电阻精度。

改进方法二:增加ADC采样时间

例如将:

ADC_SAMPLETIME_2CYCLES_5

改为:

ADC_SAMPLETIME_47CYCLES_5

或者更长采样时间。

不同STM32系列支持的采样周期选项不同,应根据对应数据手册和参考手册选择。


改进方法三:增加运算放大器缓冲

可以使用电压跟随器:

高阻抗信号
    ↓
运算放大器电压跟随器
    ↓
低输出阻抗
    ↓
ADC输入

需要选择适合ADC驱动的运算放大器,并考虑:

  • 输入共模范围;
  • 输出摆幅;
  • 稳定性;
  • 带宽;
  • 建立时间;
  • 失调电压;
  • 供电范围。

改进方法四:在ADC输入端增加电容

可以在ADC引脚附近增加一个小电容:

信号源 ── R ── ADC
               │
               C
               │
              GND

电容可以:

  • 为内部采样电容提供局部电荷;
  • 滤除高频噪声;
  • 减小采样瞬间的电压跌落。

但电容不是越大越好。

它与前级电阻构成低通滤波器:

FC = 1 / (2πRC)

如果电容过大,会导致:

  • 信号响应变慢;
  • 快速变化信号被严重衰减;
  • 多通道扫描稳定时间增加。

增加输入电容对直流或低频高阻抗信号可能有效,但它同时会降低输入带宽,因此必须根据目标信号频率设计。


4. ADC采样时间太短

ADC采样时间决定内部采样电容连接到外部信号的时间。

采样时间过短时,结果可能表现为:

低阻抗信号正常;
高阻抗信号异常;
采样速度降低后结果变准;
增加采样周期后数值明显稳定。

采样时间与以下因素有关:

  • ADC分辨率;
  • ADC时钟;
  • 信号源输出阻抗;
  • ADC内部采样电容;
  • 输入电压变化速度;
  • 目标精度。

RC充电过程可以近似表示为:

VC(t) = VIN × [1 - e^(-t/RC)]

要达到更高精度,采样电容必须更加接近最终输入电压,因此需要更长的稳定时间。

实际开发中不建议仅凭理论公式决定采样时间,而应该:

  1. 查看芯片数据手册中的ADC输入阻抗要求;
  2. 查看不同采样周期允许的最大外部阻抗;
  3. 从较长采样时间开始测试;
  4. 使用示波器或稳定电压源验证误差。

5. ADC输入没有RC低通滤波

传感器输出或者长线信号中可能包含大量高频噪声。

如果直接进入ADC:

传感器输出 ───────── ADC

高频干扰会直接反映到采样结果中。

可以增加简单RC低通滤波:

传感器输出 ── R ── ADC
                   │
                   C
                   │
                  GND

截止频率为:

FC = 1 / (2πRC)

例如:

R = 1kΩ
C = 100nF

截止频率约为:

FC ≈ 1591Hz

但RC参数必须根据被测信号带宽确定。

例如温度、电池电压等缓慢信号,可以使用较低截止频率;电流环、音频、振动信号则需要保留更高带宽。


6. 没有进行抗混叠滤波

如果输入信号中包含超过采样频率一半的频率成分,高频信号可能折叠到低频区域,形成混叠。

根据采样理论,采样频率应高于目标信号最高频率的两倍,但实际系统通常还要预留模拟滤波器过渡带。

例如:

目标信号最高频率:1kHz
ADC采样率:2kHz

理论上刚好满足两倍条件,但真实滤波器无法在1kHz到1kHz之间瞬间完成无限衰减,因此工程上应提高采样率并设计模拟抗混叠滤波器。

TI 的 ADC 资料将混叠列为采样系统中的误差来源,并建议在ADC前使用抗混叠滤波器抑制超出目标带宽的频率成分。


7. 电源纹波和负载干扰

当系统中存在以下负载时,ADC容易受到明显干扰:

  • 直流电机;
  • 无刷风扇;
  • PWM负载;
  • 蜂鸣器;
  • 继电器;
  • DC-DC电源;
  • 大功率LED;
  • 高频通信模块。

例如PWM控制风扇时,电源电流会周期性变化。

如果ADC采样时间恰好与PWM开关时刻重合,采样结果可能出现周期性波动。

改进方法

  • 模拟电源与功率电源适当隔离;
  • 增加局部去耦和储能电容;
  • 优化接地和回流路径;
  • ADC采样避开PWM开关边沿;
  • 使用定时器触发ADC,在固定相位采样;
  • 对传感器供电增加滤波;
  • 使用差分测量或者仪表放大器。

8. PCB布局不合理

ADC前端是模拟小信号电路,PCB布局会直接影响采样结果。

常见问题包括:

  • ADC输入线与PWM、SPI、时钟线平行;
  • 模拟信号跨越数字地分割区域;
  • ADC输入走线过长;
  • VREF去耦距离芯片过远;
  • 大电流回流经过模拟地;
  • DC-DC电感靠近ADC输入;
  • 运放反馈回路过长;
  • 高阻抗节点面积过大。

布局建议

  • ADC输入走线尽量短;
  • 远离高频时钟、PWM和开关节点;
  • 模拟输入周围保持干净;
  • 去耦电容靠近ADC供电和参考引脚;
  • 避免大电流回流穿过模拟区域;
  • 高阻抗输入节点尽量缩小;
  • 必要时使用完整地平面和合理分区。

9. 模拟输入超过允许范围

ADC输入电压通常必须位于规定范围内,例如:

VSSA ≤ VIN ≤ VDDA

具体范围需要查看芯片数据手册。

如果输入超过范围,可能出现:

  • ADC读数满量程;
  • 相邻通道受到影响;
  • 内部保护二极管导通;
  • 向模拟电源注入电流;
  • 采样结果异常;
  • 严重时损坏芯片。

不要认为增加一个串联电阻就一定安全。

还要检查:

  • 最大注入电流;
  • 输入钳位结构;
  • 输入负压;
  • 上电时序;
  • 外部信号在MCU掉电时是否仍然存在。

10. 传感器自身不稳定

ADC只是采集工具,输入源本身也可能波动。

例如:

  • 热敏电阻受气流影响;
  • 压力传感器电源不稳定;
  • 电流传感器零点漂移;
  • 光敏电阻受环境光变化;
  • 电位器接触不良;
  • 长线传感器受到工频干扰;
  • 电桥输出受温度影响。

排查时需要同时测量:

ADC引脚真实电压

不能只看软件打印值。

建议使用:

  • 高精度万用表;
  • 示波器;
  • 稳定直流源;
  • 示波器统计功能。

六、多通道ADC为什么会互相影响

STM32等MCU通常使用一个ADC转换核心,通过模拟多路复用器依次连接不同通道。

例如:

通道1:3.0V
通道2:0.2V
通道3:1.5V

如果通道1刚刚采样了3.0V,内部采样电容中还保存着较高电压。

紧接着切换到高阻抗的通道2,采样时间又很短,内部电容可能无法及时从3.0V放电到0.2V。

结果就可能出现:

通道2第一次采样值偏高;
第二次或第三次采样才恢复正常。

解决方法

方法一:增加采样时间

这是最直接的方法。

方法二:降低信号源阻抗

通过降低分压电阻或者增加运放缓冲。

方法三:切换通道后丢弃第一次结果

例如:

ADC_SelectChannel(channel);

ADC_ReadOnce();        /* 丢弃第一次 */

value = ADC_ReadOnce();

但这只是工程补偿方法,不能代替合理的模拟前端设计。

方法四:调整通道扫描顺序

可以尽量让相邻通道电压接近,或者将高阻抗通道安排在更有利的位置。

方法五:增加通道间隔时间

使用定时器触发或者软件延时,为信号稳定留出时间。


七、ADC结果不稳定的软件原因

1. ADC时钟设置过高

ADC时钟通常存在最大允许频率。

如果ADC时钟超过数据手册规定范围,可能导致:

  • 转换误差增大;
  • 数据不稳定;
  • 校准异常;
  • 某些温度或电压条件下失效。

需要检查:

系统时钟
  ↓
APB时钟
  ↓
ADC时钟源
  ↓
ADC分频器
  ↓
实际ADC时钟

不能只看主频,也不能直接认为ADC时钟等于CPU时钟。


2. 采样时间配置不合理

以STM32 HAL库为例:

ADC_ChannelConfTypeDef sConfig = {0};

sConfig.Channel = ADC_CHANNEL_5;
sConfig.Rank = ADC_REGULAR_RANK_1;
sConfig.SamplingTime = ADC_SAMPLETIME_2CYCLES_5;

如果输入阻抗较大,2.5 cycles可能太短。

可以逐步增加:

sConfig.SamplingTime = ADC_SAMPLETIME_19CYCLES_5;

或者:

sConfig.SamplingTime = ADC_SAMPLETIME_47CYCLES_5;

再比较采样结果。

不同STM32系列的采样周期枚举值不同,以上名称仅用于说明思路。


3. 没有执行ADC校准

部分STM32系列支持ADC偏置校准。

例如某些系列可以使用:

if (HAL_ADCEx_Calibration_Start(
        &hadc1,
        ADC_SINGLE_ENDED) != HAL_OK)
{
    Error_Handler();
}

部分旧系列的HAL函数可能是:

HAL_ADCEx_Calibration_Start(&hadc1);

不同芯片系列的校准流程和函数参数并不相同,必须查阅对应HAL驱动和参考手册。

ST 的ADC资料指出,部分STM32 ADC具有自动校准或偏置校准机制,可以用于减小ADC内部偏置误差。

推荐顺序:

ADC初始化
   ↓
执行校准
   ↓
等待校准完成
   ↓
启动ADC转换

4. 读取时序错误

轮询方式读取ADC时,应等待转换完成。

uint32_t ADC_ReadOnce(void)
{
    uint32_t value;

    if (HAL_ADC_Start(&hadc1) != HAL_OK)
    {
        return 0;
    }

    if (HAL_ADC_PollForConversion(
            &hadc1,
            10) != HAL_OK)
    {
        HAL_ADC_Stop(&hadc1);
        return 0;
    }

    value = HAL_ADC_GetValue(&hadc1);

    HAL_ADC_Stop(&hadc1);

    return value;
}

常见错误包括:

  • ADC还没转换完成就读取数据寄存器;
  • 启动失败后仍然读取旧数据;
  • 超时后没有处理错误;
  • 连续模式和单次模式理解错误;
  • EOC标志配置不正确;
  • 过载后数据被覆盖。

5. ADC通道配置错误

需要检查:

  • ADC实例是否正确;
  • 通道号是否正确;
  • GPIO是否配置为模拟模式;
  • Rank顺序是否正确;
  • 扫描通道数量是否正确;
  • DMA缓冲区长度是否匹配;
  • 引脚复用和封装是否对应;
  • 内部通道是否完成启动时间等待。

如果引脚没有设置为模拟模式,数字输入结构可能增加功耗或者引入干扰。


6. DMA数据对应关系错误

多通道DMA采集时,数据通常按照Rank顺序写入数组。

例如:

uint16_t adc_buffer[3];

配置顺序:

Rank 1:ADC_CHANNEL_1
Rank 2:ADC_CHANNEL_5
Rank 3:ADC_CHANNEL_8

那么缓冲区通常对应:

adc_buffer[0] → CHANNEL_1
adc_buffer[1] → CHANNEL_5
adc_buffer[2] → CHANNEL_8

如果软件误以为:

adc_buffer[0] → CHANNEL_5

就会出现“ADC数据乱跳”的假象。

实际并不是采样不稳定,而是通道映射错误。


7. DMA缓冲区类型或长度错误

例如ADC配置为半字传输,但缓冲区错误地使用了8位类型:

uint8_t adc_buffer[8];

更合理的是:

uint16_t adc_buffer[8];

还需要检查:

  • DMA数据宽度;
  • 内存数据宽度;
  • 循环模式;
  • 缓冲区长度;
  • 数据对齐;
  • 半传输和全传输回调。

8. Cache一致性问题

在带数据缓存的Cortex-M7等平台中,DMA直接写入内存后,CPU可能仍然读取Cache中的旧数据。

此时可能表现为:

  • DMA数据似乎不更新;
  • 数据间歇性变化;
  • 调试器看到的值与程序读取值不一致;
  • 修改优化等级后现象改变。

需要根据平台处理:

  • Cache失效;
  • Cache清理;
  • DMA缓冲区对齐;
  • 将DMA缓冲区放入非Cache区域;
  • 配置MPU属性。

这个问题主要出现在带D-Cache的MCU,不适用于所有STM32型号。


9. 整数计算溢出

例如计算平均值:

uint16_t sum = 0;

for (int i = 0; i < 100; i++)
{
    sum += adc_buffer[i];
}

12位ADC最大值为4095。

100个最大值相加:

4095 × 100 = 409500

已经超过16位无符号整数最大值65535。

结果会发生溢出,平均值当然错误。

应该使用:

uint32_t sum = 0;

如果采样数量更多,甚至需要使用:

uint64_t sum = 0;

10. 电压换算公式错误

12位ADC常见换算方式为:

voltage = adc_value * vref / 4095.0f;

常见错误包括:

voltage = adc_value / 4095 * 3.3f;

如果 adc_value4095 都是整数,整数除法会先执行。

例如:

2048 / 4095 = 0

最终结果会变成0。

应改为:

voltage = (float)adc_value * 3.3f / 4095.0f;

或者使用整数毫伏计算:

uint32_t voltage_mv;

voltage_mv = adc_value * 3300UL / 4095UL;

11. 参考电压写死为3.3V

下面的代码并不一定准确:

voltage = adc_value * 3.3f / 4095.0f;

因为实际VDDA可能是:

3.27V
3.31V
3.24V

如果对精度有要求,应使用实际测量值或者VREFINT补偿。


12. 采样频率选择不合理

采样频率太低,可能导致:

  • 快速信号丢失;
  • 混叠;
  • 波形失真;
  • 偶发采到尖峰。

采样频率过高,也可能导致:

  • 输入电路没有足够建立时间;
  • 高阻抗信号采样误差增大;
  • CPU负担增加;
  • DMA数据量过大;
  • 电源噪声增加。

采样频率应根据:

  • 信号带宽;
  • 输入阻抗;
  • ADC转换速度;
  • 滤波器截止频率;
  • 系统处理能力;

综合确定。


13. 采样时刻不固定

如果软件在主循环中直接读取ADC:

while (1)
{
    value = ADC_ReadOnce();

    DoSomething();
}

由于其他任务耗时不固定,ADC采样间隔也会变化。

对于周期信号,这可能造成采样值看起来非常混乱。

更可靠的方法是:

定时器产生固定触发
          ↓
ADC开始采样
          ↓
DMA保存结果

这样采样周期更加稳定。


八、ADC滤波是不是采样越多越好

不是。

增加采样数量虽然可以减小部分随机噪声,但会带来:

  • 响应速度下降;
  • CPU占用增加;
  • 延迟增大;
  • 瞬态变化被削弱;
  • 无法消除系统误差;
  • 无法解决参考电压漂移。

滤波之前必须先判断噪声类型。


九、常用ADC数字滤波方法

1. 限幅滤波

如果新数据与上一次数据相差过大,可以先认为它可能是异常值。

uint16_t ADC_LimitFilter(
    uint16_t new_value,
    uint16_t old_value,
    uint16_t threshold)
{
    uint16_t difference;

    if (new_value > old_value)
    {
        difference = new_value - old_value;
    }
    else
    {
        difference = old_value - new_value;
    }

    if (difference > threshold)
    {
        return old_value;
    }

    return new_value;
}

适合:

  • 缓慢变化信号;
  • 偶发尖峰噪声;
  • 温度和电池电压。

不适合:

  • 本身变化很快的信号;
  • 电流突变检测;
  • 快速控制系统。

2. 算术平均滤波

连续采样N次后求平均值:

uint16_t ADC_AverageFilter(uint16_t sample_count)
{
    uint32_t sum = 0;

    if (sample_count == 0)
    {
        return 0;
    }

    for (uint16_t i = 0; i < sample_count; i++)
    {
        sum += ADC_ReadOnce();
    }

    return (uint16_t)(sum / sample_count);
}

优点:

  • 实现简单;
  • 可以减小随机噪声;
  • 适合直流和缓慢信号。

缺点:

  • 采样次数越多,响应越慢;
  • 无法有效处理大幅异常值;
  • 无法消除系统误差。

3. 去极值平均滤波

先采集多个数据,去掉最大值和最小值,再计算平均。

uint16_t ADC_TrimmedMeanFilter(uint16_t sample_count)
{
    uint32_t sum = 0;
    uint16_t value;
    uint16_t max_value = 0;
    uint16_t min_value = 0xFFFF;

    if (sample_count < 3)
    {
        return ADC_ReadOnce();
    }

    for (uint16_t i = 0; i < sample_count; i++)
    {
        value = ADC_ReadOnce();

        sum += value;

        if (value > max_value)
        {
            max_value = value;
        }

        if (value < min_value)
        {
            min_value = value;
        }
    }

    sum -= max_value;
    sum -= min_value;

    return (uint16_t)(sum / (sample_count - 2));
}

适合存在偶发尖峰的场景。


4. 中值滤波

将采样数据排序,取中间值。

例如:

原始数据:
1000、1002、1001、3000、999

排序后:
999、1000、1001、1002、3000

中值:
1001

中值滤波对脉冲型异常值非常有效。

uint16_t ADC_Median5(
    uint16_t a,
    uint16_t b,
    uint16_t c,
    uint16_t d,
    uint16_t e)
{
    uint16_t data[5] = {a, b, c, d, e};

    for (uint8_t i = 0; i < 4; i++)
    {
        for (uint8_t j = 0; j < 4 - i; j++)
        {
            if (data[j] > data[j + 1])
            {
                uint16_t temp = data[j];

                data[j] = data[j + 1];
                data[j + 1] = temp;
            }
        }
    }

    return data[2];
}

5. 滑动平均滤波

滑动平均不是每次重新采集N个数据,而是保存最近N个样本。

新数据进入时,最旧数据移出。

#define ADC_WINDOW_SIZE 16

typedef struct
{
    uint16_t buffer[ADC_WINDOW_SIZE];
    uint32_t sum;
    uint16_t index;
    uint8_t initialized;
} ADC_MovingAverage_t;

uint16_t ADC_MovingAverageUpdate(
    ADC_MovingAverage_t *filter,
    uint16_t new_value)
{
    if (filter == NULL)
    {
        return new_value;
    }

    if (filter->initialized == 0)
    {
        for (uint16_t i = 0; i < ADC_WINDOW_SIZE; i++)
        {
            filter->buffer[i] = new_value;
        }

        filter->sum =
            (uint32_t)new_value * ADC_WINDOW_SIZE;

        filter->index = 0;
        filter->initialized = 1;
    }

    filter->sum -= filter->buffer[filter->index];

    filter->buffer[filter->index] = new_value;

    filter->sum += new_value;

    filter->index++;

    if (filter->index >= ADC_WINDOW_SIZE)
    {
        filter->index = 0;
    }

    return (uint16_t)(
        filter->sum / ADC_WINDOW_SIZE
    );
}

优点:

  • 计算效率较高;
  • 适合连续采样;
  • 波动比较平滑。

缺点:

  • 存在窗口延迟;
  • 窗口越大,响应越慢。

6. 一阶IIR低通滤波

一阶低通滤波常用公式为:

Y(k) = Y(k-1) + α × [X(k) - Y(k-1)]

其中:

X(k):当前原始采样值
Y(k):当前滤波结果
Y(k-1):上一次滤波结果
α:滤波系数,范围通常为0~1

例如:

float ADC_IIRFilter(
    float input,
    float alpha)
{
    static float output = 0.0f;
    static uint8_t initialized = 0;

    if (alpha < 0.0f)
    {
        alpha = 0.0f;
    }

    if (alpha > 1.0f)
    {
        alpha = 1.0f;
    }

    if (initialized == 0)
    {
        output = input;
        initialized = 1;
    }

    output += alpha * (input - output);

    return output;
}

参数规律:

α越大:
响应越快,滤波越弱。

α越小:
响应越慢,滤波越强。

十、过采样能提高ADC分辨率吗

在满足条件时,过采样和抽取可以改善信噪比,并可能提高有效分辨率。

基本流程是:

以更高频率采样
      ↓
累加多个结果
      ↓
平均或右移
      ↓
降低输出数据率

例如进行16倍过采样:

连续采样16次
     ↓
累加
     ↓
除以16

部分STM32型号带有硬件过采样模块,可以自动完成累加和移位,减少CPU参与。ST的资料同时介绍了硬件和软件过采样方法,主要用于改善信噪比和有效分辨率。

但过采样不是万能的。

它通常要求噪声具有一定随机性。如果输入和参考完全固定、结果始终停留在同一个ADC代码上,简单重复采样并不会凭空产生额外信息。

过采样还会降低最终输出数据率:

采样率越高
     ↓
累加数量越多
     ↓
输出更新速度越慢

十一、STM32 ADC稳定采集示例

下面以单通道轮询采集为例。

1. ADC初始化后进行校准

void ADC_ApplicationInit(void)
{
    MX_ADC1_Init();

    /*
     * 注意:
     * 不同STM32系列的校准函数参数不同。
     * 请根据对应HAL库修改。
     */
    if (HAL_ADCEx_Calibration_Start(
            &hadc1,
            ADC_SINGLE_ENDED) != HAL_OK)
    {
        Error_Handler();
    }
}

2. 单次读取函数

HAL_StatusTypeDef ADC_ReadRaw(
    ADC_HandleTypeDef *hadc,
    uint32_t *value)
{
    HAL_StatusTypeDef status;

    if ((hadc == NULL) || (value == NULL))
    {
        return HAL_ERROR;
    }

    status = HAL_ADC_Start(hadc);

    if (status != HAL_OK)
    {
        return status;
    }

    status = HAL_ADC_PollForConversion(
        hadc,
        10);

    if (status != HAL_OK)
    {
        HAL_ADC_Stop(hadc);
        return status;
    }

    *value = HAL_ADC_GetValue(hadc);

    return HAL_ADC_Stop(hadc);
}

3. 32次去极值平均

#define ADC_SAMPLE_COUNT 32U

HAL_StatusTypeDef ADC_ReadFiltered(
    ADC_HandleTypeDef *hadc,
    uint16_t *filtered_value)
{
    uint32_t sum = 0;
    uint32_t value;
    uint32_t max_value = 0;
    uint32_t min_value = 0xFFFFFFFFUL;

    if ((hadc == NULL) ||
        (filtered_value == NULL))
    {
        return HAL_ERROR;
    }

    for (uint32_t i = 0;
         i < ADC_SAMPLE_COUNT;
         i++)
    {
        if (ADC_ReadRaw(hadc, &value) != HAL_OK)
        {
            return HAL_ERROR;
        }

        sum += value;

        if (value > max_value)
        {
            max_value = value;
        }

        if (value < min_value)
        {
            min_value = value;
        }
    }

    sum -= max_value;
    sum -= min_value;

    *filtered_value = (uint16_t)(
        sum / (ADC_SAMPLE_COUNT - 2U)
    );

    return HAL_OK;
}

4. 换算为毫伏

uint32_t ADC_CodeToMillivolt(
    uint16_t adc_code,
    uint32_t vref_mv,
    uint8_t resolution_bits)
{
    uint32_t full_scale;

    if ((resolution_bits == 0U) ||
        (resolution_bits >= 31U))
    {
        return 0U;
    }

    full_scale =
        (1UL << resolution_bits) - 1UL;

    return (
        (uint32_t)adc_code * vref_mv
    ) / full_scale;
}

调用示例:

uint16_t adc_value;
uint32_t voltage_mv;

if (ADC_ReadFiltered(
        &hadc1,
        &adc_value) == HAL_OK)
{
    voltage_mv = ADC_CodeToMillivolt(
        adc_value,
        3300U,
        12U);
}

十二、如何利用VREFINT补偿参考电压

部分STM32内部带有参考电压通道VREFINT,并在系统存储区保存出厂校准值。

基本思路是:

已知内部参考电压比较稳定
          ↓
使用ADC测量VREFINT
          ↓
根据VREFINT ADC结果估算实际VDDA
          ↓
用实际VDDA换算其他通道

概念公式可以写为:

VDDA实际值
≈ 校准时VDDA × VREFINT_CAL / VREFINT_ADC

不同STM32系列的:

  • 校准电压;
  • 校准数据地址;
  • 计算宏;
  • 温度条件;

可能不同。

必须使用该芯片数据手册提供的公式和校准常量,不能直接套用其他型号代码。


十三、典型现象与原因对照表

ADC现象 可能原因
上下波动1~3个码 量化误差、正常底噪
随机跳动几十个码 电源噪声、输入悬空、布局干扰
周期性波动 PWM、电机、DC-DC或工频干扰
数值稳定但整体偏高 VREF估计错误、增益误差
数值稳定但整体偏低 采样时间不足、输入阻抗过大
切换通道后第一笔异常 采样电容残留、通道串扰
读数缓慢漂移 温度、参考源或传感器漂移
经常读取0 引脚接地、通道错误、读取失败
经常读取满量程 输入超范围、引脚开路或配置错误
DMA数据顺序混乱 Rank、缓冲区和DMA长度不匹配
电机启动后异常 电源跌落、地弹、EMI干扰
降低采样率后变准 输入阻抗过大、建立时间不足
增加采样时间后变准 内部采样电容未充分充电
平均值正常但瞬时值很乱 高频噪声或缺少滤波
万用表正常、ADC异常 万用表带宽低,可能看不到高频噪声

十四、ADC完整排查流程

遇到ADC不稳定时,可以按照以下顺序排查。

第一步:使用稳定电压源替代传感器

先不要直接测试复杂传感器。

可以使用:

  • 稳压电源;
  • 精密基准;
  • 电池;
  • 电位器;
  • DAC输出。

观察ADC是否仍然波动。


第二步:测量ADC引脚真实波形

使用示波器测量:

ADC引脚对模拟地的电压

重点查看:

  • 平均电压;
  • 纹波大小;
  • 高频尖峰;
  • PWM同步噪声;
  • 50Hz或60Hz干扰;
  • 电机启停瞬态。

第三步:检查参考电压

测量:

VDDA
VREF+
VSSA

观察负载变化时是否波动。


第四步:检查输入阻抗

计算信号源等效输出阻抗。

对于电阻分压器:

ROUT = R1 || R2

即:

ROUT = R1 × R2 / (R1 + R2)

如果阻抗较大:

  • 增加采样时间;
  • 降低分压电阻;
  • 增加输入电容;
  • 增加运放缓冲。

第五步:增加采样时间

将ADC采样周期逐步增加。

如果采样时间越长,结果越准确,通常说明:

信号源阻抗过大
或者
输入建立时间不足

第六步:降低ADC时钟

检查ADC时钟是否超过芯片规格,并适当降低ADC时钟进行对比测试。


第七步:执行ADC校准

如果芯片支持校准,应按照参考手册执行。


第八步:只保留一个ADC通道

暂时关闭多通道扫描和DMA,只测试一个通道。

如果单通道正常、多通道异常,重点检查:

  • 通道Rank;
  • 采样时间;
  • 通道切换残留;
  • DMA顺序;
  • 缓冲区长度。

第九步:取消数字滤波看原始数据

先打印原始ADC结果。

不要一开始就使用复杂滤波,否则可能掩盖问题。

建议同时打印:

最小值
最大值
平均值
峰峰值

例如:

printf(
    "ADC min=%lu max=%lu avg=%lu pp=%lu\r\n",
    min_value,
    max_value,
    average,
    max_value - min_value);

第十步:最后再设计数字滤波

确认硬件和ADC配置正确后,再根据应用选择:

  • 平均滤波;
  • 滑动平均;
  • 中值滤波;
  • 限幅滤波;
  • IIR低通;
  • 过采样;
  • 卡尔曼滤波。

十五、一个实用的ADC稳定性测试程序

下面程序连续采集一组数据,并统计最小值、最大值、平均值和峰峰值。

typedef struct
{
    uint32_t minimum;
    uint32_t maximum;
    uint32_t average;
    uint32_t peak_to_peak;
} ADC_Statistics_t;

HAL_StatusTypeDef ADC_GetStatistics(
    ADC_HandleTypeDef *hadc,
    uint32_t sample_count,
    ADC_Statistics_t *statistics)
{
    uint64_t sum = 0;
    uint32_t value;
    uint32_t minimum = 0xFFFFFFFFUL;
    uint32_t maximum = 0;

    if ((hadc == NULL) ||
        (statistics == NULL) ||
        (sample_count == 0U))
    {
        return HAL_ERROR;
    }

    for (uint32_t i = 0;
         i < sample_count;
         i++)
    {
        if (ADC_ReadRaw(hadc, &value) != HAL_OK)
        {
            return HAL_ERROR;
        }

        sum += value;

        if (value < minimum)
        {
            minimum = value;
        }

        if (value > maximum)
        {
            maximum = value;
        }
    }

    statistics->minimum = minimum;
    statistics->maximum = maximum;

    statistics->average =
        (uint32_t)(sum / sample_count);

    statistics->peak_to_peak =
        maximum - minimum;

    return HAL_OK;
}

调用:

ADC_Statistics_t adc_stats;

if (ADC_GetStatistics(
        &hadc1,
        1000U,
        &adc_stats) == HAL_OK)
{
    printf(
        "min=%lu, max=%lu, avg=%lu, pp=%lu\r\n",
        adc_stats.minimum,
        adc_stats.maximum,
        adc_stats.average,
        adc_stats.peak_to_peak);
}

这个程序比只观察单个ADC值更加有意义。

例如:

min = 2045
max = 2050
avg = 2048
pp  = 5

说明平均值比较稳定,峰峰波动约5个ADC码。

如果结果是:

min = 1800
max = 2300
avg = 2040
pp  = 500

则说明存在明显干扰,不能仅依靠平均滤波处理。


十六、常见错误做法

错误一:无限增加平均次数

平均1000次虽然可能很稳定,但系统响应可能已经无法满足要求。


错误二:看到波动就认为ADC坏了

几个LSB的轻微波动可能是正常现象。


错误三:只看万用表,不看示波器

万用表显示的是低带宽平均结果,可能看不到高频纹波和尖峰。


错误四:默认参考电压一定是3.3V

ADC结果与VREF直接相关,参考电压必须测量或补偿。


错误五:高阻分压后直接接ADC

必须检查输出阻抗与采样时间是否匹配。


错误六:滤波解决所有问题

数字滤波不能修复:

  • 输入超范围;
  • VREF错误;
  • 采样时间不足;
  • 通道映射错误;
  • ADC时钟超限;
  • PCB布局问题。

错误七:所有通道都使用最短采样时间

不同信号源阻抗可能不同,需要分别配置合理采样时间。


十七、工程中如何设计一个稳定ADC采集系统

一个比较完整的ADC采集设计应该包含:

传感器或模拟输入
        ↓
输入保护
        ↓
分压或放大
        ↓
RC低通滤波
        ↓
运放缓冲(可选)
        ↓
ADC输入
        ↓
稳定参考电压
        ↓
定时器固定触发
        ↓
DMA连续采集
        ↓
异常值处理
        ↓
数字低通滤波
        ↓
校准和单位换算

设计时需要同时考虑:

  • 输入电压范围;
  • 输入阻抗;
  • 信号带宽;
  • 采样频率;
  • 分辨率;
  • 参考电压;
  • 噪声水平;
  • 响应速度;
  • PCB布局;
  • 温度漂移;
  • 校准方式。

十八、面试中如何回答ADC结果不稳定

如果面试官问:

ADC采集结果为什么不稳定,应该怎么排查?

可以这样回答:

ADC结果不稳定需要从硬件和软件两个方面排查。

硬件方面首先检查参考电压和模拟电源是否稳定,
然后检查ADC输入是否悬空、信号源输出阻抗是否过大、
采样引脚是否存在高频干扰,以及PCB模拟地和数字地的回流是否合理。

由于MCU内部ADC通常包含采样保持电容,
如果输入阻抗过大或者采样时间太短,
采样电容无法充分充电,也会造成测量误差。

软件方面需要检查ADC时钟、采样时间、通道Rank、
校准流程、转换完成标志、DMA缓冲区和电压换算公式。

确认硬件和配置正确后,
再根据应用使用平均滤波、中值滤波、滑动平均、
IIR低通或过采样提高稳定性。

十九、总结

ADC采集结果不稳定,不一定是ADC外设本身损坏。

常见硬件原因包括:

参考电压不稳定;
ADC输入悬空;
信号源阻抗过大;
采样时间不足;
缺少模拟滤波;
电源和地线干扰;
PCB布局不合理;
输入电压超范围;
多通道之间相互串扰。

常见软件原因包括:

ADC时钟配置错误;
采样周期太短;
没有执行ADC校准;
读取时序不正确;
通道Rank和DMA顺序错误;
缓冲区数据类型错误;
整数计算溢出;
参考电压写死;
采样频率不合理;
数字滤波方法不合适。

排查ADC问题时,应按照以下顺序进行:

先检查真实模拟电压
        ↓
再检查参考电压和硬件电路
        ↓
再检查ADC时钟、采样时间和校准
        ↓
再检查DMA、通道和计算公式
        ↓
最后才增加数字滤波

最后用一句话总结:

ADC稳定采集不是单纯增加平均次数,而是要让信号源、参考电压、采样时间、模拟前端、软件配置和数字滤波共同满足系统要求。

Logo

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

更多推荐