☰
嵌入式ADC转换实战:采样原理、多通道滤波与精度提升全解析
2026/10/27 1:09:39 网站建设 项目流程

调一个多通道电压采集模块,传感器接上去,读回来的数据在正常值上下跳了十几个LSB,不管我怎么调"取平均"都压不下来那几条毛刺。后来把ADC采样的原理和精度参数从头捋了一遍,才发现问题根本不在滤波算法,而在采样保持时间和参考电压上。这篇ADC转换汇总,就是把采样原理、取平均(软件滤波)、精度这三块放在一起讲透:它们不是三步曲,而是同一个采集链路里互相牵制的三个环节。适合刚接触ADC的嵌入式新手,也适合被数据跳变折磨过的老手。我不打算一句一句念手册,而是按自己做过的项目整理:为什么采样周期会影响结果、多通道切换为什么会出现第一笔数据不准、软件滤波究竟该怎么选、12位分辨率到底能信几分。

1. 采样原理拆解:从模拟世界的连续到数字世界的离散

1.1 人话版逐次逼近:ADC是怎么"猜"出电压的

市面上绝大多数MCU内置的都是逐次逼近型ADC(SAR ADC)。这个名字听着唬人,其实原理就是一个二分查找。内部结构有三大件:比较器、可控DAC(通常是一组电容阵列)、逻辑控制单元。

转换过程大概是这样的:先把DAC的满量程设成一半,比如参考电压是3.3V,就先把比较器另一端的电压设为1.65V。如果输入电压比1.65V高,最高位记1,然后下一位电压设为2.475V继续比较;如果输入电压比1.65V低,最高位记0,下一位电压往0.825V的方向试。如此反复12次,12位的结果就出来了。

这个过程有个关键点:每一位比较都依赖采样电容上的电压,而这个电压是在"采样阶段"打进来的。如果采样电容没被充满,后面的每一次比较相当于在"半饱"的电容上做文章,结果自然不准。很多初学者以为ADC不准是算法问题,其实第一道误差在模拟前端就已经埋下了。

1.2 采样周期、采样时间、转换时间:这三个概念别搞混

配过STM32的同学都见过"SMPR寄存器"和"采样周期"这种词,但文档里通常不会解释为什么有这么多可选的周期。我把三个容易混的概念拆开:

概念含义对结果的影响
采样时间(tSAMP)采样开关闭合、给内部采样电容充电的时长太短则采集到的电压偏低,且对源阻抗敏感
转换时间(tCONV)逐次比较每一位所需的时间,通常等于(位数+N)个ADC时钟周期主要影响吞吐率,不影响单次精度
采样周期(Cycles)配置寄存器时写的那串数字,比如1.5、7.5、28.5周期采样时间与转换时间的总和工艺实现

为什么要刻意加长采样时间?因为ADC内部的采样电容一般只有几皮法到几十皮法,但充电回路里除了外部信号源阻抗,还有内部模拟开关的导通电阻。假设信号源阻抗是10kΩ,内部开关电阻按1kΩ算,采样电容按10pF算,一阶时间常数τ≈110ns。要充到12位精度(0.5LSB,相当于误差小于满量程的0.012%),理论上需要至少7个τ以上,也就是约770ns。

这个估算在项目里非常有用。比如你用一个高阻抗的传感器直连ADC引脚,采样时间配成1.5周期,ADC时钟4MHz,采样时间只有375ns,误差接近几个LSB。这时候不是代码问题,是物理上就没充满。解决办法很简单:要么降低ADC时钟、拉长采样周期,要么在传感器和ADC之间加一个运放跟随器,用低阻抗源去驱动采样电容。

1.3 采样定理告诉我们:测快信号要当心混叠

奈奎斯特采样定理是所有ADC工作的铁律:采样率至少要达到信号最高频率的两倍,才能无失真重建。工程上留余量,一般按信号最高频率的5到10倍采。但更隐蔽的问题是混叠:如果信号里有高于采样率一半的成分,这部分会"折叠"回低频段,变成假信号。

举个例子:你在测一个温度信号,板子上有50Hz工频干扰。如果采样率是60Hz,50Hz的信号会被混叠到10Hz,这时候你再怎么滤波也很难把它弄掉,因为它已经被当作真实低频成分了。正确的做法是在ADC前端加一个一阶RC低通,截止频率设得比信号带宽高一些、比采样率低很多。比如信号本身只有10Hz,采样率1kHz,RC截止频率放到100Hz,既能滤掉工频干扰,又不会伤害有用信号。

2. 让ADC好好工作:采样周期、通道切换与触发方式

2.1 配置ADC的第一步:先想清楚你有没有"盲目复制"

