
ADC这活儿说不难是真不难ADC外设配置好读取寄存器就是个电压值。但做硬件时间长了你会发现真正让ADC从“能出数”到“数得准”中间隔着一条叫“标定”的河。尤其是做电池电压监测、传感器采集、精密仪器这类场景ADC的原始转换结果直接拿来用十有八九会出问题。这期“硬件基础”系列就集中聊清楚一件事ADC电压采集的软件校准也就是标定和拟合具体怎么做为什么非做不可以及怎么做才能不过拟合、不白折腾。1. 先搞清楚ADC的误差从哪里来很多刚接触ADC的朋友容易有一个错觉觉得ADC是12位、16位分辨率够高读出来的数字就一定能精确反映真实电压。实际上ADC转换结果和真实物理量之间永远存在系统性偏差。不把这个偏差模型想明白软件校准就是无源之水。1.1 增益误差与失调误差两个大坑ADC的误差模型绝大多数情况下可以简化为一条直线真实电压 增益系数 × ADC读数 失调量这就是标定和拟合的理论基础。其中增益系数对应的是“增益误差”失调量对应的是“失调误差”。增益误差通俗点说就是ADC把1V看成0.98V、把2V看成2.03V电压越高偏差越大。它主要来自ADC内部参考电压的不准确也可以来自外部采样电路里分压电阻的容差。失调误差则是ADC在输入电压为0的时候转换结果并不是0而是带了一个固定偏置。很多芯片内部的放大器存在输入失调电压十几毫伏到几十毫伏都很常见。这个偏置是固定的电压越高它也在电压为零时它也在。所以做软件校准的核心任务就两个把这个直线的斜率纠正过来把这个直线的截距挪回原位。这两个系数确定了大部分误差就消化掉了。1.2 非线性、温漂等隐藏误差源增益和失调是最主要的误差来源但工程实践中还有几个隐藏的“敌人”需要心里有数。非线性误差是比较难缠的问题。ADC的转换曲线并不是绝对直线尤其接近满量程和接近零点的两端线性度通常较差。入门级的MCU内置ADCINL积分非线性做到正负几个LSB已经算不错了。如果追求高精度单纯做两点校准就不够需要做多点分段甚至多项式拟合。温漂是最容易被忽略的。放大器的失调电压会随温度漂移参考电压源也有温度系数。这意味着在25摄氏度下校准好的系数到60摄氏度的工作环境中可能又偏了。对于宽温工作场景校准系数需要按温度点分组存储或者用包含温度项的补偿模型。还有电源噪声和参考电压纹波。ADC的参考电压如果有纹波相当于测量基准本身在抖动这部分误差不管怎么做软件校准都无法消除。所以硬件上给VREF加足够的去耦电容甚至用专门的基准源芯片是软件校准能奏效的前提。1.3 为什么不能只靠「更高级的ADC」糊弄过去不少人有这种想法既然内置ADC精度不行那我换一颗16位甚至24位的外部ADC不就行了吗这个思路在某些场景确实有用但问题在于成本、面积、功耗和开发复杂度的增加。况且外部ADC也有自己的增益误差、失调误差和温漂。哪怕用了24位的Σ-Δ ADC芯片数据手册里的初始精度通常也只有“典型值”真正保证精度还是得靠系统级校准。反倒是把一片普通的12位MCU内置ADC用好、校准好在很多场景下完全够用能省不少成本。我在实际项目中见过不少板子MCU几乎只用了内置ADC通过合理的分压电路加软件两点校准把电压测量精度做到了0.1%以内。这已经是相当实用的精度水平了。2. 常见校准方案分析与选择软件校准根据标定点的数量和拟合方式通常可以分成三个层次单点校准、两点校准、多点拟合。每一层都有它的适用场景也有它明显的局限。2.1 单点校准怎么用又在什么时候翻车单点校准的逻辑很简单认为ADC的失调为零只校准增益。做法是给ADC输入一个已知电压测出ADC读数值然后用已知电压除以读数得到校准系数。在某些环境里这种做法是能用的比如被测电压本身比较稳定、工作在固定电压附近而且ADC的失调误差可以忽略。但“失调误差可以忽略”这个前提在实际芯片上经常不成立。只要失调不为零单点校准就会在偏离校准点的地方产生明显的偏差。我举个例子。假设某芯片的ADC在输入为0V时读数为50个LSB在校准点1.2V处读数为2405个LSB。单点校准计算出的增益系数是1.2V除以2405约等于0.000499V/LSB。但真实情况下输入为0.6V时ADC读数大约是1205 LSB按这个系数换算出来的电压是0.601V看着还行。可如果把输入电压拉到接近满量程比如3.0V误差就可能被放大到接近失调电压的满量程比例倍数这部分误差直接没法接受。结论是单点校准只能在精度要求不高、测量范围窄、失调可接受时使用。在通用电压采集场景里我建议至少做到两点校准。2.2 两点校准性价比最高的主流方案两点校准的做法是选两个已知电压点分别记录ADC读数然后用这两个点确定一条直线反推增益和失调。具体计算很简单。设两个标定点电压为V1和V2对应的ADC读数为D1和D2则增益系数 G (V2 - V1) / (D2 - D1)失调系数 B V1 - G × D1后续任何ADC读数D都可以用校准后的电压值 V G × D B 来还原。两点校准能够同时消除增益误差和失调误差对标定点的要求也不高只要标准电压源精度足够几分钟就能完成校准。实际工程中我会选一个接近测量范围低端的点和一个接近高端的点尽量覆盖实际工作区间。2.3 多点拟合与分段查表高精度场景的进阶手段当ADC的非线性误差不可忽略时两点校准校正完剩下的残差可能还在几个LSB甚至十几个LSB的量级。这时候就需要多点拟合了。多点拟合最朴实耐用的是分段线性法。把测量范围分成N段每段分别做线性校准段与段之间形成一张查表。这种方式不需要高阶数学每一段内部的线性度足够好误差就能被压制得很低实现起来也简单。另外一种是多项式拟合比如最小二乘法拟合一条二次或三次曲线。好处是模型平滑、没有分段跳变缺点是系数计算复杂度高而且如果选取的拟合阶次过高很容易出现过拟合问题在校准点之间的区域内反而产生更大的误差。这点后面单独展开讲。3. 实操标定过程、代码实现与参数验证理论讲清楚了接下来的重点就是如何在真实硬件上跑通全套标定流程得到可用的校准系数并写进固件里。3.1 标定前的硬件准备与检查做软件标定前先花五分钟确认硬件状态否则后面校出来的系数也没有参考价值。先用万用表实际测量ADC引脚的电压而不是完全相信信号源的显示值。信号发生器的显示值未必准确专业一点的还会先拿六位半万用表去读基准电压。确认参考电压稳定。VREF脚上的退耦电容必须靠近引脚摆放如果有条件用LDO单独供电比直接接在MCU的电源上要干净得多。参考电压的纹波直接影响ADC读数的跳动软件滤波可以降低噪声但无法修正系统性偏差。接下来开始采集数据。以我现在最常用的STM32F103内置ADC为例标准流程是配置ADC为单通道、软件触发模式12位分辨率外部输入一个已知电压比如用高精度稳压源输出0.5V连续采样64次取平均作为该电压点对应的ADC读数依次采集若干个标定点记录成一张表采集中需要注意的一点是每次切换输入电压后要等电压稳定1到2秒再开始采样。尤其前端有RC滤波网络时电容充放电需要时间一上来就读数往往偏低或偏高。3.2 最小二乘法线性拟合的代码实现两点校准只需要一个初中生都会的直线方程但多点拟合求最佳直线就要用到最小二乘法了。核心思想是找到一条直线使得所有标定点到这条直线的误差平方和最小。它能把测量中随机噪声的影响均摊掉避免两点校准被某一个噪声点带偏。假设有N个标定点每个点记为(D_i, V_i)最小二乘法给出的增益系数和失调系数计算公式为斜率 G (N × Σ(D_i×V_i) - ΣD_i × ΣV_i) / (N × ΣD_i^2 - (ΣD_i)^2)截距 B (ΣV_i - G × ΣD_i) / N这个公式在代码里实现起来非常直接。这里给一个在C环境里求最小二乘线性拟合系数的函数#include stdint.h typedef struct { float slope; // 增益系数 G float offset; // 失调系数 B } calib_coef_t; // data[] 为ADC读数voltage[]为对应的真实电压count为标定点数量 calib_coef_t linear_fit(const uint32_t *data, const float *voltage, uint8_t count) { float sum_d 0.0f, sum_v 0.0f; float sum_dv 0.0f, sum_d2 0.0f; for (uint8_t i 0; i count; i) { float d (float)data[i]; float v voltage[i]; sum_d d; sum_v v; sum_dv d * v; sum_d2 d * d; } calib_coef_t coef; float denominator count * sum_d2 - sum_d * sum_d; if (denominator 0.0f) { coef.slope 0.0f; coef.offset 0.0f; return coef; } coef.slope (count * sum_dv - sum_d * sum_v) / denominator; coef.offset (sum_v - coef.slope * sum_d) / count; return coef; }校准系数求出来之后MCU运行时只需要做一次乘法和一次加法。考虑到嵌入式平台上浮点运算的开销有些场景会用定点数把G和B放大成整数运算但对现代Cortex-M系列MCU来说浮点乘法也就是一条指令的事直接用float没有性能压力。3.3 多项式拟合与过拟合的边界控制线性拟合能解决大部分问题但碰到ADC线性度不够好、尤其是高精度应用的时候就需要考虑二次或三次多项式拟合了。一个二次多项式模型长这样V a × D^2 b × D c其中a、b、c三个系数的求解一般用最小二乘的矩阵形式完成也就是解一个三元一次方程组。很多PC端的工具Excel、MATLAB、Python的numpy都能直接算出系数但在单片机上直接实现矩阵求逆并不划算。比较务实的做法是用PC算出系数把浮点系数作为常量存进固件运行时直接代入计算。这里我必须提醒一句过拟合是多项式拟合的大坑。所谓过拟合简单理解就是拟合曲线完美地穿过了所有标定点但标定点之间的行为完全不可控。高阶多项式在端点附近经常出现剧烈震荡这个现象叫龙格现象。比如你用9次多项式去拟合10个点曲线在首尾两端可能会突然往上翘或往下坠校出来的结果反而更差。所以在工程实际中我的经验是标定点个数在5到10个之间时最高用三次多项式已经足够如果标定点接近20个优先考虑分段线性拟合而不是强行提高多项式阶次每增加一个拟合阶次都要用预留的验证点去检查拟合效果的残差3.4 实际项目中的校准数据记录为了让大家对校准效果有个直观的概念我整理了一份在某Cortex-M0芯片上做3.3V系统电压采集的实测记录。前端是两颗1%精度电阻分压分压比3:1ADC参考电压接在MCU的VDD上。标准电压 (V)ADC原始读数 (LSB)线性拟合后计算值 (V)绝对误差 (mV)0.00050.00110.5006220.499-11.00012390.998-21.50018571.497-32.00024741.996-42.50030922.497-33.00037082.998-2这里用的是两点校准的数据0.5V和2.5V两点其余点作为验证点。可以看到在校准点之间的误差控制在正负4mV以内这个精度对于电池电压监测、电源状态检测这类应用已经绰绰有余。4. 常见校准问题与排查技巧这部分分享一下我在调试ADC校准功能时经常碰到的问题、排查思路和一些比较有用的经验。有些坑不踩一次确实很难从书本上学会。4.1 校准后仍有偏差的排查方向问题一校准完低压段准、高压段不准或者反过来。这个现象通常是增益误差没有被完全消除。排查方向是检查分压电阻的精度和温漂、参考电压在不同负载下的稳定性。电阻的温漂系数如果是100ppm/℃板子从常温升到60℃时电阻值会漂移0.35%左右在校准系数上就会留下明显痕迹。问题二校准完零点附近还是有一个固定偏置。如果以0V输入时ADC读数不是0说明失调校准做得不够彻底。可能是校准源在校准零点时本身输出不为零也可能是PCB漏电或者输入引脚附近的焊接残留影响了微弱信号。还有一个常见原因是多路复用采样时相邻通道的电荷注入串扰尤其扫描模式下切换通道后立即读取读数会带上残余电荷。问题三校准完的数值跳动很大。这就是随机噪声问题了。软件侧先做均值滤波连续采16次取平均通常能把跳动压低不少。硬件侧检查VREF和电源引脚的去耦电容容量够不够采样引脚上的RC滤波参数合不合理。有些MCU内置ADC支持硬件过采样比如把4次转换累加再右移2位也能等效提升分辨率。问题四换了一片芯片后校准系数就失效了。这完全正常每个芯片的失调和增益都有差异出厂校准就是用来吸收这个差异的。批量生产时需要每片板子都跑一次标定流程把系数写进EEPROM或Flash的独立区域。4.2 标定过程中容易被忽略的三个细节细节一标定电压点的顺序会影响结果。建议从低到高依次施加电压每次改变输入后等待稳定再进行采样记录。从高往低走时如果前端有大电容放电时间会明显长于充电时间容易在数据表里产生虚假的高点。细节二标定点不要选得太靠近ADC的量程两端。ADC在接近满量程时线性度通常会略差一些如果你把标定点放在满量程的2%和98%处两点连线的斜率可能会被端点区域的非线性带偏。实际操作中把标定点放在量程的10%到90%区间内得到的线性拟合系数在中段的表现更好。细节三校准系数必须带校验。校准系数在量产烧录时如果把Flash写坏了或者EEPROM里读回来的是0xFF运行时的电压就会变成乱七八糟的值。我在这一类系统里通常会在存储区尾部放一个CRC或者简单的和校验初始化时校验失败就直接报错不进入工作状态。4.3 把标定做成量产自动化流程如果产品需要批量生产手工标定是很消耗时间的。比较好的做法是在产测阶段编写一个简单的自动化脚本由PC上位机控制可编程电源输出预定电压同时通过串口或SWD接口读取ADC读数自动计算校准系数并写回芯片。我经历过一个项目手工标定一片板子大概需要3到5分钟后来改成了自动化流程产测时间压到了40秒以内效率提升非常明显。自动化标定过程中还会顺带验证每个标定点的读数是否在合理范围内如果某一片板子的ADC读数明显偏离预期直接判定为硬件故障节省后续排查成本。4.4 校准需要多久做一次固定安装的设备如果环境温度和供电电压都比较稳定出厂校准一次就够了。但对于便携设备或者存在明显发热的产品建议在每次上电初始化时做一次零点校准也就是在有条件的情况下测一次零输入修正失调系数。零点校准只需要一个IO口控制MOS管或模拟开关把ADC输入短接到地成本很低却能显著改善温漂带来的误差。如果硬件上连一个可控的接地通路都没有那就只能在固件里保留自动校准功能定期在特定条件下获取已知电压值并更新系数这样能让长期运行的设备始终保持不错的测量精度。5. 软件滤波与校准配合使用的经验标定解决了系统性误差软件滤波解决的则是随机噪声。两者是互补关系很多项目里把这两件事分开考虑是完全错误的。校准前如果不做任何滤波处理采集到的标定点本身就带噪声拟合出来的直线也会被带偏。我常用的做法是原始数据先经过滑动平均或中值滤波再把滤波后的读数用于校准计算和日常转换。滑动平均窗长取16或32就够太大会让响应变慢系统的实时性会受影响。中值滤波对付尖峰脉冲干扰很有效比如电机启停产生的电磁干扰噪声窗长取5时能去掉最高和最低值保留中间值。ADC的采样周期也需要同步考虑。采样周期太短采样电容没有充分充电读数会偏低采样周期太长又容易混入低频噪声。对高阻源来说把采样周期设置为最大值通常能有效减小误差代价是转换速率下降。实际选择时根据信号源阻抗和所需采样率权衡即可。STM32的ADC采样周期可配置为1.5到239.5个ADC时钟周期面对几十kΩ级别的信号源内阻建议直接选55.5周期以上的档位。另外如果信号本身变化不快比如电池电压、温度传感器输出我会在应用层再做一次一阶低通滤波filtered_value filtered_value * 0.9f raw_value * 0.1f;这个写法非常简单系数0.9和0.1决定滤波强度现场调试时很好调整。滤波后再套用校准公式即可。6. 校准系数的存储与运行机制校准系数算出来之后怎么存、怎么加载、怎么在运行时生效也是工程上必须考虑清楚的环节。6.1 系数存储分区与备份机制我习惯给校准系数单独划分一段Flash或EEPROM区域和应用程序代码分开。这样升级固件时不会把校准数据覆盖掉。如果MCU的Flash支持按扇区擦除选一个独立扇区用于存放校准参数。存储结构建议包含魔数比如0xA5A5A5A5用于识别该区域是否已写入有效校准数据校准系数G和B以float或定点的形式保存校验值可以是CRC16或者简单和校验校准日期、批次号、版本号这类附加信息系统启动时先读取这段数据做校验。校验通过就直接使用校验失败则启用默认系数同时点亮异常指示灯或者上报错误。这套机制能有效防止用户在不知情的情况下带着错误校准数据运行设备。6.2 运行时如何快速套用校准公式校准系数加载到内存之后每次ADC转换完成只需要一行代码就能得到校准后的电压值不同场景下的代码逻辑基本一致float adc_get_calibrated_voltage(uint32_t raw_value) { return calib_coef.slope * raw_value calib_coef.offset; }如果使用定点数避免浮点开销可以把系数放大到int32或Q15格式。本项目里MCU是带FPU的Cortex-M4直接用float没有任何问题。对于没有FPU的Cortex-M0定点化才有实际意义。6.3 上位机辅助标定的优势如果产品有上位机通信接口强烈建议把标定做成上位机引导的半自动流程。上位机负责控制标准电压源、下发指令、接收ADC读数、计算系数、回写芯片。这套流程不仅适用于产线也方便研发阶段反复调整标定策略。我在研发阶段会把标定过程做成Python脚本配合串口和可编程电源一条命令就能自动跑完整个标定流程并直接画出拟合曲线。看到曲线就能直观判断线性度是否满足预期还能快速比较不同拟合方案的残差大小比纯手工记录效率高太多。7. 几个容易忽视的配套问题最后把几个容易被忽视但影响很大的配套问题集中梳理一下。7.1 参考电压的选择MCU内置ADC通常支持内部参考电压和外部参考电压两种模式。内部参考电压精度一般不高温漂系数在50ppm/℃左右已经算不错的了。外部参考电压比如TL431、REF3030这类基准芯片能做到低温漂、低噪声。对测量精度要求高的项目外部基准基本是刚需。考虑成本时注意一个平衡点软件校准可以吸收固定偏移和固定增益误差但吸收不了参考电压的随机抖动和温漂带来的动态误差。校准其实救不了不稳定的基准源。7.2 PCB布局的三个核心要点ADC相关的PCB布局直接决定采样信号的干净程度。有三个要点值得记在笔记本上参考电压的退耦电容要尽量靠近芯片引脚走线短粗直。任何串在VREF路径上的寄生电感都会在采样瞬间造成电压跌落。模拟采样线和数字信号线保持距离尤其是PWM输出线、SPI时钟线、电机驱动线这类快速翻转的信号对ADC输入的影响非常大。实在避不开时在ADC输入引脚前加RC低通滤波截止频率根据信号频率来定功耗和响应速度之间做个取舍。ADC输入引脚尽量不打过孔少走外层如果有铺地区域上下左右包地处理。ADC输入是高阻输入哪怕只有几皮法的寄生耦合都可能把数字噪声带进采样结果。7.3 ADC数据漂移的软件监测写代码的时候可以加一个简单的漂移监测周期性给ADC输入一个已知的稳定电压比如内部参考电压通道或者某个精密分压网络对比当前读数和预期值的偏差。这个偏差超出阈值时主动上报传感器异常或者自动触发重新校准流程。这种方法本质上是一种软件自检机制对长期无人值守的设备尤其有用。比如在野外工作的数据采集设备温度变化剧烈校准系数随时间缓慢漂移如果没有自检机制等发现问题时数据可能已经错了很长时间。从我自己的项目经验来看ADC校准并不是一个“调一次就一劳永逸”的工作。它和硬件设计、软件架构、产线流程都耦合在一起需要系统性地考虑。摆正这个思路后面再去做高精度采集项目就能少走不少弯路了。