1. 先把账算清楚:12位ADC的误差到底从哪来
手里这颗国产 N32 系列 MCU,Cortex-M4 内核带 FPU,主频跑到 96MHz 甚至 144MHz,片上 12 位 ADC 标称采样率能上 4MSPS 级别,怎么看都像是能直接干活的料。可真正把板子焊回来、程序跑起来,很多人第一反应是"这 ADC 是不是坏了"——输入短接到地,读数在 0 到 8 之间跳;输入接 1.5V 基准,读数在 1860 到 1885 之间晃。折算下来有效位数不到 10 位,做电池电压检测勉强能用,做电流环、做小信号采集就直接崩了。这里说的"高精度采样实战",不是把数据手册的参数抄一遍,而是把这颗 ADC 从"能读"榨到"可信"的整个过程:硬件上怎么把噪声和源阻抗按住,软件上怎么把时钟、采样时间、触发、滤波、标定这几环配平。
先说清楚这件事解决什么问题。工业现场常见的需求是:4-20mA 环路采样、电池包单体电压监测、电机相电流采样、NTC 温度采集、小信号称重传感器读取。这些场景的共同点是——信号幅度小、环境噪声大、要求长期稳定而不是单次准确。12 位 ADC 理论分辨率是 VREF/4096,按 3.3V 参考算就是 0.806mV/LSB。但"分辨率"和"精度"完全是两回事,分辨率是尺子上的刻度,精度是你量出来的数和真值差多少。一颗 12 位 ADC 在没做任何处理的情况下,实际有效位数(ENOB)经常只有 8 到 9 位,剩下的 3 到 4 位全被噪声和误差吃掉了。
这篇文章适合谁看:手上已经有 N32 系列开发板、能把 ADC 跑出数来,但读数不稳、精度不够、不知道怎么往下优化的工程师;也适合正在选型、想知道这颗 MCU 的 ADC 到底能做到什么水平的硬件负责人。全文的实操基于常见实践给出,具体寄存器名和库函数名请以对应型号的参考手册为准,不同子系列(G4xx、L4xx、G43x 等)在 ADC 时钟上限、通道数量、是否支持硬件过采样上有差异,这一点后面会专门讲怎么查。
1.1 一张误差预算表看清瓶颈
在做任何优化之前,我习惯先列一张误差预算表,把所有误差源按量级排个序,然后从最大的那个开始砍。不然很容易陷入"我在软件里加了一堆滤波,结果发现根源是电源纹波"这种无效劳动。下面这张表是我在一套 3.3V 供电、12 位、VREF 取 VDDA 的典型系统上整理的,数值是量级估计,不同板子会有出入,但相对关系基本成立。
| 误差来源 | 典型量级(LSB) | 是否可校准 | 主要对策 |
|---|---|---|---|
| 量化误差 | ±0.5 | 否(理论极限) | 过采样 |
| 参考电压噪声与纹波 | 1 ~ 4 | 部分 | 基准源、去耦、LC 滤波 |
| 电源/地弹跳 | 1 ~ 3 | 否 | 地平面、单点接地、隔离 |
| 采样保持建立不足 | 2 ~ 10 | 否 | 降源阻抗、加长采样时间、加跟随器 |
| ADC 自身失调与增益误差 | 3 ~ 15 | 是 | 两点标定 |
| PCB 串扰与数字耦合 | 1 ~ 5 | 否 | 走线隔离、远离开关电源 |
| 温度漂移 | 1 ~ 3(全温区) | 部分 | 定期重标定、比率测量 |
| 内部参考电压偏差 | 2 ~ 5(若用内部基准) | 是 | 用 VREFINT 反推 |
从表里能看出一个很关键的事实:失调和增益误差是最大的单项,但它恰好是唯一能通过标定彻底消掉的。而采样保持建立不足这一项经常被忽略,很多人在输入端串了一个 100kΩ 的分压电阻,然后抱怨 ADC 读数偏低且随信号变化,本质上就是采样电容在有限的采样窗口里没充到位。这两个点,一个靠软件,一个靠硬件,构成了整篇文章的主线。
还要提醒一句,表里没有列"ADC 时钟超规格"这一项。因为一旦超了,误差就不是几 LSB 的问题,而是直接读到垃圾数据。N32 各子系列对 ADC 时钟上限的要求不一样,有的型号 ADC 时钟最高 28MHz,有的 14MHz,必须在参考手册的 ADC 章节里确认清楚,再倒推分频系数。我见过有人把 PCLK2 设成 96MHz,分频直接给 2,ADC 跑在 48MHz,读数漂得离谱,查了两天才发现是时钟超了。这种坑,写在手册第一页,但就是容易漏。
1.2 为什么我把重心放在硬件而不是算法
很多人一上来就想用算法解决精度问题——多采几次取平均、加个卡尔曼滤波、上滑动窗口。这条路在噪声是白噪声的前提下确实有效,平均 N 次能把随机噪声压低到 1/√N。但问题是,嵌入式 ADC 前面那堆误差里,真正像白噪声的只有一小部分。电源纹波是周期性的(开关频率固定),串扰是确定性的(跟数字翻转同步),建立不足是信号相关的(跟输入电压大小有关),这几种误差你平均一万次也平均不掉,它们只会被原封不动地保留下来,甚至因为平均窗口把有用信号也磨平了。
我做过一个很直观的对比实验:同一个信号源输入 1.200V,一路是"硬件原样 + 软件 256 次平均",另一路是"硬件加 RC 滤波和跟随器 + 软件 256 次平均"。前者读数标准差约 3.5 LSB,后者约 0.8 LSB。也就是说,硬件上多花两个电阻一个电容外加一颗几毛钱的运放,效果比软件多跑 250 次循环还明显。这个结论不是否定软件滤波的价值,而是说软件滤波只能处理随机噪声,硬件设计决定了误差的下限。顺序不能反。
所以我的整体思路是三步走:第一步,硬件上把源阻抗、参考电压、地回路这三件事做对,把误差压到 5 LSB 以内;第二步,软件上把 ADC 时钟、采样时间、触发链路配对,保证每次采样都在同样的条件下完成,把确定性误差变成可重复的;第三步,用标定把剩下的失调和增益误差消掉,用定期读取内部参考电压的方式跟踪温漂。这三步走完,一颗 12 位 ADC 做到 ±2 LSB 的综合误差是可以期待的,折算到 3.3V 就是 ±1.6mV。
2. 硬件侧的第一件事:让采样保持电路"喂得饱"
ADC 的内部结构说白了就是一个采样开关加一个采样电容。采样阶段开关闭合,外部信号通过源阻抗给采样电容充电;保持阶段开关断开,电容上的电压送去比较器做逐次逼近。这里有个硬约束:采样窗口的时间必须足够长,让采样电容充电到目标电压的 1/2^(N+1) 以内,N 是位数。12 位的话就是 1/8192,约 0.012%。如果充不到这个程度,读数就会系统性偏低——注意是偏低,因为充电总是从低往高爬,永远追不上真值。这个"偏低"在小信号时尤其明显,表现就是输入电压越高误差越小,输入电压越低误差越大,看起来像是非线性,实际上是建立不足。
2.1 源阻抗与采样时间的硬约束怎么算
公式不复杂,工程上用的一阶近似是:
t_sample ≥ (R_source + R_adc) × C_adc × ln(2^(N+1))
N=12 时 ln(2^13) ≈ 9.01。R_adc 是内部采样开关的导通电阻,手册里一般给几十欧到几百欧,取 1kΩ 做保守估计;C_adc 是采样电容,典型 4pF 到 8pF,取 8pF 保守。这样:
- 源阻抗 1kΩ:(1000+1000) × 8p × 9.01 ≈ 144ns
- 源阻抗 10kΩ:(10000+1000) × 8p × 9.01 ≈ 793ns
- 源阻抗 100kΩ:(100000+1000) × 8p × 9.01 ≈ 7.3μs
对照一下采样时间档位。假设 ADC 时钟 16MHz,一个周期 62.5ns。可选的采样时间一般是 1.5、7.5、13.5、28.5、41.5、55.5、71.5、239.5 个周期这几档(具体档位以手册为准)。1.5 个周期只有 94ns,源阻抗 1kΩ 时理论刚好够,实际必须要留 3 到 5 倍余量,因为公式里的 R_adc 和 C_adc 都是典型值,工艺离散、温度变化都会让它们跑偏,而且还没算 PCB 上的寄生电容。
我的经验做法是:按理论值的 5 倍留余量,然后向下取最近的可用档位。源阻抗 10kΩ 时理论 793ns,5 倍就是 4μs,16MHz 下需要 64 个周期,那就选 71.5 周期这一档。源阻抗 1kΩ 时理论 144ns,5 倍 720ns,需要 11.5 个周期,选 13.5 周期档。这个余量看着浪费,但换来的是全温区全批次的一致性,非常值。
注意:采样时间拉长的代价是最大采样率下降。总转换时间 = 采样时间 + 12.5 个周期的转换时间。选 71.5 周期档、16MHz 时钟时,单次转换是 84 个周期 = 5.25μs,即单通道最高约 190kSPS。如果应用需要更高采样率,就必须降源阻抗,而不是硬压采样时间。
2.2 分压电阻、跟随器与开尔文接法
电池电压检测是最典型的例子。48V 电池包要测单体电压,信号必须分压到 3.3V 以内。常用的分压比是 1:16 或 1:11,电阻取值要在"功耗"和"源阻抗"之间折中。假设分压比 1:11,上电阻 100kΩ、下电阻 10kΩ,源阻抗就是并联值约 9.1kΩ。按上面的算法,采样时间要选 71.5 周期档才稳妥。如果为了省电把上电阻提到 1MΩ,源阻抗变成约 100kΩ,就得选 239.5 周期档,转换时间拉到 15.75μs,采样率掉到 63kSPS。这时候就得上运放跟随器了。
跟随器的选型有几个硬指标不能马虎:输入偏置电流要小(pA 到 nA 级),否则在 1MΩ 源阻抗上会产生明显压降;失调电压要低(几十 μV 级),因为 1 LSB 才 806μV,失调 5mV 就是 6 个码;要轨到轨输入输出,否则在接近 0V 和 3.3V 的位置会失真;带宽不用太高,但压摆率要够。这类运放市面上选择不少,选型时把这些参数卡住就行。
跟随器接上去之后,源阻抗从 100kΩ 变成运放的闭环输出阻抗(几欧到几十欧),采样时间可以直接回到 13.5 周期档甚至 7.5 周期档,采样率一下就上去了。而且跟随器还顺带把分压网络和 ADC 采样电容隔离开——采样瞬间的电荷注入不会反过来扰动分压节点,这一点在多通道轮询时特别重要,否则通道之间会互相"串味"。
电流采样用的是另一种思路:毫欧级采样电阻串在回路里,两端电压差只有几十毫伏,必须用差分放大或者专用电流检测放大器。这里有个细节很多人不注意——采样电阻的引线必须用开尔文接法,也就是电压检测线从电阻焊盘的内侧单独引出,不要和电流路径共用走线。共用走线的结果是走线上的压降被一起放大了,20A 电流下走线电阻 0.5mΩ 就是 10mV 的误差,折算到采样电阻上可能是 10% 的偏差。
2.3 参考电压和VDDA的供电细节
ADC 的参考电压决定了刻度的绝对准确性,它的噪声会一比一地传到结果里。如果 VREF 上有 1mV 的纹波,12 位 ADC 的读数就会跟着抖 1.24 个码。用 VDDA 直接当参考是最省事的做法,但前提是 VDDA 得干净。我的做法是:VDDA 引脚先串一颗磁珠(或 10Ω 电阻),再并 10μF 钽电容 + 100nF 陶瓷电容 + 10nF 高频电容,三级去耦。磁珠的作用是把数字电源上的高频噪声挡在外面,10Ω 电阻的方案更适合低频噪声但会带来压降,需要算一下 ADC 的供电电流。
如果应用对绝对精度要求高,比如要测 0.1% 级别的信号,那就别用 VDDA 当参考了,直接上外部基准源。外部基准一般两三百微安的静态电流,精度能做到 0.1% 以内,温漂几个 ppm。接线时基准的输出电容按手册要求选,不要自己乱加,有些基准对容性负载敏感,容值不对会振荡。基准的地要回到模拟地,不要就近接到数字地上,这一点后面讲地回路的时候会展开。
还有一种折中方案:用内部参考电压通道反推 VDDA 的实际值。N32 系列一般都有一个内部参考电压通道(VREFINT),标称值 1.2V 左右,出厂时会在特定条件下校准并给出校准值存在信息区。每次测量时顺带读一次这个通道,用"VREFINT 校准值 / 实测值"算出当前 VDDA 的真实电压,再去修正其他通道的结果。这个方法的巧妙之处在于,参考电压的误差被自洽地抵消掉了,不需要外部基准也能做到不错的绝对精度。代价是要多花一个通道和一个采样周期,而且要确认校准值是在什么条件下测的(一般是 25℃、VDDA=3.3V)。
3. 硬件侧的第二件事:PCB上那些不起眼但要命的细节
硬件设计里最容易出问题、也最难定位的其实是 PCB。原理图上的连接关系都是对的,元件参数也都是算过的,但板子一上电就是有噪声,怎么调软件都没用。这种情况十有八九是地回路或者走线耦合的问题。我见过一块板子,ADC 读数里叠加了一个 100kHz 的周期性纹波,峰峰值 8 个码,查了两天,最后发现是 DC-DC 的电感离模拟走线只有 3mm,磁场耦合过去了。把电感转了个方向、挪了 5mm,纹波直接掉到 1 个码。
3.1 地平面与回流路径
模拟地和数字地要不要分开,这是个老话题。我的观点是:在中小规模的板子上,不要机械地"分割地平面",而要做"分区布局 + 单点连接"。具体做法是整板保留一个完整的地平面,然后用布局把模拟器件和数字器件物理隔开,让模拟信号的回流电流自然地在模拟区域流动,不要被迫绕到数字区域去。ADC 芯片本身跨在两个区域之间,把它放在分界线上,AGND 引脚朝向模拟侧,DGND 引脚朝向数字侧。
真正需要"分割"的场景是:板上有大功率开关电路(电机驱动、大电流 DC-DC),开关瞬间的地弹跳可能上百毫伏。这时候用 0Ω 电阻或者磁珠把模拟地和功率地单点连起来,连接点选在电源入口的滤波电容负极附近,这样功率回流不会流经模拟区域。注意是单点,不是多点,多点连接等于没分割,还会形成地环路。
ADC 下方的地平面要完整,不要被过孔和走线打碎。输入走线的下方必须有连续的地,这样信号的回流路径就在正下方,环路面积最小,抗干扰能力最强。我见过有人在 ADC 输入端走线下面开了一个槽(为了隔离),结果信号的回流被迫绕一大圈,环路面积变大,反而更容易拾取噪声。这种"看起来更隔离、实际更糟"的设计,在做 EMC 的时候会集中爆发。
3.2 抗混叠滤波与走线隔离
ADC 前面要不要加 RC 滤波,取决于信号带宽。抗混叠滤波的目的是把高于奈奎斯特频率的成分滤掉,防止它们折返到有用频段里。就算采样率不高,一个简单的 RC 也有好处——它能滤掉高频噪声,还能给采样电容提供一个局部的电荷储备,改善建立特性。
参数怎么定?截止频率取信号最高频率的 3 到 5 倍,或者取采样率的 1/10 以下。举例:采样率 10kSPS,信号带宽 1kHz,那 RC 截止频率取 2kHz 到 3kHz 比较合适。R 取 1kΩ,C 取 68nF,截止频率 = 1/(2π×1000×68e-9) ≈ 2.34kHz。注意这个 R 会加到源阻抗里,算采样时间的时候必须把它算进去——1kΩ 的滤波电阻加上分压网络的 9.1kΩ,源阻抗就变成 10.1kΩ 了。这是个很常见的疏漏。
走线隔离方面,几条经验:模拟输入走线尽量短、尽量直,不要和 SPI、I2C、PWM 这些数字线平行走超过 5mm;如果必须交叉,走成垂直的;模拟走线两侧包地,包地要打过孔到主地平面,间距小于 λ/20(对 100MHz 大概是 15cm,实际按 5mm 打孔足够);晶振、DC-DC、电机驱动这些噪声源要远离模拟区域,至少 10mm 以上。
3.3 一个反例:省掉磁珠之后的实测对比
说个真实踩过的坑。有一版板子为了省成本和面积,把 VDDA 前面的磁珠换成了 0Ω 电阻,觉得反正都是直通。结果同一套固件、同一个信号源,读数标准差从 1.2 LSB 涨到 4.7 LSB,而且能看到明显的 500kHz 周期性成分——正好是板上 DC-DC 的开关频率。换回磁珠,问题消失。这个实验说明,磁珠在这里不是"锦上添花",而是实打实的分水岭。
顺便说一下磁珠怎么选。要看两个参数:100MHz 时的阻抗(一般 600Ω 到 1kΩ 够用)和额定电流(要大于 ADC 供电电流的 3 倍以上,避免饱和)。直流电阻也要看,太大的话会带来压降,影响 VDDA 的实际值。如果 VDDA 电流只有几毫安,选 600Ω/100mA 的规格就够,直流电阻通常几十欧以内,压降可以忽略。
4. 软件配置:时钟、采样时间与触发链路
硬件把底子打好之后,软件的任务就变成了"保证每次采样都在完全相同的条件下完成"。这句话听起来简单,做起来涉及时钟树、采样时间、触发源、DMA 搬运、校准时机好几个环节。任何一个环节不一致,都会引入确定性误差,而确定性误差是最讨厌的——它不会因为平均而减小,反而会在数据里形成固定偏移或者规律性波动。
4.1 ADC时钟与采样时间的确定过程
第一步查手册确认 ADC 时钟上限。前面提过,不同子系列不一样,有的 28MHz,有的 14MHz,这个必须查。假设查到的上限是 28MHz,系统 PCLK2 是 96MHz,那分频系数最小只能取 4,得到 24MHz,留了一点余量。为什么不取 6 得到 16MHz?因为采样率高一点有好处,能给我们更多的平均次数。但如果应用对噪声敏感,把时钟降下来反而更好——ADC 内部的开关噪声和时钟频率正相关,16MHz 下的噪声通常比 24MHz 低一些。这是个取舍,我在电流环这种要求快的场合用 24MHz,在电池监测这种要求稳的场合用 12MHz 甚至 8MHz。
采样时间的确定按第 2 节的公式来,算完留 5 倍余量再取档位。这里有个实操细节:不同通道的源阻抗可能不一样,理论上应该给每个通道配不同的采样时间。但大多数 MCU 的 ADC 在扫描模式下所有通道共用同一个采样时间配置,只有部分型号支持逐通道配置。如果不支持,就按最差的那个通道来配,宁可慢一点。
/* ADC 初始化骨架:以 ADC 时钟 16MHz、采样时间 71.5 周期为例 * 具体寄存器名/位域名请对照所用量产型号的参考手册 */ void adc_hw_init(void) { /* 1. 时钟使能:先开 DMA、ADC、GPIO 的时钟 */ RCC_EnableAHB1Periph(RCC_AHB1_DMA1); RCC_EnableAHB2Periph(RCC_AHB2_ADC1); RCC_EnableAHB1Periph(RCC_AHB1_GPIOA); /* 2. ADC 时钟分频:PCLK2=96MHz,分频 6 -> 16MHz * 务必确认该型号 ADC 时钟上限,超了读数不可信 */ RCC_SetADCClockDiv(RCC_ADCCLK_DIV6); /* 3. GPIO 配成模拟输入,不要配成浮空输入 */ GPIO_InitAnalog(GPIOA, GPIO_PIN_0 | GPIO_PIN_1); /* 4. 上电后先做一次自校准,校准期间不能启动转换 */ ADC_Enable(ADC1, DISABLE); ADC_SetCalibration(ADC1, ADC_CALIB_START); while (ADC_GetCalibrationStatus(ADC1) == SET) { } }这段代码里有两个点值得多说。第一,ADC 引脚必须配成模拟模式,配成浮空输入的话,引脚上的施密特触发器还在工作,会带来额外的漏电流和输入电容,虽然不一定致命,但没有理由留着。第二,校准要在 ADC 上电稳定之后做,一般要求上电后延时 1ms 以上,等内部参考建立好,校准才有意义。校准过程会测量内部失调并写入校准寄存器,之后所有转换都会自动扣掉这部分失调。
4.2 定时器触发 + DMA 的配置骨架
为什么不用软件轮询或者连续转换模式?软件轮询的问题是采样间隔不均匀,而间隔不均匀会导致混叠特性变差——本来设计好的抗混叠滤波器是按固定采样率算的,采样间隔一抖,滤波器就失效了。连续转换模式虽然间隔固定,但你没法精确控制它和 PWM、和其他外设的相位关系,在多通道同步或者和功率级联动的场合会很别扭。
定时器触发 + DMA 是标准答案。定时器产生精确的更新事件,触发 ADC 启动一次转换;转换完成后 DMA 自动把结果搬到数组里,CPU 完全不参与。这样采样间隔由定时器决定,精度是时钟级的,抖动只有几十纳秒;CPU 在后台处理数据,也不会因为中断响应延迟影响采样时序。
/* 定时器触发 ADC + DMA 搬运的配置思路(伪代码,按库函数映射) */ void adc_trigger_dma_init(void) { /* DMA:循环模式,外设地址是 ADC 数据寄存器,内存是缓冲区 */ DMA_InitType dma; dma.channel = DMA_CH1; dma.periphAddr = (uint32_t)&ADC1->DAT; dma.memAddr = (uint32_t)adc_raw_buf; dma.bufSize = ADC_BUF_LEN; /* 建议 64 或 128,2 的幂 */ dma.dir = DMA_PERIPH_TO_MEM; dma.periphInc = DISABLE; dma.memInc = ENABLE; dma.circularMode = ENABLE; /* 循环,自然形成滑窗 */ dma.periphDataSize= DMA_PDATA_16BIT; /* ADC 结果 16 位右对齐 */ dma.memDataSize = DMA_MDATA_16BIT; DMA_Init(DMA_CH1, &dma); /* ADC:外部触发选定时器 TRGO,扫描多通道,DMA 使能 */ ADC_SetExternalTrig(ADC1, ADC_EXTTRIG_TIM1_TRGO, ADC_EXTTRIG_RISING); ADC_SetScanMode(ADC1, ENABLE); ADC_SetDMAContinuous(ADC1, ENABLE); ADC_Enable(ADC1, ENABLE); /* 定时器:1kHz 触发频率,更新事件输出到 TRGO */ TIM_SetPrescaler(TIM1, 96 - 1); /* 96MHz / 96 = 1MHz */ TIM_SetPeriod(TIM1, 1000 - 1); /* 1MHz / 1000 = 1kHz */ TIM_SelectOutputTrigger(TIM1, TIM_TRGO_UPDATE); TIM_Enable(TIM1, ENABLE); }缓冲区长度我建议取 2 的幂,64 或 128。原因有两方面:一是后面做滑动平均的时候,2 的幂可以用移位代替除法,省几个周期;二是 DMA 的半传输中断正好把缓冲区劈成两半,可以做双缓冲处理,一半在采、一半在处理,不会打架。这个技巧在采样率高的场合非常关键,能避免"处理数据的时候 DMA 还在覆盖"这种偶发的数据错乱。
4.3 校准与首采样丢弃
有个细节容易被忽略:扫描模式下,通道切换后的第一次采样是不准的。原因是 ADC 内部有一个多路选择开关,从通道 A 切到通道 B 时,内部的寄生电容还残留着 A 的电压,第一次采样是"带着上一通道余温"的,误差可能有几个到几十个 LSB。解决办法是把第一次采样丢掉,从第二次开始取。
具体做法有两种。一种是在扫描序列里给每个通道配两次采样,DMA 搬完之后在处理阶段隔一个取一个,代码简单但浪费一半采样率。另一种是利用 ADC 的"采样延时"或者手动在切换通道后先做一次空转换,把结果丢弃。如果你的型号支持逐通道采样时间配置,还可以给需要切换的通道配一个特别短的第一次采样时间,让丢弃的代价小一点。
/* 隔点取样的后处理:raw 里是 [ch0_dummy, ch0, ch1_dummy, ch1, ...] */ #define CH_NUM 2 uint16_t ch_value[CH_NUM]; void extract_channels(const uint16_t *raw, uint16_t *out, uint16_t frames) { for (uint16_t i = 0; i < frames; i++) { for (uint8_t c = 0; c < CH_NUM; c++) { /* 每个通道第 2 个样本才是有效的 */ out[c] = raw[i * CH_NUM * 2 + c * 2 + 1]; } } }还有一种更省事的做法:上电初始化阶段,把每个通道都连续转换 10 次,结果全部丢弃,让内部节点电压"预热"到稳定状态,之后正常扫描时首采样误差就很小了。这个方法不能完全消除误差,但能把几个 LSB 压到 1 LSB 以内,成本几乎为零,我在很多项目里都这么干。
5. 数字后处理:过采样、滤波与标定
硬件和底层配置都到位之后,最后一段是数据处理。这一段的目的是把剩余的随机噪声压下去,把系统误差标定掉。注意顺序不能乱:先降噪,再标定。反过来的话,标定系数会被噪声污染,标出来的结果不可靠。
5.1 过采样到底能提几位
过采样的理论是:每增加 4 倍采样次数并做平均,信噪比提升 6dB,等效分辨率提升 1 位。16 倍过采样理论上能提 2 位,256 倍能提 4 位。但这个结论有个严格前提——噪声必须是白噪声,而且在信号带宽内均匀分布。如果噪声是周期性的(比如开关电源的纹波),过采样完全无效,甚至会因为采样点固定落在纹波的同一相位上而让误差固化下来。
实际能做到什么程度?我做过一组测试,在硬件已经优化到位(RC 滤波 + 跟随器 + 磁珠去耦)的前提下,单次采样的标准差约 1.2 LSB;16 倍平均后约 0.35 LSB;64 倍平均后约 0.2 LSB;再往上就没什么收益了,因为已经接近量化噪声的极限(±0.5 LSB)。所以我的经验值是:过采样倍数取 16 到 64 之间,超过 64 就是浪费 CPU。
/* 累加平均:N 必须是 2 的幂,用移位代替除法 */ #define OS_N 16 #define OS_LOG 4 uint16_t oversample_avg(ADC_Type *adc, uint8_t ch) { uint32_t sum = 0; for (uint8_t i = 0; i < OS_N; i++) { ADC_StartConvert(adc, ch); while (!ADC_IsConversionDone(adc)) { } sum += ADC_GetResult(adc); } return (uint16_t)(sum >> OS_LOG); /* 结果仍然是 12 位量纲 */ }如果想要更高分辨率,可以做"累加不右移",也就是把 12 位结果累加成 16 位甚至 18 位,保留小数部分。比如 16 次累加后结果用 16 位存,相当于多出 4 位小数,后续做滤波和标定的时候不会被整数截断反复引入误差。这个技巧在做小信号的时候效果明显,尤其是信号只占满量程的 10% 以内的场合。
5.2 滑动平均和中值滤波怎么搭配
单纯的滑动平均有个问题:它对突变响应慢,而且如果数据里混进了孤立的野值(比如一次 DMA 错位、一次电源抖动),这个野值会污染整个窗口,要等窗口滑过去才恢复。所以在滑动平均前面加一级去极值,也就是"去掉最大值和最小值再平均",或者干脆用中值滤波。
我的常用组合是:5 点中值 + 8 点滑动平均。中值滤波负责干掉野值,滑动平均负责压平随机噪声。计算量上,5 点中值用插入排序也就十几次比较,8 点滑动平均用环形缓冲加增量更新(加新值减旧值)只有两次加减,对 M4 内核来说开销可以忽略。
/* 环形缓冲 + 增量滑动平均,O(1) 更新 */ #define WIN 8 static uint32_t ring[WIN]; static uint8_t idx = 0; static uint32_t sum = 0; static uint8_t filled = 0; uint16_t sliding_avg(uint16_t new_val) { sum -= ring[idx]; ring[idx] = new_val; sum += new_val; idx = (idx + 1) & (WIN - 1); if (filled < WIN) filled++; return (uint16_t)(sum / filled); }这里有个细节要注意:窗口长度要和信号变化速度匹配。窗口太长,响应慢,做电流环的时候会引入相位滞后,可能引起振荡;窗口太短,噪声压不住。经验值是:窗口覆盖的时间常数取信号最快变化周期的 1/5 到 1/10。比如被测信号最快 10ms 变化一次,窗口总时长取 1 到 2ms 比较合适,1kHz 采样率下就是 1 到 2 个点,那就只能靠中值滤波和硬件滤波了。所以采样率要留足余量,宁可高采样率 + 长窗口,也不要低采样率 + 短窗口。
5.3 用内部参考反推VDDA做补偿
如果 VREF 用的是 VDDA,那 VDDA 的波动会直接变成读数误差。假设 VDDA 从 3.300V 漂到 3.270V,同一个 1.000V 输入,读数会从 1241 变成 1252,差了 11 个码,相当于 9mV 的误差。这个量级在电池监测里是不可接受的。
解决办法是定期读内部参考通道。N32 系列一般有一路内部参考电压接到某个固定通道,标称 1.2V 左右,出厂校准值存在信息区。读它的实际转换值,反推当前 VDDA:
VDDA_actual = VREFINT_CAL × VREFINT_CAL_VALUE / VREFINT_MEASURED
其中 VREFINT_CAL_VALUE 是出厂校准值(通常是在 VDDA=3.3V 下测得的 12 位结果),VREFINT_MEASURED 是当前实测值。算出 VDDA 之后,把其他通道的结果按比例修正:
V_signal = ADC_VALUE × VDDA_actual / 4096
这个方法的好处是不需要外部基准,成本为零,精度能到 1% 以内。缺点是内部参考本身的温漂和长期漂移会引入误差,而且校准值的测试条件(温度、VDDA)跟实际使用条件不完全一致。如果应用要求 0.1% 级别的绝对精度,还是得老老实实用外部基准。
还有个更巧的用法:如果测量的是比率型信号——比如电阻分压、电桥、电位器,它们的输出和激励电压成正比,而激励电压正好就是 VDDA——那 VDDA 的波动在分子分母上会互相抵消,根本不需要补偿。电池电压检测就是这种情况:分压比固定,读数直接反映"电池电压相对于 VDDA 的比例",只要 VDDA 稳定在允许范围内,结果就是准的。理解这一点能省掉很多不必要的补偿代码。
5.4 两点标定
标定是消灭失调和增益误差的最有效手段。原理很简单:给两个已知的输入电压,读出两个 ADC 值,解一个二元一次方程组,算出实际增益和偏移。线性模型是:
ADC_reading = gain × V_input + offset
两点标定:
gain = (ADC2 - ADC1) / (V2 - V1) offset = ADC1 - gain × V1
标定点怎么选?理论上是越分散越好,我一般取满量程的 10% 和 90%。为什么不取 0% 和 100%?因为太靠近轨的地方,运放和 ADC 都可能进入非线性区,标定出来的系数反而不准。10% 和 90% 这两个点在线性区中间,可靠性高。
标定时的输入电压要用高精度源,最好比目标精度高一个数量级。比如目标精度 0.1%,那标定源至少要 0.01%。标定过程中要等读数稳定,一般采 100 次平均再记录下来,避免把噪声标进系数里。标定系数存到 Flash 或者 EEPROM 里,上电时读出来用。如果产品要过温漂,最好在高低温箱里做多点标定,拟合出温度系数,运行时按片上温度传感器做补偿——不过这一步的工作量就大了,一般消费类产品做到两点标定就够。
6. 实测数据与调优记录
说了这么多方法,最终还是要看数据。下面这组数据是在一块四层板、3.3V 供电、12 位 ADC、VREF 取 VDDA 的平台上测的,信号源用的是 1.200V 的精密基准,采样率 1kSPS,每组统计 1000 个样本,看标准差和最大偏差。测试过程中只改一个变量,其他条件保持不变,这样才能看出每一项措施的实际贡献。
6.1 逐项改进的对比表
| 阶段 | 改进内容 | 标准差(LSB) | 最大偏差(LSB) | 备注 |
|---|---|---|---|---|
| 基线 | 直接采样,无任何处理 | 11.8 | 47 | 能明显看到 500kHz 纹波 |
| 1 | VDDA 加磁珠 + 三级去耦 | 4.7 | 22 | 纹波基本消失 |
| 2 | 加 RC 抗混叠滤波(1k + 68nF) | 3.1 | 15 | 高频噪声明显下降 |
| 3 | 加运放跟随器,源阻抗降到 50Ω | 1.9 | 9 | 建立不足问题解决 |
| 4 | 采样时间从 13.5 周期改到 71.5 周期 | 1.4 | 6 | 余量更足,一致性更好 |
| 5 | 16 次过采样平均 | 0.42 | 2 | 接近量化极限 |
| 6 | 5 点中值 + 8 点滑动平均 | 0.31 | 1 | 野值被完全消除 |
| 7 | 两点标定 | 0.30 | 1 | 系统性偏差清零 |
从表里能看出几个有意思的现象。第一,最大的单次收益来自电源去耦,从 11.8 LSB 降到 4.7 LSB,一项就吃掉了大部分误差。这说明在多数实际板子上,电源噪声就是主要矛盾。第二,跟随器带来的收益比 RC 滤波还大,从 3.1 降到 1.9,因为源阻抗从 9.1kΩ 降到了 50Ω,采样建立问题彻底解决。第三,软件滤波的收益在硬件优化之后才显现,如果基线那一版直接上 16 次平均,标准差大概只能从 11.8 降到 8 左右,因为噪声里有大量非白成分。
换算成电压:最终 0.30 LSB 的标准差,在 3.3V 参考下是 0.24mV;最大偏差 1 LSB 是 0.81mV。对于 1.2V 输入来说,相对误差约 0.067%,这个水平用来做电池监测、温度采集、一般的电流环完全够用。如果要求 0.01% 级别,就得上外部 16 位以上的 ADC 了,12 位 ADC 的天花板就在这里。
6.2 温度与长时间漂移观察
静态精度测完之后,我还做了一组温漂测试:把板子放进温箱,从 -20℃ 升到 70℃,每 10℃ 保温 20 分钟,等热平衡后记录同一个输入电压下的读数。结果是没有做标定时,读数在 -20℃ 到 70℃ 之间的漂移约 12 LSB(主要是内部参考和运放失调的温漂);做了两点标定之后漂移降到 5 LSB 左右;再叠加内部参考反推 VDDA 的补偿后,能压到 3 LSB 以内。
长时间漂移方面,连续通电 72 小时,每 8 小时记录一次,读数漂移在 1 LSB 以内。这个结果说明系统是稳定的,主要漂移来自温度而不是老化。如果你的应用运行环境温度变化大,建议每隔一段时间(比如每 10 分钟)读一次内部参考通道,动态修正 VDDA 的估计值,这个成本很低,收益不小。
还有一点值得提:ADC 自身的失调会随温度变化,而且这个变化不是线性的。所以如果应用对精度要求高,最好在温度变化大的时候触发一次重新校准。N32 的 ADC 校准可以随时执行,只要当前没有转换在进行。我的做法是在初始化时校准一次,然后在检测到温度变化超过 20℃ 时再校准一次,两次校准之间的误差在可接受范围内。
7. 常见问题速查与避坑心得
调试 ADC 的过程中会遇到各种各样的问题,大部分有个共同特征:现象看起来像是"精度不够",但根本原因五花八门。下面这张表是我这些年攒下来的,按现象分类,希望能帮你少走点弯路。
7.1 现象-原因-对策速查表
| 现象 | 最可能的原因 | 排查方法 | 对策 |
|---|---|---|---|
| 读数末尾 3~5 位持续跳动 | 电源噪声、参考不稳 | 短路输入到地,看是否还跳 | 加磁珠和去耦,必要时上外部基准 |
| 输入电压越高误差越小 | 采样保持建立不足 | 把采样时间加长一倍再测 | 降源阻抗、加跟随器、加长采样时间 |
| 多通道轮询时通道间互相干扰 | 通道切换首采样未丢弃 | 单通道测试是否正常 | 丢弃切换后第一次采样 |
| 读数整体偏低固定值 | 失调误差 | 输入 0V 看读数是否为 0 | 上电自校准 + 两点标定 |
| 读数随温度明显变化 | 基准温漂、运放失调温漂 | 温箱测试 | 内部参考反推 + 定期重校准 |
| 读数有固定频率的周期纹波 | DC-DC 或 PWM 耦合 | 关掉 DC-DC 看纹波是否消失 | 调整布局、加屏蔽、改采样相位 |
| 采样率上不去 | 采样时间配太长 | 算总转换时间 | 降源阻抗后缩短采样时间 |
| DMA 数据错位 | 缓冲区被覆盖 | 检查半传输中断处理 | 双缓冲、加大缓冲区、提高处理优先级 |
| 读数偶尔出现极大野值 | 电源瞬态、DMA 错位 | 加中值滤波看是否消失 | 去极值平均、检查电源瞬态 |
| 校准后精度反而变差 | 校准条件不对 | 检查校准前是否上电稳定 | 上电延时 1ms 以上再校准 |
7.2 几条踩坑心得
第一条,先确认输入信号本身是干净的。我遇到过一次,ADC 读数抖得厉害,查了半天硬件和软件,最后发现是信号源本身有 20mV 的纹波。用示波器直接夹在 ADC 引脚上看一眼,比自己猜半天强。养成习惯:任何"ADC 不准"的怀疑,第一步都是拿示波器看引脚上的实际波形,交流耦合、20mV/div、看百毫伏级的细节。
第二条,不要迷信数据手册的 ENOB 指标。手册上写的有效位数通常是在理想条件下测的——干净的电源、低阻抗源、合适的采样时间。实际板子上的 ENOB 会比手册低 1 到 2 位,这是正常的。真正有价值的是自己在实际板子上测出来的数据,那个才是你能依赖的。
第三条,采样时间和源阻抗要一起算,不能分开看。很多人配采样时间的时候只看手册推荐值,不看自己的源阻抗。源阻抗 100kΩ 配 13.5 周期采样,理论上就差了好几倍,实测会发现读数偏低且非线性。这个坑我在早期项目里踩过,后来养成习惯:原理图定稿的时候就把每个 ADC 通道的源阻抗算出来,写在设计文档里,软件配置的时候直接对着抄。
第四条,内部参考通道的校准值有使用条件。有的型号的校准值是在特定 VDDA 和温度下测的,如果实际使用条件差别大,直接套用会有偏差。用之前翻一下手册的电气特性表,确认校准条件,必要时自己做一次单点修正。
第五条,DMA 缓冲区的对齐和大小要注意。有些型号的 DMA 对内存地址有对齐要求,缓冲区没对齐会导致传输错位,读出来的数据一半是上一个通道的。这个问题很隐蔽,因为现象看起来像是"通道串扰",很容易往模拟方向去查。建议缓冲区用__attribute__((aligned(4)))显式对齐,长度取 2 的幂。
第六条,也是最容易被忽略的一条:ADC 的精度是整条链路的结果,不是 ADC 一个人的事。从传感器、运放、RC、PCB、电源、参考、ADC 内部、数字处理,任何一环掉链子,最终读数就不准。所以调试的时候要从源头开始,一段一段往下查,不要一上来就怀疑 ADC 芯片本身。我至今遇到的"ADC 不准"案例里,真正是芯片问题的不到 5%,剩下 95% 都在外围。
最后分享一个我在实际项目里固定下来的开发流程:硬件设计阶段就把每个 ADC 通道的源阻抗、信号带宽、采样时间需求列成表;打样后在固件里加一个"诊断模式",能单独输出原始采样值、过采样后的值、滤波后的值、标定后的值四个层次的数据,用串口打到上位机画波形;调优的时候一次只改一个参数,记录数据。这套流程走下来,通常两三天就能把一个通道调到接近理论极限,比盲目试参数快得多。这套东西后来被我固化成了一个模板工程,新项目直接拿来改参数就行,省了大量重复劳动。