模拟信号和数字信号之间这条护城河一直在嵌入式开发里占着特殊位置。很多人写了几年代码天天操作寄存器读ADC值但你要问他这个数值怎么来的、为什么有量化误差、为什么偏偏是逐次逼近型占主流他可能只会说“芯片就是这么工作的”。这篇就把ADC的底裤扒干净从比较器开始一步步搭出一个完整的逐次逼近型ADC再补上实际工程里最常用的STM32配置和滤波思路。看完你会觉得这东西的巧妙程度确实配得上“极其巧妙”四个字。1. 为什么需要ADC模拟世界和数字世界的翻译官单片机再聪明它也只能识别0和1。但现实世界的温度、压力、光照、声音全部是连续变化的模拟量——不是0和1能直接表达的。传感器输出的电压可能是1.253V也可能随着环境波动在1.251V到1.255V之间缓慢漂移。想让MCU“看懂”这个电压就必须有一个器件能把连续电压映射成一串二进制数。ADCAnalog-to-Digital Converter模数转换器干的就是这个翻译工作。你可能觉得这个映射很自然但仔细想一下这里面藏着两个关键问题连续到离散模拟电压是无数个可能值的但数字量只能表示有限个离散状态。8位ADC只有256个刻度16位ADC有65536个刻度。拿有限刻度去描述无限连续必然会有误差——这就是量化误差的根源。时间维度模拟信号随时间连续变化但ADC不可能无限快地采样。每隔一段时间抓一个点把“一段连续曲线”变成“一串离散点”——这就是采样定理处理的问题。理解ADC其实就是理解它怎么解决这两个问题。说透了核心就三个字比较、逼近、量化。2. ADC内部的核心架构从比较器到逐次逼近2.1 一切的起点一个比较器ADC大厦的地基是一个再简单不过的电路——比较器。比较器有两个输入端同相端和反相端输出只有两种状态同相端电压 反相端电压输出高电平同相端电压 反相端电压输出低电平就这么一个“比大小”的电路所有的ADC都是基于它发展出来的。就像盖房子先打地基比较器就是那个地基。从比较器往外延伸最简单的想法是我拿一个已知电压和被测量的输入电压比一下如果输入大输出1如果输入小输出0。这不就是一个1位的ADC吗确实一个比较器外加一个参考电压就是一个1位ADC。虽然只能分辨“大”和“小”两个状态但原理上完全成立。那想要更多位数的分辨率怎么办答案是把参考电压切成很多份逐档去比。这就是“逐次逼近”四个字的来历。2.2 逐次逼近型ADCSAR ADC用二分法快速锁定电压逐次逼近型ADCSuccessive Approximation Register ADC简称SAR ADC是嵌入式世界里应用最广的结构。STM32内部集成的ADC就是SAR型。它的工作过程可以拿“猜数字”游戏来类比裁判心里想了一个0到255之间的数你每次猜一个数裁判只告诉你“大了”或“小了”怎么用最少的次数猜中当然是二分法——先猜128如果裁判说大了就猜64说小了就猜192。每猜一次范围缩小一半。猜8次就能锁定到0到255里的唯一整数。SAR ADC干的正是这件事。它内部有四个核心部件采样保持电路在转换开始前电容开关先连通输入引脚让电容充电到输入电压值然后断开连接靠电容存储住这个电压。因为比较需要时间如果电压一直变化比较结果就没有意义。比较器负责判断“猜测电压”比“输入电压”大还是小。逐次逼近寄存器SAR这是整个模块的逻辑大脑决定下一步猜什么数、存储当前猜测结果。内部DAC把寄存器里的数字猜测值转换回一个模拟电压送给比较器和输入电压对比。整个转换过程是这样的采样阶段开关闭合内部电容阵列充电到输入电压保持阶段开关断开电容上的电压保持住开始猜测先把最高位置1即猜测值是满量程的一半比如8位ADC就是猜128内部DAC把128转换成对应电压和输入电压比较如果输入电压大于猜测电压说明猜小了最高位的1保留如果输入电压小于猜测电压说明猜大了最高位清零然后继续猜下一位同样用二分法接着比较依次处理完8位或者12位、16位寄存器里最终锁存的就是转换结果也就是说每一次比较确定一个bit12位ADC需要12次比较16位ADC需要16次比较。这也是“逐次逼近”这个词的精确含义。2.3 内部DAC的巧妙实现电容阵列权重二分法SAR ADC里最巧妙的部分其实是那个内部DAC。你可能会问数字值怎么变成模拟电压如果每一bit都用电阻网络分压那要匹配精度很高的电阻成本高、误差大。工程师们想出了一个更聪明的办法用电容阵列。每个bit对应一个电容电容的容值按二进制权重排列。比如最高位对应的电容是C次高位是C/2再下一位是C/4以此类推最低位是C/2^(N-1)。所有电容的一端连接在一起接到比较器输入端。采样时所有电容的另一端都接到输入电压上电容充满电。然后开始逐位逼近先断开输入最高位电容的另一端切换到参考电压Vref而其余电容保持接地。这时候比较器输入端的电压会因为电荷再分配而变化。如果输入电压足够高比较器输出高电平最高位确定为1否则最高位确定为0电容切换回接地。这个方案的优雅之处在于电容阵列同时充当了采样保持电路和DAC一个结构干了两件事而且不需要精密电阻只需要电容之间的比例关系准确即可。现代CMOS工艺中电容比例的匹配精度可以做得非常高所以SAR ADC在速度和精度之间取得了极好的平衡。实际中还有一个常见变体叫电荷重分配型SAR ADC工作原理完全一致只是实现细节上对电容阵列做了优化比如加入冗余电容来容忍比较器的失调误差。3. 关键参数看懂ADC的性能指标才算真正会用原理搞清楚了但实际选型和调试时你还会遇到一堆参数。这些参数不是纸面上的数字游戏它们直接影响你的测量结果准不准。3.1 分辨率Resolution分辨率是ADC能分辨的最小电压变化量单位是bit。12位ADC表示能输出2^124096个不同的数字值16位就是65536个。对应的最小电压变化量是最小分辨率 参考电压 / 2^分辨率位数假设参考电压是3.3V12位ADC的最小分辨率就是3.3V / 4096 ≈ 0.0008057V ≈ 0.806mV也就是说输入电压变化小于0.806mV时ADC的输出数字量不会变化。这个值也叫LSBLeast Significant Bit最低有效位。这里有个容易踩的坑很多人以为12位ADC测出来的精度就是0.806mV但这是“分辨率”不是“精度”。精度还受参考电压稳定性、噪声、非线性误差等因素影响。分辨率只代表“刻度有多细”不代表“量得有多准”。3.2 参考电压Vref的稳定度直接决定精度ADC的转换结果本质是输入电压和参考电压的比值ADC值 输入电压 / 参考电压 × 2^N注意如果参考电压本身波动了哪怕输入电压不变ADC值也会跟着变。更麻烦的是这种误差是乘性的——参考电压偏了1%ADC结果就偏1%而且是满量程范围内所有点都偏。这也是为什么高精度ADC电路里参考电压芯片往往比ADC本身还要讲究。你要做12位以上的采样Vref就不能直接从单片机供电引脚拿最好用专门的基准源芯片比如REF3030、ADR4525这类低温漂器件。3.3 采样率Sampling Rate与奈奎斯特定理ADC的另一个核心参数是采样率也就是每秒能采样多少次单位是SPSSamples Per Second或Sa/s。根据奈奎斯特采样定理要无失真地还原一个频率为f的信号采样率至少要大于2f。注意是“大于”不是“大于等于”。工程上一般留3到5倍余量。举一个具体场景你想采集一个频率为1kHz的音频信号理论上采样率需要大于2kHz但实际上建议至少10kHz以上否则还原出来的信号噪声会非常大。这就是为什么音频CD的标准采样率是44.1kHz——人耳能听到的最高频率是20kHz44.1kHz大约是它的2.2倍。STM32F4系列内部ADC的最高采样率大约是2.4MSPS12位分辨率下能做中速采样但要采高频射频信号就远远不够了那需要专门的射频ADC或者高速示波器里的那种ADC芯片。3.4 有效位数ENOB与信噪比SNR这是我个人觉得最实用、也最容易被忽略的参数。ADC数据手册上写的分辨率是“标称分辨率”但实际真正能达到的精度要用ENOBEffective Number of Bits有效位数来评估。ENOB (SNR - 1.76) / 6.02其中SNR是信噪比单位是dB。这个公式的物理含义是每增加1位有效位数信噪比需要提升大约6dB。比如一个16位ADC标称分辨率是16bit但如果它的SNR只有80dB那实际有效位数就是(80 - 1.76) / 6.02 ≈ 13bit也就是说你花16位ADC的钱实际只拿到了13位的真实测量精度剩下3位基本被噪声吃掉了。这个参数在选型时要特别留意。有些低端ADC标称24位但ENOB只有16位开头的24bit数据全是芯片内部的数字滤波器的功劳——不是真实模拟性能。做精密测量时先看ENOB别被标称位数唬住。3.5 采样保持时间一个容易被忽视的工程细节SAR ADC在转换前内部电容需要时间充电到输入电压。如果输入信号源的内阻比较大充电时间常数就大电容还没来得及充满采样开关就断开了这时候测到的电压就会偏低。STM32 ADC的采样时间是可以配置的常见的配置有1.5、7.5、28.5、239.5个ADC时钟周期。如果信号源内阻高就得把采样时间调长。这个参数和ADC时钟频率共同决定了你的最大可靠采样率。我曾经接过一个项目用分压电阻采集电池电压分压电阻用了100k级别。刚开始ADC采样时间用的是默认的1.5周期测量结果比万用表低了差不多2%。后来把采样时间改到239.5周期误差立刻降到了0.2%以内。原因就是采样电容没充满电就断开了分压电阻的内阻太大。4. 从原理到实战STM32上搞定ADC采集与滤波原理说了一堆最终还是要落到代码上。STM32是目前嵌入式开发最常用的平台之一它的ADC外设就是标准的SAR架构。下面用一个最常见场景——单通道DMA多次采样并做软件滤波——把整个流程串起来。4.1 硬件接线的坑首先说接线。ADC输入引脚一般建议接一个0.1uF的电容到地这个电容作用有两个作为电荷池为内部采样电容提供瞬时充电电流滤除高频噪声但注意这个电容不能太大。如果太大了信号源驱动能力不够电容充放电时间变慢反而会影响ADC跟随快速信号的能力。一般0.1uF到1uF之间比较合适。如果信号源本身有较高的输出阻抗比如几十kΩ的传感器最好加一级运放缓冲器用低输出阻抗去驱动ADC输入。这是提高测量稳定性的最有效手段。4.2 STM32 HAL库配置单通道DMA多次采样下面用STM32F407为例展示一个完整的ADC单通道DMA采样配置流程。首先在CubeMX里做以下配置ADC1通道0PA0引脚分辨率12位采样时间改成最大档位比如480周期F4系列里最大的选项开启连续转换模式Continuous Conversion Mode这样DMA可以持续搬运数据DMA设置为循环模式Circular数据宽度半字Half Word因为12位ADC结果是16位对齐的配置好生成代码后在main.c里加这样的逻辑#define ADC_BUF_SIZE 64 __IO uint16_t adc_buf[ADC_BUF_SIZE]; void adc_init(void) { ADC_ChannelConfTypeDef sConfig {0}; // 使能ADC时钟和DMA时钟 __HAL_RCC_ADC1_CLK_ENABLE(); __HAL_RCC_DMA2_CLK_ENABLE(); hadc1.Instance ADC1; hadc1.Init.ClockPrescaler ADC_CLOCK_SYNC_PCLK_DIV4; hadc1.Init.Resolution ADC_RESOLUTION_12B; hadc1.Init.ScanConvMode DISABLE; hadc1.Init.ContinuousConvMode ENABLE; hadc1.Init.DiscontinuousConvMode DISABLE; hadc1.Init.ExternalTrigConv ADC_SOFTWARE_START; hadc1.Init.DataAlign ADC_DATAALIGN_RIGHT; hadc1.Init.NbrOfConversion 1; HAL_ADC_Init(hadc1); sConfig.Channel ADC_CHANNEL_0; sConfig.Rank 1; sConfig.SamplingTime ADC_SAMPLETIME_480CYCLES; HAL_ADC_ConfigChannel(hadc1, sConfig); // DMA配置 hdma_adc1.Instance DMA2_Stream0; hdma_adc1.Init.Channel DMA_CHANNEL_0; hdma_adc1.Init.Direction DMA_PERIPH_TO_MEMORY; hdma_adc1.Init.PeriphInc DMA_PINC_DISABLE; hdma_adc1.Init.MemInc DMA_MINC_ENABLE; hdma_adc1.Init.PeriphDataAlignment DMA_PDATAALIGN_HALFWORD; hdma_adc1.Init.MemDataAlignment DMA_MDATAALIGN_HALFWORD; hdma_adc1.Init.Mode DMA_CIRCULAR; hdma_adc1.Init.Priority DMA_PRIORITY_HIGH; HAL_DMA_Init(hdma_adc1); __HAL_LINKDMA(hadc1, DMA_Handle, hdma_adc1); HAL_ADC_Start_DMA(hadc1, (uint32_t *)adc_buf, ADC_BUF_SIZE); }主函数里通过判断DMA传输完成标志来读取数据uint16_t get_adc_average(void) { uint32_t sum 0; uint16_t i; for (i 0; i ADC_BUF_SIZE; i) { sum adc_buf[i]; } return (uint16_t)(sum / ADC_BUF_SIZE); }这里有个细节DMA在循环模式下会不断写入adc_buf数组所以读取的时候必须一次性读完64个点再累加求平均。如果边采边读可能读到新旧混合的数据平均值会抖动。4.3 常见的3种软件滤波方式DMA多次采样的数据直接使用会有比较大的跳动特别在电源环境较差或者外部干扰明显的场景。工程上常用以下三种滤波方式第一种是上面那种算数平均滤波。优点是简单直接对白噪声的抑制效果不错缺点是对脉冲干扰比如电机启动瞬间的尖峰抵抗力差一个离群值就能把平均值拉偏。N取16到64之间比较常用N太大会让响应变慢。第二种是中位值滤波。对连续采样的N个值排序取中间值作为结果。这种滤波器对抗脉冲干扰效果很好但做排序需要额外的时间和RAM开销。N一般取奇数比如5、7、9。如果数据里偶发尖峰较多中位值滤波效果远好于平均滤波。第三种是滑动平均滤波递推平均。维护一个FIFO缓冲区每采一个新值就把队头弹出、队尾压入然后对新缓冲区求平均。这种方式响应快、输出平滑适用于需要实时性的控制场景。缺点是缓冲区需要常驻RAM而且对异常值的抵抗力也不强。实际工程里我常用“中位值平均”的组合先取5个值做中位值滤波把脉冲干扰剔掉再对多次中位值结果做平均把白噪声压下去。效果比单一滤波器稳健很多。4.4 ADC原始值换算物理量标定与线性化ADC采集回来的是整数工程上要换算成电压、温度、压力这类物理量。最基本的换算公式电压 (ADC值 / 4096) × Vref温度传感器如果输出是线性的比如常见的NTC热敏电阻搭配分压电阻电压和温度的关系往往是非线性的查表或线性插值是常用的办法。举一个NTC测温度的例子假设NTC和10k电阻串联接在3.3V和地之间ADC采集的是NTC两端电压。首先根据ADC值算出NTC当前电阻值Rntc (ADC值 / (4096 - ADC值)) × R分压然后根据NTC厂商提供的B值公式计算温度T 1 / (1/T0 ln(Rntc/R0) / B) - 273.15代入代码实现#include math.h float ntc_temp_celsius(uint16_t adc_val, float R_ref, float R0, float B, float T0_kelvin) { float voltage_ratio (float)adc_val / 4096.0f; float r_ntc R_ref * voltage_ratio / (1.0f - voltage_ratio); float T 1.0f / ((1.0f / T0_kelvin) (logf(r_ntc / R0) / B)); return T - 273.15f; }这里容易踩的坑是温度计算涉及对数函数浮点运算开销较大。如果MCU没有硬件浮点单元比如STM32F103这类Cortex-M3建议用查表线性插值的方式替代否则计算时间太长影响采样周期。5. 反面教材那些我踩过的ADC调试大坑做了这么多年嵌入式ADC相关的坑我踩过不少。很多问题查到最后根因既不是芯片坏了也不是代码有bug而是一些看似无关紧要的细节。5.1 采样值跳动的隐藏祸首Vref引脚噪声有一回做12位ADC采集Raw值上下跳动了十几个LSB怎么滤波都压不下来。示波器一看Vref引脚上有大约30mV的高频纹波频率和板子上某个DC-DC的开关频率一致。原因很简单DC-DC的纹波耦合到了Vref引脚而Vref是所有转换的基准基准在抖转换结果自然会跟着抖。解决思路有两条一是换低噪声的LDO给模拟部分供电二是加LC滤波把Vref引脚的纹波滤干净。从那之后我设计PCB的经验规则是Vref引脚的滤波电容一定要靠近引脚放置最好用0.1uF10uF并联走线尽量短粗。5.2 ADC值“卡死”在满量程输入过压导致的闩锁效应另一个印象深刻的案例客户反馈ADC突然一直输出4095断电重启又恢复正常。排查了很久最后发现是输入信号在系统启动瞬间有超过VDD的过冲电压触发了芯片内部的闩锁效应导致ADC通道短路到参考电压。解决方法是给ADC输入引脚加钳位保护用一个1kΩ串联电阻限流再用两个二极管把输入电压钳位在GND-0.3V到VDD0.3V之间。从那以后所有ADC输入电路我都习惯性加上保护成本几乎为零但能避免不少返工。5.3 DMA数据错位内存地址对齐的教训还有一次用STM32的ADC多通道采集DMA每4个通道做一次“组”传输结果发现通道数据老是错位——CH1的数据跑到CH3的位置上。后来发现DMA设置的Memory Data Alignment配置成了字对齐Word但ADC的数据寄存器是16位的导致DMA搬运时每两个通道的数据拼接成一个32位字寄存器数据偏移了一个位置。把对齐改成半字对齐后问题就消失了。这个问题没有原理上的难度纯粹是对外设寄存器宽度理解不够导致但排查起来真的要花半天。5.4 采样周期不够导致读数偏低回到前面提到的分压电阻问题。当时测得结果偏低排查步骤是这样的先用万用表量ADC引脚的电压确认电压正常再用一个最简单的裸机程序无限循环读ADC不用DMA不用RTOS去读发现还是偏低排除了软件多任务干扰的可能把采样时间从1.5周期改成239.5周期数据立刻正常这个过程的经验是遇到ADC读数偏低第一个优先怀疑采样时间不足特别是信号源带高阻抗的场合。6. 选型思路什么场景选什么ADC最后简单聊一下选型这部分不是硬核原理但对实际项目很有参考价值。ADC的种类不止SAR一种不同应用场景适合不同架构。6.1 低频高精度场景Delta-Sigma ADC如果测量对象是温度、称重、压力这类变化缓慢、但精度要求很高的信号Delta-Sigma ADC是主流选择。典型芯片有HX711称重、ADS1232压力/桥式传感器、MCP3561精密采集。这类芯片的原理和SAR完全不同它用极低的单bit量化器配合数字滤波器做过采样把噪声从信号频带外推走再通过数字滤波把高分辨率结果提取出来。牺牲速度、换取精度是Delta-Sigma的典型特征。实际使用中要注意的是这类芯片的数据输出是需要时序读取的而且对主控的电平转换有时会有要求。比如HX711输出是24位数据用普通IO模拟时序就能读但一定要等它的DRDY引脚拉低后才能读否则会读到中间态数据。6.2 中速通用场景SAR ADCSTM32内置ADC、各类MCU的内置ADC基本上都是SAR架构是嵌入式应用最广泛的ADC类型。它的优势是单次转换时间短微秒级、功耗低、无需外部驱动时钟。12位分辨率满足绝大多数现场应用。6.3 高速场景流水线ADCPipeline ADC比如电机控制里的相电流采样需要几百kHz采样率、视频信号数字化。流水线ADC把多个转换级串联每一级处理几位然后级联拼装出最终的转换结果速度和精度兼得。但高成本、高功耗、高复杂度决定了它不是大众选择。6.4 不同场景的选型速查表场景推荐ADC类型典型代表采样率范围分辨率范围温度/压力/称重Delta-SigmaHX711/ADS1232/MCP356110 SPS ~ 1kSPS16 ~ 24 bit通用MCU电压采集SARSTM32内部ADC/ADS11181kSPS ~ 1MSPS8 ~ 16 bit电网/电能计量Delta-SigmaADE7753/ATM90E361kSPS ~ 8kSPS16 ~ 24 bit高速信号/电机电流PipelineAD9226/ADS62P49几十MSPS ~ 几百MSPS12 ~ 16 bit音频采集Delta-SigmaPCM1808/CS534344.1kSPS ~ 192kSPS16 ~ 24 bit有人会问既然Delta-Sigma精度这么高为什么不全用Delta-Sigma因为速度上不去转换时间通常在毫秒级别而且需要额外的数字滤波器。SAR恰好覆盖了从低频到中频这个嵌入式最常用的区间所以大家平时接触到的“ADC”绝大多数都是SAR。7. 我做了那么多次ADC调试之后的几个心得最后分享几个在实际项目里沉淀下来的习惯不算什么高深理论但能帮你少走很多弯路。第一ADC输入电路一定要预留RC滤波的位置。原理图阶段就把串联电阻和并联电容画上去哪怕先不焊。因为现场总会出现噪声超预期的状况到时候飞线加电阻电容比改板子方便得多。第二校准永远比理论计算更可靠。ADC的理论换算公式只能给出大致值因为电阻精度、基准源漂移、信号源内阻都会引入系统误差。最好的做法是做一个两点校准测一个低端基准电压比如0.1V再测一个高端基准电压比如3.0V然后算出实际增益和偏置在软件里做线性修正。这个方法简单高效能把系统误差压到很小。第三DMA 环形缓冲区 均值滤波是嵌入式ADC采集的黄金组合大部分低速采样场景都可以直接套用。但要注意缓冲区大小要取2的整数次幂这样可以用位运算做环形索引效率高很多。第四别迷信高分辨率。很多人一看到“24位ADC”就激动但你的传感器噪声、电源纹波、布线干扰能提供的真实信噪比往往远达不到24位对应的水平。购买ADC时花大价钱买标称高位数不如把精力花在信号调理和PCB布局上。ADC这个东西表面上看是模拟电路和数字电路的边界地带但理解了SAR的二分法机制之后你会发现它的设计是那么优雅——用最简单的比较器通过极聪明的算法构造出一个看似复杂的转换系统。写代码的人如果能把这一层原理想明白调试驱动的时候心里会踏实得多。希望这篇能帮到你。
