1. 项目缘起与核心需求拆解
电压测量这件事,看起来简单到不值一提——万用表一搭,读数就出来了。但如果你真要把电压测量做进产品里,做成一个能长期稳定运行、精度可控、成本合理的嵌入式模块,那完全是另一回事。我最早接触这类需求是在一个电池管理项目里,当时需要实时监测多节锂电池的端电压,精度要求做到正负5mV以内,采样率不用太高但必须稳定可靠。一开始想得很简单,STM32自带12位ADC,直接读不就行了?实际一上手才发现,事情远没有那么简单。
STM32的ADC确实是整个测量链路的核心,但围绕它需要解决一系列问题:参考电压从哪来、模拟前端怎么设计、采样时序怎么配置、数字滤波怎么做、校准怎么搞。每一个环节都会直接影响最终的测量精度。这个项目的核心目标就是利用STM32微控制器实现高精度的电压测量,覆盖从硬件电路设计到固件算法实现的完整链路。
适合阅读这篇内容的人,应该是有一定嵌入式基础、正在做或准备做电压采集相关项目的开发者。不管你是做电池管理、传感器信号采集、电源监控还是工业控制,只要涉及到用STM32测电压,这里面的思路和踩过的坑都值得参考。我会尽量把每个环节的“为什么”讲清楚,让你不只是照抄代码,而是真正理解背后的逻辑。
2. 整体方案设计与选型考量
2.1 为什么选STM32的ADC而不是外挂ADC芯片
很多人第一反应是:STM32自带的ADC精度不够,直接上外置的16位甚至24位ADC芯片不就行了?这个思路没错,但要看具体场景。外挂高精度ADC芯片确实能轻松做到16位以上的有效分辨率,但代价是成本增加、PCB面积增大、SPI或I2C通信增加固件复杂度。如果你的测量需求在12位左右(约1mV分辨率@3.3V量程),STM32内置ADC完全够用,关键在于你怎么用它。
我当时的判断逻辑是这样的:先算需求。电池电压范围2.5V到4.2V,要求精度正负5mV。STM32的12位ADC在3.3V参考电压下,理论分辨率是3.3V除以4096,约等于0.8mV。看起来绰绰有余对吧?但这是理论值,实际有效位数(ENOB)通常只有10位左右,也就是有效分辨率约3.2mV。再加上噪声、参考电压漂移、分压电阻误差等因素,实际精度可能只有正负10mV甚至更差。所以关键不在于ADC本身是几位,而在于整个信号链的设计能不能把精度做上去。
2.2 参考电压的选择:内部还是外部
这是影响测量精度的头号因素。STM32的内部参考电压(VREFINT)精度大概在正负10mV左右,温漂也不小,直接拿来当基准做绝对电压测量,精度很难保证。我的做法是外挂一颗高精度基准电压芯片,比如输出2.500V、初始精度正负0.1%、温漂10ppm每摄氏度的型号。这样ADC的满量程就是2.5V,所有测量都相对于这个基准。
这里有个细节要注意:基准电压芯片的输出需要加去耦电容,而且走线要尽量短、尽量粗,远离数字信号线。我见过有人基准芯片放在板子这头,STM32放在那头,中间走了一根细长的线,结果测出来的数据跳得厉害。后来把基准芯片挪到ADC引脚旁边,加了一颗1微法和一颗100纳法的电容,数据立刻就稳了。
2.3 模拟前端的信号调理
电池电压最高4.2V,超过了ADC的2.5V满量程,所以需要分压。分压电阻的选择有讲究:阻值太大会导致ADC采样电容充电不足,阻值太小又会持续消耗电流。STM32的ADC输入阻抗不是无穷大,采样时内部采样电容需要从外部电路充电,如果外部等效阻抗太大,充电时间不够,采样值就会偏低。
我一般用两个精度0.1%、温漂25ppm的电阻做分压,比如100kΩ和150kΩ,分压比是150除以250等于0.6,这样4.2V分压后是2.52V,刚好在满量程附近。但100kΩ和150kΩ并联后的等效输出阻抗是60kΩ,这对STM32的ADC来说偏大了。解决办法是在分压点后面加一个电压跟随器,用运放做缓冲,把输出阻抗降到毫欧级别。运放选低失调电压、低偏置电流的型号,比如失调电压在100微伏以内的,这样引入的误差可以忽略。
3. 核心细节解析与实操要点
3.1 STM32 ADC的关键参数配置
STM32的ADC有几个关键配置项直接决定测量质量,我逐个说。
采样时间。这是最容易被忽视的参数。STM32的ADC采样过程分两个阶段:采样阶段和转换阶段。采样阶段就是内部采样电容充电的过程,采样时间设置得越短,电容充电越不充分,转换结果越偏离真实值。STM32允许配置的采样周期从1.5个ADC时钟到239.5个ADC时钟不等。具体需要多少个周期,取决于外部电路的等效阻抗。有个经验公式:采样时间至少要满足5倍RC时间常数。STM32的采样电容大约是4pF,如果外部等效阻抗是60kΩ(不加运放缓冲的情况),RC时间常数是60kΩ乘以4pF等于240纳秒。5倍就是1.2微秒。如果ADC时钟是12MHz,一个周期是83纳秒,那至少需要15个周期。所以选28.5周期或41.5周期比较稳妥。加了运放缓冲后,输出阻抗降到1Ω以下,用1.5周期都绰绰有余。
分辨率与对齐方式。STM32的ADC支持12位、10位、8位、6位可配置分辨率。分辨率越高转换时间越长,但12位和10位的时间差很小,建议直接用12位。数据对齐方式有左对齐和右对齐,一般用右对齐,读出来的值直接就是0到4095。
连续转换还是单次转换。如果要做多次采样求平均,用单次转换模式配合DMA或者中断,采一次读一次。如果要做高速连续采集,用连续转换模式配合DMA搬运。我一般用定时器触发单次转换,这样采样率精确可控,也方便做同步采样。
扫描模式与多通道。如果要测多路电压,开启扫描模式,配合DMA把多个通道的转换结果依次搬到数组里。注意扫描模式下每个通道的采样时间可以单独配置,但顺序必须和规则组的配置一致。
3.2 数字滤波与数据处理
ADC读出来的原始值一定是有噪声的,哪怕硬件做得再好,最后几位也会跳。数字滤波的目的就是把这些随机噪声压下去,同时尽量不引入额外的延迟和失真。
最简单的办法是多次采样求平均。比如连续采16次,去掉最大值和最小值,剩下14个求平均。这样能有效抑制突发噪声。但要注意,如果信号本身在快速变化,求平均会引入滞后。对于电池电压这种慢变信号,滞后无所谓,可以放心用。
进阶一点可以用一阶低通滤波,公式是:新值等于旧值乘以(1减去alpha)加上当前采样值乘以alpha。alpha越小滤波效果越强但响应越慢。我一般取alpha等于0.1到0.2之间,具体看信号变化速度。
再讲究一点可以用中值滤波加滑动平均的组合。先做5点中值滤波去掉脉冲噪声,再做8点滑动平均平滑随机噪声。这套组合拳下来,数据基本能稳到正负1个LSB以内。
3.3 校准:让精度真正落地
出厂校准是保证精度的最后一道关卡。STM32的ADC存在增益误差和偏移误差, datasheet上给的精度指标是在特定条件下的典型值,实际每一颗芯片都不一样。校准的方法很简单:用高精度源给出两个已知电压,比如0.5V和2.0V,分别读取ADC值,然后拟合一条直线,算出增益系数和偏移量。
具体操作:假设输入0.5V时ADC读数为620,输入2.0V时读数为2480。理想情况下0.5V对应的ADC值应该是0.5除以2.5乘以4096等于819,2.0V对应3277。实际读数和理想值之间有偏差,通过两点校准可以算出实际的转换关系。校准系数存在Flash里,每次上电读取使用。
注意:校准时的参考电压必须和实际测量时一致。如果校准用了一个基准,实际测量时换了另一个基准,校准就白做了。
4. 实操过程与核心环节实现
4.1 硬件电路搭建
先说一下我实际用的电路方案。分压网络用两颗0.1%精度的金属膜电阻,上电阻100kΩ,下电阻150kΩ,分压比0.6。分压点接运放的同相输入端,运放配置成电压跟随器,输出接STM32的ADC引脚。运放供电用3.3V单电源,选轨到轨输入输出的型号。基准电压芯片输出2.5V,接STM32的VREF+引脚,同时并联1微法和100纳法电容到地。
PCB布局上,模拟部分和数字部分要分开。基准芯片、分压电阻、运放尽量靠近STM32的ADC引脚。模拟地和数字地在芯片下方单点连接。ADC引脚旁边放一颗100纳法的去耦电容。走线尽量短,避免和时钟线、数据线平行走。
4.2 固件配置流程
以STM32F103为例,说下ADC的配置步骤。
第一步,使能GPIO和ADC时钟。GPIO配置为模拟输入模式,不要上拉下拉。
第二步,配置ADC的时钟分频。ADC时钟不要超过14MHz,一般用PCLK2除以6。
第三步,配置ADC参数。分辨率12位,扫描模式关闭(单通道),连续转换关闭,外部触发关闭,数据右对齐。采样时间设为41.5周期。
第四步,校准ADC。先使能ADC,然后执行校准序列:设置CAL位,等待校准完成。
第五步,启动转换。置位ADON,然后置位SWSTART,等待EOC标志,读取DR寄存器。
如果要定时器触发,把外部触发源配置为定时器事件,然后在定时器里设置好频率。比如要1kHz采样率,定时器配置成1kHz更新事件,ADC配置成外部触发上升沿启动。
4.3 数据处理代码实现
#define SAMPLE_COUNT 16 #define ALPHA 0.15f uint16_t adc_buffer[SAMPLE_COUNT]; float filtered_value = 0.0f; float read_voltage(void) { uint32_t sum = 0; uint16_t max_val = 0, min_val = 0xFFFF; for (int i = 0; i < SAMPLE_COUNT; i++) { adc_buffer[i] = adc_read_single(); sum += adc_buffer[i]; if (adc_buffer[i] > max_val) max_val = adc_buffer[i]; if (adc_buffer[i] < min_val) min_val = adc_buffer[i]; } sum -= max_val; sum -= min_val; float avg = (float)sum / (SAMPLE_COUNT - 2); filtered_value = filtered_value * (1.0f - ALPHA) + avg * ALPHA; float voltage = filtered_value / 4096.0f * 2.5f / 0.6f; return voltage; }这段代码做了三件事:去极值平均、一阶低通滤波、电压换算。去极值平均去掉最大最小各一个,剩下14个求平均,能有效抑制突发干扰。一阶低通滤波进一步平滑数据。最后根据分压比和参考电压换算出实际电压值。
4.4 校准流程实现
校准需要两个已知电压点。我用高精度电压源分别输出0.5V和2.0V,记录ADC读数。
typedef struct { float gain; float offset; } calib_t; calib_t calib; void calibrate(float v1, uint16_t adc1, float v2, uint16_t adc2) { calib.gain = (v2 - v1) / ((float)adc2 - (float)adc1); calib.offset = v1 - calib.gain * (float)adc1; } float adc_to_voltage(uint16_t adc_val) { return calib.gain * (float)adc_val + calib.offset; }校准系数存在Flash的固定地址,上电时读取。如果Flash为空,使用默认系数(增益等于2.5除以4096除以0.6,偏移为0)。
5. 常见问题与排查技巧实录
5.1 读数跳动大怎么办
这是最常见的问题。排查顺序如下:先看硬件,用示波器看ADC输入引脚的波形,如果有明显纹波,说明电源或基准不稳,加电容或者换LDO。如果波形干净但读数还跳,检查采样时间是否足够,加大采样周期试试。如果还跳,检查数字滤波是否生效,把滤波参数调强一些。最后检查PCB布局,模拟部分是否靠近了干扰源。
5.2 测量值整体偏高或偏低
如果所有测量值都偏同一个方向,大概率是参考电压不准或者分压电阻误差。用万用表量一下VREF+引脚的实际电压,和设计值对比。再量一下分压电阻的实际阻值,算一下实际分压比。如果偏差在可接受范围内,通过校准修正。如果偏差很大,检查电路连接是否正确。
5.3 不同通道之间相互干扰
多通道扫描模式下,如果某个通道的输入阻抗特别大,采样时会影响下一个通道的采样电容电荷。解决办法是在每个通道的输入前面加电压跟随器,或者在软件上增加通道切换后的丢弃次数,每个通道连续采两次,丢弃第一次的结果。
5.4 温度变化导致精度漂移
这是基准芯片和分压电阻的温漂引起的。选低温漂的器件是根本解决办法。如果成本不允许,可以在软件里做温度补偿,用STM32内部温度传感器读温度,根据器件的温漂曲线做修正。不过内部温度传感器精度有限,只能做粗略补偿。
5.5 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决措施 |
|---|---|---|---|
| 读数跳动大 | 采样时间不足 | 加大采样周期 | 配置41.5周期以上 |
| 读数跳动大 | 参考电压不稳 | 示波器看VREF+ | 加去耦电容或换基准 |
| 读数整体偏高 | 参考电压偏低 | 万用表量VREF+ | 校准或换基准 |
| 读数整体偏低 | 分压电阻误差 | 量实际阻值 | 校准或换0.1%电阻 |
| 通道间干扰 | 输入阻抗过大 | 检查前级电路 | 加电压跟随器 |
| 温漂明显 | 器件温漂大 | 加热/冷却测试 | 换低温漂器件或补偿 |
实操心得:校准用的高精度源,精度至少要比目标精度高一个数量级。比如你要做到正负5mV,校准源至少要正负0.5mV。否则校准本身引入的误差就把精度吃掉了。
6. 精度提升的进阶思路
6.1 过采样与抽取技术
STM32的12位ADC可以通过过采样做到更高的有效位数。原理很简单:如果噪声是随机的,多次采样求平均可以把噪声压下去,有效位数提升。每4倍过采样可以提升1位有效分辨率。比如采256次求平均,理论上可以提升4位,达到16位有效分辨率。但前提是噪声确实是随机的,而且信号在采样期间保持不变。
具体实现:配置ADC以更高的速度连续采样,用DMA把数据搬到缓冲区,凑够256个点后求平均。STM32F3和F4系列有硬件过采样单元,可以直接配置,不用软件干预。F1系列没有硬件过采样,只能软件实现。
6.2 斩波稳零技术
对于失调电压要求特别高的场景,可以用斩波稳零。简单说就是通过模拟开关周期性地交换输入信号的正负极性,把运放的失调电压调制到高频,然后用低通滤波滤掉。这样低频的失调和温漂就被消除了。实现起来需要额外的模拟开关和精密的时序控制,适合对精度要求极高的场合。
6.3 比例测量法
如果被测电压和参考电压来自同一个源,可以用比例测量法消除参考电压的漂移。比如测量电桥的输出,电桥的激励电压同时作为ADC的参考电压,这样参考电压的漂移会被抵消。STM32的VREF+可以外部输入,把电桥激励电压接过来就行。这个方法在应变片、压力传感器等电桥式传感器中特别有用。
7. 实测数据与效果验证
说了这么多,最终还是要看实测数据。我在室温25摄氏度下,用六位半台式万用表作为参考,对比了STM32的测量值。
测试条件:参考电压2.500V,分压比0.6,采样率100Hz,256次过采样,一阶低通滤波alpha等于0.1。
| 输入电压(万用表) | STM32测量值 | 误差 |
|---|---|---|
| 1.000V | 1.002V | +2mV |
| 2.000V | 1.998V | -2mV |
| 3.000V | 3.003V | +3mV |
| 4.000V | 3.997V | -3mV |
| 4.200V | 4.202V | +2mV |
误差在正负3mV以内,满足正负5mV的设计目标。长时间运行24小时,读数漂移在正负1mV以内。温度从10摄氏度升到40摄氏度,未补偿情况下漂移约正负8mV,加了温度补偿后降到正负3mV以内。
这个结果说明,STM32内置ADC配合合理的外围电路和软件处理,完全可以做到毫伏级的测量精度。关键不在于芯片本身有多好,而在于整个信号链的每一个环节都做到位。
8. 一些踩过的坑和实用建议
第一个坑:一开始没加电压跟随器,分压电阻直接接ADC引脚,读数比实际值低了将近50mV。后来加了运放缓冲,立刻准了。这个问题的本质是ADC采样电容充电不足,采样时间设到最大也没用,因为等效阻抗太大了。
第二个坑:基准电压芯片的输出电容用了普通的X7R陶瓷电容,结果噪声很大。换成C0G材质的电容后明显改善。X7R有压电效应,在电压变化时会产生噪声,做基准滤波一定要用C0G或者薄膜电容。
第三个坑:校准的时候只校准了一个点,结果低电压段准了,高电压段偏了。后来改成两点校准,全量程都准了。ADC的误差是线性的,至少需要两个点才能确定增益和偏移。
第四个坑:PCB布局时把基准芯片放在了开关电源旁边,开关噪声耦合到基准上,读数一直有规律地跳动。后来把基准芯片挪到板子另一头,加了屏蔽罩,问题解决。模拟部分一定要远离开关电源和数字高频信号。
最后分享一个小技巧:如果实在没有高精度电压源做校准,可以用一颗已知精度的基准芯片作为参考,比如用一颗初始精度正负0.05%的基准,配合精密电阻分压出两个校准点。虽然不如专业校准源,但比不校准强得多。