很多网上的ADC例程都是"打开ADC、软件启动、死等转换完成、读数据",抄过来用了也没什么问题,但实际项目里往往不是一路信号,而是多路传感器轮询、或者要求定时采样。配置前先问自己四个问题:信号带宽是多少?源阻抗高不高?需要几路通道?采样任务是否要精确等间隔?

如果是低速传感器,比如温度、压力、电池电压,把ADC时钟放到2~4MHz,采样周期选大一点,比如STM32的239.5周期,转换一次也就百微秒级,完全够用。如果信号变化快,比如电流采样、振动信号,就要关注采样率是否满足需求,这时候可以考虑定时器触发+DMA,而不是在主循环里轮询。

2.2 多通道切换:别小看"切换之后的那一跳"

多通道扫描模式下,很多工程师遇到一个诡异问题:每个通道的第一笔数据明显不准,但后面几笔数据正常。原因就是采样电容还停留在上一个通道的电压上,切换通道的瞬间采样马上开始,电容根本没有时间建立到新通道的电压。

解决办法有三个:第一种,启动转换前先切换通道,然后空采一次或者手动延时几个ADC周期;第二种,把扫描模式里的每个通道采样时间尽量配长,给电容足够时间充电;第三种,用DMA的循环模式,采集完丢掉前几个数据。还有一点,如果相邻两个通道的源阻抗差距很大,高阻通道对低阻通道的串扰会很明显。PCB布局上尽量让模拟走线短而粗,通道间的耦合电容别跨越采样保持电路。

2.3 TIM触发、DMA和中断:什么时候用哪个

这是个老生常谈却又经常配错的问题。简单说:

  • 软件触发:适合采集一次读一次、对时序无要求的场景。
  • 定时器触发(比如STM32的TIM触发ADC):适合PMSM电机控制、逆变器、需要每PWM周期同步采样的场景。定时器更新事件一发生,ADC立刻启动,采样时刻精确,不和主循环的随机性挂钩。
  • DMA:多通道连续采集、高速数据流必须用。ADC每转换完一个结果就请求DMA搬运到内存,CPU只在DMA传输完成中断里处理一次数据,避免频繁进中断。
  • 普通ADC中断:适合单通道、转换频率不高的场合,图省事。

NXP的S32K312这类汽车级MCU也有类似机制,PDB模块触发ADC,思路和TIM触发一模一样。如果你之前只会软件触发,建议换成定时器+DMA的组合,采集数据的抖动和毛刺大概率能改善一圈。

2.4 调试时怎么把ADC波形画出来

TI生态里,Code Composer Studio可以用Graph工具直接看ADC数组。把DMA搬运到的数组名填进去,选择数据长度和数据类型(16位无符号),Graph模式选"Single Time",立刻就能看到整段采集曲线的形状。这个方法比我之前用串口一个个打印数字快得多,尤其是找毛刺、看混叠的时候。

3. 取平均与滤波:软件层面让数据"稳下来"

3.1 为什么不能直接把20次采集取平均

采集回来的数据有噪声,脑子里第一反应就是取平均。但直接平均有个隐患:如果噪声里混入了脉冲干扰,比如开关切换瞬间的尖峰,一个离谱的值会把整段平均值抬高一大截。我自己就吃过这个亏:某次采集到的数据里有一两个值是正常值的两倍,平均完之后读数一直偏高,排查了半天才发现是取平均把异常值"平均"进去了。

所以滤波算法的选择,要看你面对的是什么噪声类型。白噪声用平均最有效,脉冲噪声要先剔除再平均,周期性噪声要先搞清来源,靠软件滤波不一定压得住。

3.2 三种实用方案:滑动平均、去极值平均、EMA

最常用的三件套:

  1. 滑动平均:维护一个长度为N的环形缓冲区,每次采到一个新值就推入,缓冲区满了之后滚动更新,取平均值。适用于噪声平稳、无大尖峰的场合。
  2. 去极值平均:先把N个样本排序,去掉最大最小,再对中间值取平均。对付脉冲干扰立竿见影,代价是内存占用稍多。
  3. EMA(指数移动平均):每次输出只依赖当前输入和上一次输出,权重指数衰减。响应快、代码极短,适合突发噪声。

C语言里,滑动平均和去极值平均可以合并写成一个函数:

