ADC 采样精度实战:同一颗 12 位 ADC,凭什么别人做到 11.5 bit 有效精度,我只有 9 bit?

2026年9月19日 0 By admin

上个月接手一个项目:用 STM32G4 内置的 12 位 ADC 采一路应变桥信号,理论上满量程 4096 个码,客户要求分辨到 1mV 级别——按说绰绰有余。结果实际采回来的数据在低位疯狂跳动,我做了个直方图一统计,有效精度(ENOB)只有 9 bit 出头,等于白花了 3 个 bit 的钱。

这周把信号链从头到尾撸了一遍,从采样电路、基准电压到软件滤波,最后把 ENOB 做到了 11.4 bit。这篇文章就把整个过程拆开讲讲:精度是怎么一层一层丢掉的,又怎么一层一层捡回来。

先算账:你的 ADC 真有 12 位精度吗

先建立一个基本认知:分辨率(Resolution)≠ 有效精度(ENOB)。

数据手册上的 12 位是”能把量程切多少份”,而实际有效位数要看噪声。理想 ADC 的信噪比公式是:

SNR = 6.02 × N + 1.76  (dB)

反推有效位数:ENOB = (SNR - 1.76) / 6.02。

我们用最朴素的方法测:输入端接一个稳定的直流电平(我用的是基准芯片分压后固定在半量程),连续采 4096 个点,统计标准差 σ。假设 σ = 12 个 LSB,那你的”噪声自由位”就是 log₂(12) ≈ 3.6 位,有效精度只剩 12 – 3.6 ≈ 8.4 bit。跳动 12 个 LSB,看起来就是末三位在乱跳——和我项目的现象完全吻合。

一句话记住:跳动几个 LSB,就丢掉 log₂(跳动数) 位精度。 低位乱跳不是玄学,全是噪声。

丢掉 3 个 bit 的噪声从哪来?我后来排查出来一共四路,按”贡献度”排序:信号源阻抗问题、基准电压问题、地与布局问题、以及软件压根没做滤波。下面一个个说。

坑一:信号源阻抗太高,采样电容充不满

ADC 内部有个采样保持电路,本质上是一个小电容(几百 pF 到几 nF)通过内部开关接到输入引脚。采样开关闭合的瞬间,内部电容上的电荷要和外部电路重新分配——如果你的信号源阻抗太高,或者采样时间设太短,内部电容根本充不到真实电压,采出来的值就偏低且随前一次状态变化,表现为”跳码”和通道间串扰。

我的初始配置是标准的”例程风格”:

/* 初始配置:照抄例程,采样时间 1.5 cycle —— 灾难 */
hadc1.Init.Resolution = ADC_RESOLUTION_12B;
sConfig.SamplingTime = ADC_SAMPLETIME_1CYCLE_5;

而我的信号链是:应变桥 → 仪表运放(输出阻抗约 1kΩ)→ 1kΩ 串联电阻 → ADC 引脚,总源阻抗 2kΩ 上下。粗算一下:内部采样电容按数据手册取 5pF(G4 的 ADC),RC 时间常数 τ = 2k × 5p = 10ns,要充到 0.1% 精度(10 bit 有效)需要 τ × ln(1000) ≈ 69ns,对应采样时间至少 10 个 cycle(170MHz 的 ADC 时钟)。要 12 位满精度,还得再乘,保守直接给到 100 cycle 以上。

修改后的配置:

/* 修改后:给足采样时间 + 外部加 RC,双保险 */
sConfig.SamplingTime = ADC_SAMPLETIME_247CYCLES_5;  /* G4 支持最长档 */

同时在外围加了一阶 RC:1kΩ + 10nF 直接放在 ADC 引脚旁边。这个电容的作用是”电荷水库”——采样开关闭合瞬间的电荷冲击由它就近提供,而不是逼着远处的运放快速响应。注意这个 RC 的截止频率(1/2πRC ≈ 16kHz)远高于我的信号带宽(几百 Hz),不会损伤有用信号。

经验法则记住一条:外部 RC 的电容至少要是 ADC 内部采样电容的 1000 倍,采样开关闭合引起的压降才能压到千分之一以下。

坑二:基准电压不稳,等于刻度尺在晃

ADC 是个比值测量器件:结果 = Vin / VREF × 4096。VREF 抖 1%,你的读数就全体抖 1%,而且这种误差做软件平均也压不掉(因为它是有相关性的抖动,不是白噪声)。

我项目里 VREF 一开始直接用的 VDDA(3.3V 由 DCDC 提供)。示波器挂上去一看,纹波 + 开关噪声接近 20mV,折算下来刚好贡献好几个 LSB 的跳动。处理方式:

  • 应变桥这种”激励源和基准相关”的场合,最优雅的做法是比率测量:用同一个基准(或同一颗激励电源)同时喂传感器桥和 ADC 的 VREF,激励源的波动在比值里直接抵消;
  • 退一步也要给 VREF 单独供:磁珠 + 大电容 + 1μF 陶瓷紧贴 VREF 引脚,或者干脆上专用基准芯片(REF3033 这类,成本几毛钱);
  • 如果芯片有独立的 VREF+ 引脚,务必单独走线并就近放去耦电容,不要和数字电源共用。这一步在之前那篇《LDO 与 DCDC 电源设计实战》里也踩过,电源是 ADC 的地基,地基晃楼必晃。

改完基准这一项,直方图的标准差从 12 LSB 降到了 4 LSB 左右——单项贡献最大。

坑三:地与布局,模拟部分的”安静”是画出来的

