前一阵帮朋友调试一块数据采集板,现象很有意思:同一路电池电压,用万用表量是 3.60V,板子上的 ADC 转换读出来却只有 3.42V,而且最后两三位还在来回跳。换一片单片机之后,数值又不一样了。排查了半天,问题拆开来看都不算难——采样时间没给够、参考电压不稳、滤波策略选得不对,但它们叠在一起,就能把一个看似普通的采集任务搞得非常折腾。
这篇文章算是我这些年做 ADC 转换相关项目的一个小结,把采样原理、取平均、精度、校准这几块串起来讲一遍。适合正在用单片机做电池监测、传感器采集、仪器仪表的同行,尤其适合那种"读出来的数总是不对劲,但又说不出问题在哪"的朋友。
1. 采样原理不是"读寄存器"那么简单
1.1 SAR 型 ADC 内部的"二分查找"
绝大多数单片机内置的都是逐次逼近型 ADC,也就是 SAR ADC。名字听着高级,其实核心思路就是二分法猜电压。
ADC 转换一个 N 位数值,内部会做 N 次比较:先把输入电压和内部 DAC 生成的一半量程电压比较,如果输入更高,最高位记为 1,下次比较四分之三量程;如果更低,最高位记为 0,下次比较四分之一量程。这样每比较一次就锁定一位,12 位 ADC 就是 12 次比较。整个过程里外里也就是几个微秒到几十微秒。
很多人以为这么快的转换速度意味着"随时读都是准的",其实大错特错。ADC 转换的是某一个瞬间的电压,而这个瞬间能不能代表你想要的信号,取决于前面的采样保持电路。
1.2 采样保持:你得抓住正确的一瞬间
SAR ADC 的输入端通常有一组开关和采样电容。转换开始前,开关闭合,让外部信号给这个小电容充电;充到和外边电压一致了,开关断开,电容就"保持"住这个电压,后面 12 次比较全是基于这个电容上保住的电荷。
这里就出现第一个坑:如果开关闭合的时间不够长,电容根本没充满,保持住的电压就不是真实电压。充电过程服从 RC 指数规律:
Vc(t) = Vin(1 - e^(-t/τ))
其中 τ = Rsource × Csample,Rsource 是信号源的等效输出电阻,Csample 是 ADC 内部采样电容。12 位 ADC 要求充电误差小于半个 LSB,也就是 e^(-t/τ) 要小于 1/8192,算下来需要 t ≥ 9τ。
我手头某块常用单片机的 ADC 采样电容标称约 14pF,如果用单片机的引脚直接去读一个内阻 100kΩ 的信号,τ = 1.4μs,9τ 就是约 12.6μs。很多芯片出厂默认采样时间连 5μs 都不一定有,这种情况下读数就会系统性偏低。不是你程序写错,是物理没等电容充满。
1.3 高内阻信号源为什么总让读数偏低
最常见的引发源有两种:一个是电阻分压网络,另一个是加了外部 RC 滤波。
比如你要用两个 100kΩ 电阻把 12V 分压到 6V 再给 ADC 量,信号源等效内阻就是两个电阻并联的 50kΩ,这就已经接近上面说的临界值了。如果还为了滤波在 ADC 引脚前面加一个 10kΩ 电阻和 0.1μF 电容,那 RC 滤波本身的时间常数就有 1000μs,你得等好几个毫秒才能让后级稳定。很多项目根本不注意这个时间,主循环一口气读完,数据自然不对。
解决思路按项目条件选:
- 能改硬件的话,把分压电阻整体降低到 10kΩ 级别,或者干脆加一级运放缓冲,让输出阻抗降到几十欧姆。
- 改软件的话,把采样保持时间设置调到最大,或者在 ADC 启动前预留足够建立时间。
- 外部 RC 滤波的参数要算总建立时间,不要凭感觉选 100kΩ 级别的电阻。
提示:单独在 ADC 引脚并联 0.1μF 电容不是万能药。它能让信号源瞬间驱动采样电容,但电容本身也要先充满,信号源内阻大时它充电同样慢。正确做法是把它当作电荷存储器,同时保证外电路有足够时间把它充满。
2. 取平均到底能带来多少收益
2.1 平均次数和噪声之间的数学关系
ADC 读数的随机波动,大部分来自热噪声、电源纹波和电磁干扰,这类噪声在统计上接近白噪声。白噪声有一个很有意思的性质:对 N 次采样求平均,噪声幅度按根号 N 的倒数衰减。
也就是平均 4 次,噪声减半;平均 16 次,噪声变成四分之一。对应到有效位数,平均 4 次大约能多拿 1 位有效位,平均 16 次多拿 2 位。这也是为什么很多人调试时发现"加了平均值之后数字稳多了"——不是心理作用,是有数学依据的。
但必须泼一盆冷水:取平均只能对付随机噪声,对系统误差毫无办法。如果读数系统性偏大 30mV,平均一万次也是偏大 30mV。解决系统误差靠的是第四章的校准,不是滤波。
2.2 突发采样、分散采样还是滑动窗口
实际项目里"取平均"有三种常见做法,很多人不知道它们的差别:
| 策略 | 做法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 突发采样 | 连续快速读 N 次后求均值 | 实现简单,延迟小 | 相邻采样间隔太短,噪声相关性高,平均效果打折 | 信号变化快、噪声主要是高频热噪声 |
| 定时分散采样 | 每隔固定时间读一次,累积 N 次平均 | 样本之间不相关,对周期性干扰有抑制作用 | 延迟增大,占内存 | 电源纹波、工频干扰明显的场合 |
| 滑动窗口 | 维护最近 N 个读数的 FIFO,每来一个新值就更新均值 | 实时性好,适合实时控制 | 窗口太长会让响应变迟钝 | 持续采集的监控类场景 |
我自己的习惯:温度、电池电压这类慢变量,用定时分散采样,间隔 10~50ms 读一次,累计 8~16 次平均,效果比突发采样稳得多。突发采样最大的问题是内部连续转换的周期太固定,如果噪声和这个周期存在某种固定相位关系,平均结果会出现很难查的残余偏差。
滑动窗口也不是越多越好。窗口 64 意味着信号突变了要 64 个采样周期才能完全跟上来。做电池电量显示没问题,做电流环控制就得掂量一下。滤波参数要和你的系统时间常数匹配,不是数值越大越好。
2.3 过采样提高有效位数,划算吗
取平均的极限玩法是过采样。理想条件下额外增加 K 位有效分辨率,需要过采样 4K 倍,也就是想从 12 位有效位提到 14 位,要采 16 次;提到 16 位,要采 256 次。条件很苛刻:输入噪声必须超过 1 个 LSB,且最好均匀分布,否则提升不了那么多。
所以我的建议是:把过采样当作"软件增益"用,别指望它替代真正的硬件设计。它适合低成本项目里把 12 位 ADC 假装成 14 位用,前提是你的信号本身够吵、够稳定,且采样速率有余量。如果信号源内阻大、参考电压又漂,过采样救不了精度,只是把一个不准的数变得更平滑而已。
3. 标称 12 位,实际有多少位能用
3.1 有效位数 ENOB 才是精度的真相
芯片手册写 12 位,指的是分辨率——最低一位对应多少电压,也就是动态范围被切成了 4096 份。但实际测量中能用到的位数,通常达不到标称值。有效位数 ENOB 综合考虑噪声、失真、非线性,才是你真正能信的数字。
一块常见的 12 位 SAR ADC,标称量程 3.3V,1 个 LSB 是 0.8mV。听起来很高大上,但如果你实测读数有 ±5 个 LSB 的跳动,实际有效位也就剩 9 位左右。这也是为什么很多人拿着 12 位 ADC 做称重传感器,做完发现最后一位完全是噪声。
精度是"读出来的值和真值差多少",分辨率只是"最低一位的步进"。这两个概念的区别,是 ADC 项目里最容易出问题的认知盲区。
3.2 刚性误差与软性误差清单
我把常见误差源按"能不能靠软件修"分成两类,做项目时心里要有个台账:
| 误差源 | 典型量级(12位) | 能不能校准 | 影响表现 |
|---|---|---|---|
| 偏移误差 | ±3~±10 LSB | 能,两点校准修 | 零点不对 |
| 增益误差 | ±0.5%~±1.5% | 能,两点校准修 | 满量程偏差明显 |
| 差分非线性 DNL | ±0.5~±1 LSB | 基本不能 | 个别码宽异常 |
| 积分非线性 INL | ±2~±4 LSB | 只能查表部分修 | 中段弯曲 |
| 参考电压温漂 | 10~50ppm/℃ | 换外部基准 | 全量程跟着飘 |
| 电源噪声耦合 | 不定 | 硬件滤波 | 读数跳动 |
| 引脚接触/漏电 | 不定 | 清洗PCB | 读数偏高偏低不定 |
偏移和增益误差,占到整个误差的大头,也是第四章两点校准能处理的部分。DNL 和 INL 是芯片自身的非线性,软件很难完全修掉。真正让"标称 12 位"和"实际 12 位"拉开距离的,往往是后面那三项——参考电压、电源、PCB。
3.3 参考电压才是那把"尺子"
ADC 的本质是用参考电压当尺子量输入。Vref 如果是 3.3V,ADC 输出 2048 就代表 1.65V;如果 Vref 悄悄变成 3.28V,同样那个码代表的真实电压就变了。很多人折腾半天采样电路,其实问题是那把尺子本身漂了。
单片机内部参考电压大多不怎么讲究,温漂几十 ppm 是常态,而且很多芯片内部参考和模拟电源共用一条路线,数字电路一跑起来,Vref 就跟着抖。实在要求高,外挂一个基准芯片是正路;不想加芯片,至少要保证 Vref 引脚附近的去耦电容足够,常见做法是 10μF 钽电容并联 0.1μF 陶瓷电容,紧贴引脚放置。
注意:别只看 Vref 引脚的标称电压。用万用表实测一下上电瞬间和运行时的 Vref 数值,如果明显偏低或波动超过几个毫伏,先解决这个再去折腾采样。
3.4 布板阶段就能决定的精度
很多精度问题是画 PCB 那一刻埋下的雷。模拟信号走线和数字时钟线并排跑 5cm,读出来的数不跳才怪。几个我最近常提的布板原则:
- 模拟地和数字地单点连接,不要让大电流地回路穿过 ADC 的模拟参考地。
- ADC 输入走线尽量短,两边包地,远离 PWM 输出和晶振区域。
- 采样信号线不要跨过板子上的电源层分割线,否则回流路径绕大圈,噪声全进来了。
- 分压电阻用 1% 甚至 0.1% 精度,成本高不了多少,但系统增益误差能明显改善。
同样的电路,一个按这些规则布板,一个随便拉线,实测有效位数能差 1 到 2 位,这个性价比是所有精度优化里最高的。
4. 两点校准,把偏移和增益误差一起干掉
4.1 为什么线性校准够用
ADC 转换链路的误差模型,在绝大多数情况下可以近似成一条直线:实际电压 = 斜率 × ADC 原始码 + 截距。斜率偏离理想值的部分就是增益误差,截距不为零的部分就是偏移误差。
只要 DNL 和 INL 没那么离谱,用两个标准电压点拟合出这条直线,就能对整个量程做修正。这个方法简单、有效,是工业仪表的基本功。如果需求特别高,可以在量程内多取几个标准点做分段线性校准,每段拟合一条直线,工程上也够用。
4.2 校准实施的前置条件
校准不是拿个电压表随便量一下就开始。有两个关键前提:
第一,标准源精度至少比目标精度高一个数量级。目标是 10mV 以内,标准源至少要是 1mV 级别,最好用 4 位半以上的万用表,或者一个经过验证的精密电压源。
第二,校准只对当前配置有效。换了采样时间、换了参考电压设置、换了芯片,都需要重新校准。校准系数最好和配置参数绑定存储,不要修了一个采样周期结果旧的校准系数还在用。
4.3 完整计算过程示例
我用一个 12 位 ADC、量程 0~3.3V 的项目演示。选两个校准点,一个靠近量程低端,一个靠近高端。这里以某块板子的实测数据为例:
- 输入 0.500V 时,ADC 原始码读数 620
- 输入 3.000V 时,ADC 原始码读数 3750
注意,理想情况下 3.000V 应该在 3723 左右(3.000/3.3×4095),实际读到 3750,说明这一段偏正;0.500V 理想值 620.5,实际 620,偏移也不明显。单看某一个点会觉得"还行",但两个点连起来斜率已经偏了。
求斜率和截距:
k = (3.000 - 0.500) / (3750 - 620) = 2.500 / 3130 ≈ 0.0007987 V/码
b = 0.500 - k × 620 = 0.500 - 0.4952 ≈ 0.0048V
校准公式:V = 0.0007987 × Code + 0.0048
用这个公式,读码 2048 时电压 = 1.6409V,而理想值是 1.65V,差了 9mV 左右。别小看这 9mV,12 位分辨率才 0.8mV,这就是精度和分辨率的差距所在。不做校准,这块板的显示值永远多偏一点。
代码实现非常简单,几乎不占资源:
typedef struct { float k; float b; } adc_cal_t; adc_cal_t g_cal = {0.0007987f, 0.0048f}; float adc_to_voltage(uint16_t code) { return g_cal.k * code + g_cal.b; }系数在产线标定后写入非易失存储,每次上电读出来用就行。追求省事也可以用整数定点计算,但 float 在现代单片机上完全不是负担。
4.4 校准之后该注意什么
校准是在特定温度和电压下做的,温度变了校准系数会漂。我之前做过一个户外设备,冬天和夏天的读数差出来 1% 左右,后来在低温环境下重新跑了两点校准,问题就压下去了。
另外,校准修正不了 INL 的非线性,所以别期待中间每个点都像校准点一样准。如果中间段实测误差还是超出需求,考虑三点或五点分段校准。分段之间的系数要做线性插值,否则切换区间时会看到台阶跳变。
还有一个容易忽略的点:校准系数的有效范围只在输入量程内。超过满量程输入,ADC 已经饱和到 4095,再用线性公式算会得出超出量程的电压值,软件里要做上下限钳位。
5. 三类采样场景的高频坑与排查顺序
5.1 电池电压监测:不能把 Vref 当成永远稳定
电池监测项目的问题往往不是采样电路,而是系统电源。大部分单片机的 Vref 和 ADC 模拟电路共用主电源轨,锂电池电压从 4.2V 掉到 3.4V 的过程中,Vref 跟着变,ADC 的"尺子"就一直在变。结果读数可能显示"电压没怎么降",因为量程也跟着缩小了。
这类项目要么用外部基准独立供电,要么做一个折中方案:每轮采样先读内部基准通道,用内部基准的已知电压反推当前 Vref,再对后面的采样值做归一化。虽然麻烦一点,但比加一颗基准芯片便宜。
5.2 NTC 温度采样:高阻分压网络的连锁问题
NTC 电阻随便都是 10k~100k 级别,和匹配电阻分压后直接进 ADC,信号源内阻非常高,配合前面说的采样电容充电不足,读数会系统性偏低。更麻烦的是,NTC 在高阻区每变化几度电阻才动几百欧,读数不准直接导致温度算错好几度。
解决方案优先级,我认为是:加运放缓冲 > 降低分压电阻阻值 > 提高 ADC 采样时间。同时要注意 NTC 自热效应,流过的电流太大,电阻自身发热,读出来的温度会偏高。分压总电流控制在 0.1mA 以下比较稳妥。
5.3 电流传感器小信号:先放大再送 ADC
大量程电流传感器的输出信号往往只有毫伏级,直接进 ADC 等于浪费了分辨率。正确做法是先经过运放放大到接近满量程,再进入 ADC。这时候运放的失调电压和温漂就成了新主角,选低失调的运放比追求 ADC 位数更重要。
信号放大的倍数不是越大越好。放大到让最大输入信号对应 ADC 的 90%~95% 量程,留一点裕量防过冲,比较合适。另外电流采样电阻要选温漂低的合金电阻,普通贴片电阻的温漂会让小信号跟随温度乱飘。
5.4 踩坑后的标准排查链路
当你遇到"读数明显不对"时,不要一上来就改校准系数。我的排查顺序固定如下:
| 排查项 | 方法 | 典型发现 |
|---|---|---|
| 输入引脚实际电压 | 万用表探头直接接触 ADC 引脚 | 引脚电压和预想一致,但读数不对 → 问题在后面 |
| 采样时间配置 | 把采样保持周期调到最大再读一次 | 读数明显上升 → 是高内阻充电不足 |
| 参考电压实测 | 万用表量 Vref 引脚 | 比标称低几十毫伏 → 参考源问题 |
| 校准系数 | 用标准源两点复查 | 低点准、高点偏 → 增益误差 |
| PCB 漏电 | 洗板后复测 | 读数漂移 → 助焊剂残留 |
这套链路救过我至少三次。最近一次是某块板读数偏高 8%,查到最后是参考电压去耦电容虚焊,重新焊好后一切正常。如果我先去调软件,估计要折腾一整天。
6. 每次上电先看一眼基准通道
最后分享一个我这两年固定下来的习惯:每次系统上电,先对内部基准电压通道做一次 ADC 采样,把原始码记录下来。如果这块板子正常工作时的基准原始码应该在 2100 左右,某天突然变成 2070,那就说明参考电压明显漂了,后续所有测量结果都要打折看待。
这个方法的原理很简单:内部基准电压本身是芯片相对准确的一个物理量,它的 ADC 读数变化能侧面反映 Vref、采样电路、甚至芯片本身的状态。不需要额外硬件,就是一个软性自检。
我之前有个项目在低温环境读数整体偏高 2% 以上,查了很久最后就是靠基准通道的读数变化才定位到参考电压温漂和分压电阻的温度系数叠加。从那以后,带有 ADC 的固件里我基本都会保留这个开机自检记录,并且在连续多次读数异常时给出提示。
做 ADC 项目这几年,我最大的体会是:这类问题大多数时候不是某一个点彻底坏了,而是采样时间、输入阻抗、参考电压、滤波策略这些细节各自偏差一点点,最后叠出一个明显错误的结果。把开头那套"采样原理到两点校准"的链条过一遍,按顺序排查,九成问题都能在你怀疑人生之前解决。