#define ADC_SAMPLE_N 7 uint16_t adc_filtered_read(void) { uint16_t sample[ADC_SAMPLE_N] = {0}; uint16_t tmp; int i, j; // 连续采N个点 for (i = 0; i < ADC_SAMPLE_N; i++) { sample[i] = read_adc_channel(0); } // 排序,经典插入排序,N很小,效率无所谓 for (i = 1; i < ADC_SAMPLE_N; i++) { tmp = sample[i]; j = i - 1; while (j >= 0 && sample[j] > tmp) { sample[j + 1] = sample[j]; j--; } sample[j + 1] = tmp; } // 去掉最大最小,取剩余N-2个的平均 uint32_t sum = 0; for (i = 1; i < ADC_SAMPLE_N - 1; i++) { sum += sample[i]; } return (uint16_t)(sum / (ADC_SAMPLE_N - 2)); }

这个实现里排序只是为了去极值,如果N很大,可以改用快速选择,N在7到15之间完全没必要折腾。

3.3 EMA的系数怎么定,和np.convolve()有什么关系

EMA公式是:y[n] = α·x[n] + (1-α)·y[n-1]。α越大,当前值的权重越大,响应越快,但平滑效果越差;α越小,越平滑,但滞后越明显。初学者常问α取多少合适,经验法则是:想等效于N次滑动平均,取α ≈ 2/(N+1)。比如想要大约10个样本的平滑效果,α取0.18左右。

嵌入式里不想用浮点,可以把α换成256为基数的整数表示,比如α=64就表示约0.25的权重:

static uint16_t ema_val; #define EMA_ALPHA 64 // 相当于 64/256 = 0.25 uint16_t adc_ema(uint16_t new_sample) { ema_val = (uint16_t)(((uint32_t)new_sample * EMA_ALPHA + (uint32_t)ema_val * (256 - EMA_ALPHA)) >> 8); return ema_val; }

注意累加时要用uint32_t,别让16位变量溢出,这是新手最容易踩的坑。

如果你是在PC端离线分析ADC数据,用Python的np.convolve()可以直接实现等权滑动平均:

import numpy as np data = np.array([/* 原始ADC数组 */]) window_size = 16 smoothed = np.convolve(data, np.ones(window_size) / window_size, mode='valid')

EMA本质上也是一个卷积,只不过卷积核是无限长的指数衰减函数。用np.convolve传一个指数核也能得到一模一样的输出。这个等价关系在调参时非常有用:我通常先在PC上把整个数据流用不同的窗口长度跑一遍,找到合适的平滑参数,再搬到MCU上写代码,避免在板子上反复试错。

3.4 浮点还是定点:MCU滤波的取舍

如果用的MCU没有硬件浮点单元,尽量别在滤波函数里频繁用float。滑动平均用uint32_t累加,EMA用定点整数乘加,完全够用。如果必须用浮点,也只在最后一步把平均值转成浮点用于显示,不要每采一个样本就做一次浮点除法。

4. 精度的真相:你以为的12位和实际能拿到的12位

4.1 量化误差与LSB:分辨率不等于精度

这是一个很重要的概念:分辨率代表ADC把模拟量分成多少级,精度代表"分出来之后准不准"。12位、参考电压3.3V时,LSB=3.3/4096≈0.8mV,这是最小刻度。理论上量化误差在±0.5LSB,也就是约±0.4mV。但那是理想器件。

实际ADC还有DNL(微分非线性)和INL(积分非线性)。DNL表示每个相邻码之间的宽度和理想宽度差多少,超过1LSB就会出现丢码;INL是整体转换曲线偏离理想直线的程度。这些参数在具体芯片手册上都写着,普通STM32的12位ADC能做到几个LSB的INL已经算不错了。换句话说,你看到12位,意味着读数最多有4096个台阶,但每个台阶"站的位置"并不一定精确。

4.2 有效位数ENOB与信噪比:12位ADC的真实可用位数

判断一个ADC的精度水平,更靠谱的指标是有效位数ENOB,它和信噪比SNR的关系是:ENOB = (SNR - 1.76) / 6.02。

理想12位ADC的SNR约等于6.02×12+1.76≈74dB。实际芯片测试出来往往是65~70dB,算下来ENOB只有10.5~11.3位。如果PCB布局、参考电压、电源纹波处理不好,ENOB掉到9位也不是没可能。

这个认知能帮你少折腾很多无用功。比如你用12位ADC量一个3.3V基准电压,读数在理论值附近跳3个LSB,这是正常的,不必怀疑代码。如果硬要"测得更准",办法不是反复改滤波窗口,而是降低噪声:稳参考、净电源、洗地线。

4.3 硬件层面的精度提升:从参考电压到PCB布局

软件滤波最多压白噪声,系统的绝对精度主要看硬件。

参考电压是最容易被忽略的一环。很多MCU允许直接把参考电压接VDD,VDD上有负载波动,参考电压跟着晃,转换结果就会跳。对精度敏感的场合,用外部基准芯片,比如低噪声串联基准,把基准输出接到VREF+引脚,并在引脚边上放一个1μF左右的低ESR电容。