原理图改对了,布局不合理照样白搭。这一波我重点动了三处:

  • ADC 输入走线远离时钟和开关电源。原来那根模拟信号线紧贴着一路 25MHz 的 SPI 时钟走了 3cm,改版后让它单独走,两侧包地;
  • VREF 去耦电容紧贴引脚,回路面积最小化;STM32 的 VDDA 单独经磁珠从 VDD 取电,模拟部分单点接地;
  • 运放和分压电阻的返回电流路径避开 ADC 的模拟地。数字电流流过模拟地,哪怕几 mV 的压降,对一个几 mV 级的信号就是灾难。

软件上也留了一手——G4 系列支持硬件过采样:

/* 硬件过采样:16 倍采样 + 累加右移 4 位,等效 16 位输出 */
hadc1.Init.OverSampling = ADC_OVERRSAMPLING_ENABLE;
hadc1.Init.OversamplingMode = ADC_TRIGGEREDMODE_SINGLE_TRIGGER;
hadc1.Init.Oversampling.Ratio = ADC_OVERSAMPLING_RATIO_16;
hadc1.Init.Oversampling.RightBitShift = ADC_RIGHTBITSHIFT_4;

软件端捡精度:过采样不是简单求平均

硬件过采样开了之后,剩下的在软件里继续榨。核心原理:白噪声每平均 4 个点,精度提高 1 位(标准差按 1/√N 缩小,16 倍平均就是 2 位)。想多捡 2 位,就过采样 16 倍;捡 3 位,64 倍。

但要成立有个前提:噪声得是白噪声。如果噪声里混着 50Hz 工频干扰,简单平均对它效果很差——因为它是相关的。两个对策:

  1. 让采样窗口对齐工频整周期。50Hz 周期 20ms,让”一次平均”的总时长正好是 20ms 的整数倍(比如 64 点平均,则采样周期 = 20ms/64 = 312.5μs),工频干扰在一个窗口内积分恰好为零;
  2. 均值 + 中值的组合滤波。滑动均值对付白噪声,偶发的尖刺用中值滤掉。我最后用的窗口是”每次触发采 64 点求均值,输出端再做 8 点滑动平均”,等效又多捡了 1 位。
/* 触发一次 = 采 64 点求均值(DMA 循环搬运,主循环取用) */
#define OSR_N   64
uint32_t acc = 0;
for (int i = 0; i < OSR_N; i++) acc += raw_buf[i];
uint16_t avg = acc >> 6;   /* 除以 64 */

注意别走火入魔:过采样提升的是分辨噪声的最小刻度,不提升线性度和增益误差。如果你的信号本身在晃(真信号),平均只会把真实变化抹平。所以平均窗口要和信号的响应速度匹配——我这里温度/重量信号变化在秒级,几十毫秒的窗口毫无压力。

校准:出厂两点校准,把系统误差钉死

过采样做完,随机噪声压下去了,剩下的就是系统误差:偏移和增益。这类误差平均不掉,只能校准。我的做法是设备出厂时做两点校准:

  • 点 1:输入已知低电平(比如 0.5V),记读数 R1;
  • 点 2:输入已知高电平(比如 2.5V),记读数 R2;
  • 把 (R1, V1)、(R2, V2) 存进 Flash,运行时线性插值反算。
/* 两点校准反算:y = k*x + b */
float k = (V2 - V1) / (float)(R2 - R1);
float real = k * ((float)avg - (float)R1) + V1;

这招对电阻、传感器的温漂只能部分缓解(那是非线性问题),但对 ADC 本身的增益/偏移误差基本可以归零。STM32 部分型号还有出厂自校准(HAL_ADCEx_Calibration_Start),上电记得调一次,这个函数不加,偏移能差出好几个 LSB——我第一版就是忘了调,空采一直有个 20 LSB 的底偏。

怎么验证:直方图和标准差是最好的尺子

改了这么多,怎么知道有没有用?我的验证手段很朴素但有效:

/* 连续采 N 个点,输出 min / max / std,终端一眼看出跳动量 */
for (int i = 0; i < N; i++) {
    buf[i] = read_avg();
    sum += buf[i];
    if (buf[i] < mn) mn = buf[i];
    if (buf[i] > mx) mx = buf[i];
}
mean = sum / N;
for (int i = 0; i < N; i++) acc2 += (buf[i] - mean) * (buf[i] - mean);
std = sqrt(acc2 / N);   /* ENOB ≈ 12 - log2(2*std) */

调优前后的实测对比:

指标 调优前 调优后
峰峰跳动 (min~max) ~45 LSB ~6 LSB
标准差 σ 12 LSB 1.5 LSB
有效精度 (估算) ~8.4 bit ~11.4 bit
1mV 分辨能力 达不到 稳定达到

跳码从 45 个 LSB 收敛到 6 个,客户验收当天一次过。

写在最后:一张 ADC 精度排查清单

把这轮调试沉淀成 checklist,下次遇到”ADC 数值乱跳”直接按顺序过:

  • ☑ 输入端源阻抗多大?外部加 RC(≥ 内部采样电容 1000 倍)了吗?
  • ☑ 采样时间给够了吗?按 12 位满精度算,宁多勿少;
  • ☑ VREF 来源是什么?纹波多大?基准芯片 / 比率测量用上了吗?
  • ☑ VREF、VDDA 的去耦电容紧贴引脚了吗?模拟地数字地单点相连了吗?
  • ☑ 模拟走线避开了时钟和开关电源吗?
  • ☑ 上电调过 HAL_ADCEx_Calibration_Start 了吗?
  • ☑ 硬件/软件过采样开了吗?平均窗口对齐工频周期了吗?
  • ☑ 出厂两点校准数据写进 Flash 了吗?

回头看整个过程,其实每一步都不难,难的是量化思维:别对着乱跳的数字干瞪眼,先统计出标准差,把”跳了多少个 LSB”翻译成”丢了几位精度”,再按信号链逐级归因。噪声不会撒谎,直方图会告诉你答案。