1. 分压测量方案的整体设计思路
1.1 为什么分压电阻的选择会直接决定ADC采集的精度
先聊个很常见的翻车场景:某天你拿STM32F103C8T6想做电池电压采集,电路很简单,电池正极串个电阻、再并个电阻分出一路电压,拉到PA0脚。板子焊完,上位机一看,电压读数比万用表差了0.3V还多,甚至跳来跳去。你先怀疑ADC坏了,再怀疑代码写得不对,折腾半天才发现问题出在那两个电阻上。
这种问题我在实际项目里踩过不止一次,也帮人排查过好多次。ADC本身只是一个把模拟电压变成数字量的部件,它测量的对象是“引脚上的电压”,而不是“你脑子里想的那个电压”。而分压电阻的作用,就是把待测电压转换成一个ADC能安全、准确采样的低电压。这个转换过程如果设计得不严谨,误差会一路传播到最终显示结果里。很多开发者在计算时只用了最简单的公式:Vout = Vin × R2 / (R1 + R2),算出个比例完事,却忽略了电阻精度、源阻抗、温漂、采样时间、引脚下拉等效阻抗等一系列问题,最后精度崩了都不知道原因出在哪。
这篇内容不会只停留在“你要用高精度电阻”这种正确的废话上,而是从电路设计、参数计算、STM32的ADC配置到实测数据,完整走一遍流程,告诉你每个环节该怎么算、怎么选、怎么验证。适合正在做嵌入式项目但被模拟量采集精度折磨过的朋友,也适合刚接触STM32 ADC、想少走弯路的学生开发者。
1.2 高边采样和低边采样:先想清楚电路结构
很多人直接拿两个电阻一串就完事,但对“被测电压”和“系统参考地”之间的关系没有仔细思考。
以最常见的电池电压采集为例:电池正极接R1,R1另一端接R2,R2另一端接GND,R1和R2的连接点就是ADC输入。这个结构把“相对于GND的正电压”降压后送给ADC,是低边分压的标准接法。但如果被测信号本身不是以GND为参考的,比如你要采集一个悬浮电压,或者采集的是高边电流采样电阻两端电压,这种简单分压结构就不成立,需要引入差分放大器或仪表放大器。PCB空间允许、成本允许的前提下,把信号预处理放到运算放大器里处理,比强行分压要靠谱得多。
这里还有一个容易踩的坑:如果你要采集的是类似TL431反馈网络中的分压点,或者ATX电源辅助绕组反馈电路里的采样电压,这种场景下分压电阻不仅决定反馈量,还会影响到TL431的阴极电流和环路稳定性,电阻取值不能只看分压比,还要满足最小工作电流要求。原理和STM32分压是相通的,都是一个电阻网络做比例衰减,但工程约束条件不同。放到我们ADC采集的场景里,同样要先问三个问题:
- 被测电压范围是多少?会不会有超过ADC输入耐压的瞬间?
- 被测电压的源阻抗高不高?信号能不能驱动ADC内部采样电容?
- 分压网络的功耗是否在系统预算内?电池供电的设备长期接在分压电阻上,耗电是不能忽略的。
这三个问题确定之后,再去算R1、R2的具体阻值,才不会返工。
2. 电阻选型背后的量化计算
2.1 由待测电压范围反推分压比
假设现在要做的是一个0V到16V的直流电压采集,STM32的VDDA接3.3V,ADC是12位,满量程对应数字量4095。第一步不是上手挑电阻,而是定分压比。
分压比n = Vout / Vin,Vout取ADC输入范围,要留出安全余量。比如3.3V供电的ADC,理论上最大能测3.3V,但实际电路里3.3V有波动,而且接近满量程时转换误差会略变大,所以最好把最大采样电压控制在3.0V以下。对应16V范围,分压比n = 3.0 / 16 ≈ 0.1875。分母分子同时乘个方便计算的数,比如取R1 = 100kΩ、R2 = 22kΩ,分压比就是22/122 ≈ 0.1803,16V输入时输出2.885V,余量足够。如果取R1 = 100kΩ、R2 = 20kΩ,分压比0.1667,12V输入时输出2.0V,16V输入时输出2.667V,这也行,而且计算方便。
分压比选好后,还要检查一个东西:待测电压的源内阻。假设待测信号源本身有10kΩ内阻,那么串联在分压网络里的等效电阻就会改变,实际分压比也跟着变。对高内阻信号源,最好在分压前加一级电压跟随器。对绝大多数项目,这里的前提是:被采集电压源的输出阻抗很低,或已经过稳压。
这里顺带验证一个点,即“总电阻取多大”。有些初学者习惯用4.7kΩ甚至1kΩ电阻做分压,理由是“阻抗低了采样稳”。确实,源阻抗低了ADC采集更稳,但代价是分压网络功耗变大。12V供电时,R1+R2=5.7kΩ,消耗电流约2.1mA,24小时就要耗掉约0.6Wh,对电池供电的产品是不能接受的。100kΩ加22kΩ组合下,待机功耗只有约0.1mA,一年耗电不到1mAh(按3.7V电池折算约0.3mAh),完全不是一个量级。所以定位决定取值,实验室验证板可以用低阻电阻,便携设备必须考虑静态功耗。
2.2 电阻精度与温漂带来的误差到底有多大
这是整个避坑指南的核心。电阻允许误差、温度系数都直接影响分压精度,而分压比的误差会直接映射到被测电压上。
先看电阻精度。假设用了最不讲究的5%精度电阻,R1标称100kΩ,实际阻值可能在95kΩ到105kΩ之间,R2标称20kΩ,实际在19kΩ到21kΩ之间。分压比可能出现的极限情况:
- 最大分压比:R2取上限21kΩ,R1取下限95kΩ,n_max = 21/116 ≈ 0.1810
- 最小分压比:R2取下限19kΩ,R1取上限105kΩ,n_min = 19/124 ≈ 0.1532
两者相比,分压比从理想值的0.1667偏离到不同方向,最大偏差约-8.1%。在读12V电压时,计算得到的测量结果可能是11.0V到12.6V之间,这在很多应用里已经不可接受了。
换成1%精度电阻,R1的范围是99kΩ到101kΩ,R2是19.8kΩ到20.2kΩ,极限分压比约在0.1618到0.1717之间,偏差约-2.9%到+3.0%。读12V时,结果落在11.65V左右到12.36V左右,仍然有0.7V的波动。
再换成0.1%精度电阻,极限偏差约0.3%,12V读数偏差大约0.036V,这个精度对于多数电压监测项目就足够了。不过0.1%电阻价格高,而且市场上很多标称0.1%的贴片电阻,温度漂移仍然有25ppm/°C甚至50ppm/°C。温度系数带来的影响也不能忽视:设两个电阻温漂方向相反,温度变化50°C,分压比可能再偏移千分之几。
有一种做法是只买0.1%电阻,然后在校准阶段用高精度万用表实测每个电阻的准确值,把实际阻值写进代码做参数修正。实测校准能补偿常温下的固定偏差,但不能补偿温漂。所以最稳妥的方法是:选温漂指标明确的电阻,比如25ppm/°C或更好;如果系统温度变化很大,可以用软件查表做温度补偿,或者干脆上数字电位器加自动校准回路。
2.3 源阻抗、采样电容和采样时间:低频电路里的高频问题
分压电阻选得再准,如果采样时间不够,ADC读出来的电压还是会偏低。很多人不理解为啥会有这回事,其实道理很简单:STM32 ADC内部并不是一个无穷大输入阻抗的理想电压表,它工作时会通过内部模拟开关把外部电压接到一个采样电容上,这个电容在采样阶段要充电到跟引脚电压一致。电容充电需要时间,时间常数正比于外部源阻抗和内部采样电容的乘积。
STM32F1系列ADC内部采样电容典型值在几皮法到十几皮法之间,具体数值要查参考手册。以12pF为例,如果外部源阻抗是20kΩ,那么时间常数τ约等于0.24µs。ADC转换过程中,采样阶段由采样时钟周期数决定。STM32F103的ADC采样时间可配置为1.5周期、7.5周期、13.5周期、28.5周期、41.5周期、55.5周期、71.5周期和239.5周期。ADC时钟频率最大14MHz,对应周期约71.4ns,采样时间是周期数乘这个值。
如果采样时间设为1.5周期,采样窗口只有约107ns,连0.5个τ都不到,电容根本充不满,测量结果会明显偏低。如果设为41.5周期,约2.96µs,大约能充到接近12个τ,误差已经很小。为保证12位精度,我习惯至少用55.5周期以上。这里有个快速估算公式:外部源阻抗尽量控制在10kΩ以下。当分压网络等效阻抗超过50kΩ时,哪怕是239.5周期的采样时间,也不一定能把误差压到1个LSB以内,尤其ADC时钟还跑到14MHz时更要小心。
特别提醒一下,分压网络的等效阻抗不是R1+R2,而是R1和R2的并联值,也就是从ADC引脚往回看进去看到的等效源阻抗。100kΩ和20kΩ并联后约16.7kΩ,这数字看着不大,但在高速采样或高精度应用里已经不能再往上加了。想让源阻抗更低,可以把两个电阻同时缩小,比如改成10kΩ和2kΩ,并联等效约1.67kΩ,采样非常“稳”,但静态功耗也会涨到约1mA@12V。所以做设计时永远是在精度、功耗、面积和成本之间做交换。
3. STM32工程配置与采集代码实现
3.1 CubeMX配置要点:采样时间、时钟和参考电压
这里以STM32F103C8T6 + STM32CubeIDE为例,把ADC采集流程过一遍。CubeMX里需要关注几个关键配置项。
一是ADC时钟分频。F1的ADC时钟来自APB2,最高14MHz。如果APB2是72MHz,那么ADC预分频至少选6分频,得到12MHz。有人为了追求采样率,把ADC时钟压到最大14MHz是可以的,但高ADC时钟下同样采样周期数对应的绝对采样时间更短,外部源阻抗要更低。
二是扫描模式和连续转换。对单通道采集,ScanConvMode选择Disable,ContinuousConvMode看需求。如果只是定期测一次电压,关掉连续转换,用定时器触发或软件触发更省电;如果要监控动态电压,开连续转换加DMA传输更合适。
三是采样时间,这是最容易踩坑的配置项。之前在2.3节算过,外部源阻抗16.7kΩ时,采样时间至少要55.5个周期才能保证12位精度。CubeMX里直接把Sampling Time选成55.5 Cycles或239.5 Cycles,别图省事保留默认的1.5 Cycles。虽然很多信号源内阻低时1.5周期也能用,但对分压电阻网络这种动不动几十kΩ源阻抗的场景,采样时间不够就是读数偏低,而且这种偏低不是线性偏移,是你很难用软件校准补偿的。
四是校准。STM32F1的ADC内部有校准机制,CubeMX会在ADC初始化时自动执行校准函数,但要注意校准必须在ADC使能之前完成。如果你手动重写了HAL初始化流程,漏了校准,ADC的零点偏移和增益误差会变大,实测结果会整体偏移几十个LSB。
参考电压方面,STM32F103的ADC参考电压就是VDDA引脚电压,一般接3.3V或外部精密基准源。如果项目对电压测量精度要求很高,建议给VDDA单独用LDO供电,或加一个外部基准芯片,比如REF3030、TL431改的参考源,否则VDDA随负载波动会直接影响转换结果。这里顺便说个经验:很多开发板用AMS1117给3.3V供电,AMS1117输出精度本身有2%到3%的偏差,所以“Vref=3.3V”只是一个理想假设,实际要拿万用表实测VDDA电压再代入软件计算。
3.2 采集与换算代码:除了公式还有两个坑
CubeMX配置完成后,直接调HAL库的ADC接口就能完成一次采集。核心代码很简单:
uint32_t adc_read_once(void) { HAL_ADC_Start(&hadc1); HAL_ADC_PollForConversion(&hadc1, 100); uint32_t val = HAL_ADC_GetValue(&hadc1); HAL_ADC_Stop(&hadc1); return val; }软件换算部分,我用的公式是:
float VREF_ACTUAL = 3.28f; // 用万用表实测VDDA得出 const float R1 = 100000.0f; // 实测实际阻值 const float R2 = 20000.0f; // 实测实际阻值 const float DIVIDER_RATIO = R2 / (R1 + R2); const float FULL_SCALE = 4095.0f; float get_input_voltage(void) { uint32_t raw = adc_read_once(); float vadc = (float)raw * VREF_ACTUAL / FULL_SCALE; float vin = vadc / DIVIDER_RATIO; return vin; }这里有两个易错点。
第一个是VREF不能写死3.3V。STM32F103内部没有高精度基准,VDDA就是参考电压。很多开发板的3.3V实测只有3.23V甚至3.15V,你写死3.3V,读出来的电压整体偏高2%以上。所以要么实测,要么把VREF_ACTUAL做成可校准参数。
第二个是分压电阻的实测值要替代标称值。我习惯在硬件调试阶段用四位数以上的台式万用表测量每个电阻,拿到实际阻值后写进宏定义。这样做的效果等同于把电阻精度从5%或1%提升到仪表精度,成本几乎为零。有朋友问:那我能不能只做软件校准,不分压比,直接线性拟合?当然可以,但用实测电阻值更接近物理本质,校准点也更稳定。
实际项目里我还会在ADC前端加一个RC低通滤波:从分压点到ADC引脚之间串联一个1kΩ电阻,再对地并联一个100nF电容。这个RC有两个作用,一是滤掉高频噪声,二是给ADC采样电容提供一个低阻抗的电荷源。加了RC之后ADC引脚处等效阻抗还是1kΩ左右,但高频干扰被滤掉了,读数稳定性会明显提升。注意串联电阻也不能太大,超过10kΩ会反而增加源阻抗,对采样时间要求更高。
3.3 均值滤波与异常值剔除:好数据是过滤出来的
即使电路设计没问题,单次ADC转换结果也可能受噪声干扰出现跳动。最直接的办法是连续采多次再求平均。
我常用的软件滤波方式分三层:
- 中值滤波:连续采5次或7次,排序后取中间值,能有效剔除尖峰毛刺。
- 算术平均:对中值滤波后的连续N个结果求平均,比如取16次平均,能把白噪声降低到原来的四分之一。
- 限幅滤波:和上一次有效值的差超过一定阈值就丢弃,防止异常跳变直接进到显示值。
这三个组合起来就类似滑动窗口的中值平均滤波,适合直流电压采集场景。代码写起来也不复杂:
uint32_t filter_adc_value(void) { uint16_t buf[5]; for (int i = 0; i < 5; i++) { buf[i] = adc_read_once(); } // 冒泡排序取中值 for (int i = 0; i < 4; i++) { for (int j = i + 1; j < 5; j++) { if (buf[j] < buf[i]) { uint16_t tmp = buf[i]; buf[i] = buf[j]; buf[j] = tmp; } } } return buf[2]; }之后还要求平均的话,把上面这个中值函数调多次再平均即可。滤波不能替代硬件设计,但能解决最后那一点“读数乱跳”的问题。对实时性要求高的场景,每次转换都在做一个滤波反而影响采样率,这时可以选择开DMA + 连续转换,把一批采样结果收集完再做后处理。
4. 实测数据与误差复盘
4.1 使用不同精度电阻的实测对比
讲完理论,用实际数据说话。我在工作台上用一个可调稳压电源输出标称12.00V(用六位半万用表实测为12.003V),分别搭了三套分压网络,接入同一块STM32F103C8T6的最小系统板,VDDA实测3.28V,采样时间设为55.5周期,软件用3.2节代码换算电压。
第一组用5%精度电阻,R1标称100kΩ实测97.8kΩ,R2标称20kΩ实测21.3kΩ。第二组用1%精度电阻,R1实测100.4kΩ,R2实测19.9kΩ。第三组用0.1%精度电阻,R1实测100.01kΩ,R2实测20.03kΩ。结果如下表:
| 电阻组别 | ADC原始值 | 软件显示电压 | 相对误差 | 备注 |
|---|---|---|---|---|
| 5%电阻 | 2764 | 11.09V | -7.6% | 分压比偏差直接带入 |
| 1%电阻 | 2814 | 12.19V | +1.56% | 误差来自R1和R2单只偏差 |
| 0.1%电阻 | 2796 | 12.02V | +0.14% | 接近万用表实测值 |
由此可见,5%电阻带来的误差高达0.9V,这在整个12V系统里足以触发低压告警或者电压异常判断。而0.1%电阻的实测结果已经非常理想,但这个准确度的前提还是“没有把电阻阻值实际测量后写进代码”。更夸张的是,如果我先用万用表实测了5%电阻的阻值,再用实测值代入软件计算,误差会立刻从7.6%降到0.2%以内。这说明什么?说明电阻标称精度是硬件层面的上限,而实际精度可以通过校准来逼近。
4.2 误差来源分解与优先级排序
做一个12V电压采集系统,可能误差来源按贡献排序大概是这样的:
| 误差来源 | 典型大小 | 是否可以软件补偿 |
|---|---|---|
| 分压电阻精度偏差 | 0.5%到8% | 可以,用实测阻值或两点校准 |
| VDDA参考电压偏差 | 1%到3% | 部分可以,实测后写死 |
| ADC量化误差 | 0.024%(12位,1LSB=0.8mV) | 可降噪但不能消除 |
| ADC积分非线性误差(INL) | F1系列约2 LSB | 难校准 |
| 采样时间不足导致的充电误差 | 0.1%到5% | 修改采样时间设置 |
| PCB走线串扰、电源噪声 | 0.1%到1% | 硬件滤波、布局改善 |
在排查一个采集误差问题时,我的习惯是从大到小一项项排除:先用万用表确认分压点实际电压,如果这个电压都不对,那是硬件问题;如果分压点电压对但ADC读数不对,再查采样时间和VDDA;如果这些都排除,才怀疑STM32本身的非线性。很多朋友一上来就怀疑ADC坏掉,其实八成是前端电路和基准电压的问题。
这里面最容易被人忽略的是VDDA的影响。STM32F103的VDDA如果直接接在电源模块输出端,而电源模块带载后电压下降了0.1V,那么在3.3V基准下计算出的结果就会偏移约3%。如果你的系统里,VDDA还同时给LED或电机驱动供电,纹波会直接干扰ADC结果。设计时务必把VDDA和数字电源用磁珠或RC隔离,有条件就用独立LDO。
5. 常见问题排查与避坑笔记
5.1 读数异常跳动的排查流程
把这个排查过程写成速查表,遇到问题可以按顺序对照:
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 读数整体偏高 | VDDA实际值比程序写的低 | 用万用表测VDDA,修正代码 |
| 读数整体偏低 | 分压电阻阻值偏小,或采样时间不足 | 实测电阻值,加大采样周期数 |
| 读数跳动大 | 外部噪声、引脚悬空、接线过长 | 加RC低通滤波,缩短飞线,使用屏蔽线 |
| 靠近满量程读数非线性 | 分压设计余量不足,接近ADC饱和 | 重新规划分压比留足余量 |
| 采样值有规律波动 | 开关电源纹波干扰 | 改善电源滤波,ADC输入加电容 |
| 多通道间相互干扰 | 引脚间串扰或DMA搬运出错 | 增加采样间隔,检查通道切换时序 |
这里特别提一个现象:用杜邦线从分压板连到开发板引脚时,读数的跳动往往大于0.5%。杜邦线尤其是那种十几厘米长的母对母杜邦线,等效电感和天线效应都很明显,在开关电源环境里能耦合出几十毫伏的噪声。最简单的办法是直接把分压电阻焊在靠近STM32引脚的地方,或者用双绞线把分压信号引到板上并在引脚附近加100nF电容。
5.2 向ADC引脚灌入过高电压的后果
有人会用分压比算出一个看似安全的输出电压,但忽略了上电瞬间或热插拔时可能出现的高压脉冲。如果待测电压突然冲到24V,而分压比按16V满量程设计,输出电压就达到4V,这已经超过STM32引脚允许的耐压范围,长时间或反复冲击会损坏引脚内部的ESD保护二极管,严重时芯片直接报废。
设计阶段从两个方向防:第一,分压比按“可能出现的最大值”而不是“正常工作值”来设计;第二,给ADC引脚加钳位保护,比如对地并联一个3.3V的TVS二极管或肖特基二极管,或者串联一个10kΩ电阻限制输入电流。加了钳位保护的代价是源阻抗变大,需要再加深RC滤波。我在批量产品上通常使用串联10kΩ电阻加外部肖特基二极管的方式,既能钳位又能限流,RC截止频率也低,抗干扰能力更强。
5.3 换用GD32、S32K等芯片时,STM32经验不能照搬
最近不少项目在往国产芯片或车规芯片迁移,比如GD32、HC32、S32K系列。它们部分型号的ADC架构和寄存器兼容STM32,但时序和内部模拟参数差别不小。GD32H7系列就带有硬件滤波功能,可以在ADC内部对多次采样结果做平均,不需要CPU参与。S32K312的ADC支持不同采样相位、不同转换模式,配置比STM32复杂很多,不能把CubeMX里那一套完全照搬过去。
经验就一条:拿到任何芯片,先看参考手册里的ADC章节,找到采样电容大小、最大源阻抗推荐值和采样时间计算表。数据手册里写的“最大外部阻抗”往往是一个保守参考值,实际工程里要结合采样时间和精度要求重新算。之前硬搬STM32配置到国产芯片翻过车的案例不少,症状基本就是:寄存器配置看着没问题,转换结果却偏得离谱,其实是因为内部采样电容更大或ADC时钟分频不同。
除了芯片本身,开发环境也容易卡壳。比如Keil里同时装了C51和STM32的芯片包,或者STM32F103的芯片包版本过旧,工程编译时会出现头文件冲突。换芯片后务必检查芯片包版本,CubeIDE里也要确认下载的固件包与芯片型号匹配。遇到USB识别不了设备这种问题,先查驱动和ST-Link固件版本,别急着认为是芯片坏了。
5.4 两个被反复问到的小问题
问得最多的一个问题是:能不能直接采集STM32内部的VREFINT通道来校准VDDA?答案是可以。STM32F103内部有一个内置参考电压,约1.2V,可以通过对应的通道采集。但这个参考电压精度不算高,数据手册给的是1.16V到1.24V范围,也就是带 ±3%,用它校准VDDA能做粗略基准,但达不到精密电压测量要求的精度。如果项目真需要高精度电压采集,还是建议外置基准。
另一个问题在数据采集项目中更常见:怎么把波形数据导入电脑分析,比如在WaveVersion等工具里画ADC采集到的数据。简单可靠的办法是把ADC原始值以文本格式通过串口发送,一帧一个采样值,然后导入到电脑端的串口绘图软件或数据分析工具中处理。注意设置一致的行分隔符和波特率,否则数据错位。如果要分析高频信号,直接拷贝串口输出的文本效率太低,建议开DMA环形缓冲区,批量打包后用二进制传输。
6. 根据我个人经验做个收尾
这篇内容本来打算写到电阻选型就收住,但实在觉得对新手来说,后面的实测数据和排查思路才是真正会节省他们几天时间的东西。我想用自己踩坑换来的经验做个结尾。
分压电阻这件事,看起来是元器件选型里最不起眼的一环,实际却决定了整个ADC采集系统的精度上限。我现在的习惯,不管项目多急,都会先算清楚分压比、等效源阻抗、功耗预算,再确定电阻精度和温漂指标,最后在打样阶段用万用表把实际阻值测出来写进校准参数。这一套动作做完,后面再出电压读数不准的概率几乎为零。
如果你正在做基于STM32的电压采集,不管是毕业设计、智能家居项目,还是工业设备监控,都建议先拿一块板子做最小验证:搭电阻、量VDDA、调采样时间、看ADC原始值。把流程跑通后,再考虑滤波、隔离、校准这些进阶内容。最后再分享一个小技巧:调试时在代码里保留一个“原始ADC值”的调试输出,这比直接看换算后的电压更能帮你判断问题出在硬件还是软件,我靠这个习惯少走了很多弯路。