电源处理上,模拟电源域建议用磁珠加100nF+10μF去耦,小电流模拟电路可以单独用一组LDO供电。PCB布局上,模拟地的铺铜要完整,模拟部分和数字部分避免大面积重叠,单点连接。别小看这一点,我见过两块完全一样的板子,只是地分割不同,ADC噪声差了整整3个LSB。

输入信号路径上,加一个运放跟随器能解决源阻抗的问题,同时在跟随器输出和ADC引脚之间串一个几十到几百欧的电阻,再并一个1~10nF的电容,构成一个低通滤波,截止频率按信号带宽算好后取值。

4.4 软件层面的精度提升:校准、过采样、查表

硬件到位之后,软件还能再榨出一点精度。

校准是最直接的。采集一路接GND的信号得到零点偏移offset,再采集一路已知精确电压(比如2.5V基准)得到满量程增益gain,然后用y_adjusted = (y - offset) × gain做一次线性校正。两个标定点能把失调误差和增益误差同时拉平。

过采样降噪的思路是:如果噪声近似白噪声,每过采样4倍,有效位数增加1位。原来有效位数是10.5位,要变成12位,大约需要16倍过采样。代价是采样率翻好几倍,适合低速信号。

查表法更进阶:用精密电压源给ADC输入一系列已知电压,记录ADC原始读数,建立一张"输出码-实际电压"的查找表,中间值用线性插值。这个方法能同时修正INL和部分刻度误差,缺点是校准成本高,适合产品批量不大的测量设备。

5. 踩坑实录:项目里最常见的问题与排查链路

5.1 数据跳变没有规律:先别急着改滤波,检查参考和电源

遇到ADC数据乱跳,我现在的排查顺序是:万用表测参考电压稳不稳,再看模拟电源纹波,然后看VREF+电容是否靠近引脚。这一串下来,大部分"无规律跳变"都能找到根源——电源噪声或者基准漂移。纯靠软件滤波去压电源噪声是治标不治本,因为噪声来源一直在那里,滤波只是把它变隐蔽。

一个典型案例:某次做采集,读数稳定了几个月,换了一版电源PCB之后突然开始跳,万用表量VDD只有20mV的波动,但基准引脚上叠加了好几毫伏的噪声。查了半天才发现新板的模拟地和数字地没有单点连接,开关电源的电流噪声直接灌进了模拟地平面。把它改成单点连接之后,跳变立刻消失,滤波前后的差别也不大了。

5.2 多通道切换后第一笔值不准:建立时间不足

现象很典型:扫描模式采集4路,每一路的第一个采样点偏高或偏低,后面的点正常。排查思路就是回到采样原理:通道切换后电容要重新建立电压,而第一个采样点根本没有足够的建立时间。

解法按优先级试:增大采样周期,比如STM32从7.5周期提到71.5周期;或者在每次切换通道后让ADC空转一次,丢弃结果;如果源阻抗高,直接加跟随器,效果立竿见影。注意如果是DMA循环模式,要把序列里的第一个数据当作填充数据丢掉,从第二笔开始用。

5.3 转换值整体偏大或偏小:分压电阻和参考电压的锅

电压采样电路用了一个电阻分压,理论算出来应该读出某个值,实际读出来的数系统性偏移了3%到5%。这种情况最先怀疑分压电阻的精度,尤其当电阻是0603封装的常规品种时,1%精度是最常见的。更隐蔽的是,有些板子上的基准内部误差就有±3%,你拿标准万用表去量输入电压,再对照ADC读数,会发现满量程刻度本身就是歪的。

处理方式是两台一校准:先量出真实输入电压和ADC原始码,算出offset和gain,在代码里做一次线性校正,或者用更高精度的外部基准替代内部基准。

5.4 隔离变量:把原始码、滤波值和实际电压一起打印

定位ADC问题,最怕一上来就调滤波。我的习惯是调试阶段把三样东西同时打出来:ADC原始码、滤波后的值、用万用表量到的实际电压。原始码和实际电压一对比,能直接看出是硬件偏差还是软件滤波引起的滞后;滤波值则是用来验证算法有没有改出效果。这个日志法能帮你快速圈定问题在哪个环节。

最后再分享一个组合套路:定时器触发ADC+DMA搬运+环形缓冲,配合中值平均滤波,再加上外部基准和RC低通,这套组合能应对绝大多数低速高精度采集场景。每家的外设名字不一样,但思路是通用的。你如果被ADC数据跳变折磨过,建议从参考电压和采样建立时间查起,而不是先换滤波算法,往往能省掉不少无用功。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询