news 2026/9/29 16:15:36

STM32内置VREFINT电池电量监测方案:替代库仑计的低成本高精度实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32内置VREFINT电池电量监测方案:替代库仑计的低成本高精度实现

1. 为什么我要放弃库仑计,改用VREFINT

搞嵌入式电池供电项目的人,迟早会撞上一个绕不开的问题:怎么知道电池还剩多少电。我最早做手持设备的时候,第一反应就是上库仑计,比如TI的BQ系列或者MAXIM的燃料计芯片。贵,是真的贵,一颗芯片比主控还烧钱,而且外围还得配采样电阻,PCB面积也上去了。后来换了个思路,用STM32自带的ADC去测电池分压,结果发现读数飘得厉害,满电和快没电的时候电压差只有零点几伏,ADC参考电压本身又跟着VDD一起抖,测出来的值根本没法看。

直到我把STM32参考手册翻到VREFINT那一章,才意识到芯片内部藏了一个现成的“标尺”。VREFINT是内部参考电压,出厂时校准过,典型值1.20V左右,关键是它不随VDD变化。用它反推VDD,再用VDD去修正电池ADC读数,整个测量链路就稳了。这套方案不需要任何额外芯片,成本几乎为零,精度做下来在手持设备上完全够用。

这篇文章适合谁看?如果你正在做STM32的电池供电项目,手头没有库仑计,又想让电量显示别那么“玄学”,那这套方法可以直接抄。我会把原理、代码、踩过的坑全部摊开讲,代码基于STM32F1系列,但F0、F3、F4、G0、G4这些带VREFINT的型号思路完全一样,改改寄存器名就能跑。

注意:VREFINT不是所有STM32型号都有,拿不准的话先去参考手册搜“VREFINT”或者“Internal reference voltage”,确认你的芯片支持再往下看。

2. 核心原理拆解:VREFINT到底怎么当尺子用

2.1 先搞懂ADC的参考电压从哪来

STM32的ADC是逐次逼近型,它的输出公式很简单:

ADC_Value = (Vin / Vref) * Full_Scale

Full_Scale对于12位ADC就是4095。问题就出在Vref上。大部分低成本方案里,Vref直接接VDD,而VDD来自电池或者LDO。电池从4.2V掉到3.3V,Vref就跟着掉,同一个Vin测出来的ADC_Value完全不一样。这就是为什么直接用VDD当参考去测电池,读数会“越用越高”——因为尺子本身在缩短。

VREFINT的作用就是提供一把不缩水的尺子。它内部是一个带隙基准源,输出电压固定在1.20V左右(具体值看芯片数据手册,F1典型值1.20V,F4是1.21V)。这个电压不随VDD和温度大幅变化,芯片出厂时还会在特定条件下测一次,把校准值存到系统存储区里。

2.2 用VREFINT反推VDD的数学过程

思路很直接:既然VREFINT的电压是已知的,那我用ADC去测VREFINT,得到一个ADC读数,就能反推出当前VDD是多少。

设VREFINT的实际电压为Vrefint,ADC测VREFINT得到的读数为ADC_vrefint,那么:

VDD = Vrefint * 4095 / ADC_vrefint

这里有个关键点:Vrefint不能直接用数据手册的典型值1.20V,因为每颗芯片都有个体差异,典型值只是统计中心。STM32在出厂时会把这个芯片的实际Vrefint校准值存在特定地址,F1系列存在0x1FFFF7BA(16位,单位mV),F4系列存在0x1FFF7A2A。读出来代入公式,精度能提高一大截。

拿到VDD之后,再去测电池分压:

Vbat = ADC_bat * VDD / 4095 * (R1 + R2) / R2

R1和R2是分压电阻。这样整个链路里,VDD的波动被VREFINT修正掉了,电池电压的测量就只取决于分压电阻的精度和ADC本身的噪声。

2.3 为什么不用内部温度传感器或者别的基准

有人会问,STM32不是还有内部温度传感器吗,能不能用它来修正?可以,但温度传感器的绝对精度很差,F1的数据手册标的是±1.5°C,换算成电压误差很大,而且它本身也受VDD影响。VREFINT是专门为“提供稳定参考”设计的,温漂和电压调整率都比温度传感器好一个数量级。至于外部基准芯片,比如REF3030,精度确实高,但要多一颗料、多一路电、多一块PCB面积,对于成本敏感的项目来说不划算。

2.4 精度到底能做到多少

我实测过一批STM32F103C8T6,用出厂校准值修正后,VDD的测量误差在±20mV以内,电池电压经过分压还原后误差在±30mV左右。对于判断“满电、中等、低电、关机”这四个档位,这个精度绰绰有余。如果你要做电量百分比显示,建议做分段映射,不要直接线性换算,因为锂电池的放电曲线不是直线。

3. 硬件设计与参数计算:分压电阻怎么选

3.1 分压电阻的取值逻辑

电池电压最高4.2V,STM32的ADC输入不能超过VDD,通常VDD是3.3V。所以必须分压。分压比怎么定?假设R1是上臂,R2是下臂,电池接R1,R2接地,ADC接中间。

分压后的电压:

Vadc = Vbat * R2 / (R1 + R2)

要求Vadc最大不超过3.3V,留点余量取3.0V。电池最高4.2V,那么:

R2 / (R1 + R2) = 3.0 / 4.2 ≈ 0.714

取R1 = 10kΩ,R2 = 24kΩ,分压比0.706,4.2V时分压后2.96V,安全。但这里有个坑:电阻太大,ADC采样保持电容充电慢,读数会偏低;电阻太小,静态功耗大,电池还没用就没电了。

3.2 静态功耗与采样时间的平衡

STM32的ADC采样保持电容大概8pF,采样时间可配置。如果分压电阻是10k和24k并联,等效源阻抗约7kΩ。F1的ADC手册建议源阻抗不超过10kΩ,7kΩ勉强可以,但采样时间要拉长。我一般把采样时间设成239.5个ADC周期,这样充电充分,读数稳定。

静态功耗方面,分压电阻一直在耗电:

I = Vbat / (R1 + R2) = 4.2 / 34k ≈ 123μA

123μA对于大容量电池不算什么,但对于小容量电池或者长期待机设备,这个电流不能忽略。我的做法是在分压电阻下臂串一个NMOS,平时关断,要测量的时候再打开,测完立刻关掉。这样平均功耗可以降到几微安。

3.3 滤波电容和PCB布局

ADC输入端并一个0.1μF电容到地,可以滤掉高频噪声。这个电容要尽量靠近MCU的ADC引脚,走线短而粗。分压电阻的接地点要和MCU的模拟地单点连接,不要和数字地混在一起。如果PCB空间允许,在电池输入端再并一个10μF的电容,应对电池内阻带来的瞬态压降。

提示:分压电阻选1%精度的金属膜电阻,温漂选50ppm/°C以内的。普通碳膜电阻温漂大,夏天和冬天测出来的电量能差一档。

4. 软件实现:从校准值读取到滤波输出

4.1 读取出厂校准值

F1系列的VREFINT校准值存在0x1FFFF7BA,是一个16位值,单位是mV。读的时候要注意,这个地址是系统存储区,直接指针访问即可:

#define VREFINT_CAL_ADDR ((uint16_t*)0x1FFFF7BA) uint16_t vrefint_cal = *VREFINT_CAL_ADDR; // 单位mV,典型值1200左右

F4系列地址不同,是0x1FFF7A2A,而且F4的校准值是在VDD=3.3V、温度25°C条件下测的,单位也是mV。G0、G4系列有的型号校准值存在OTP区域,具体地址查参考手册的“Device electronic signature”章节。

4.2 ADC多通道配置与DMA搬运

我习惯用DMA来搬ADC数据,这样不占CPU,采样也稳定。配置两个通道:一个测VREFINT,一个测电池分压。用规则组扫描模式,DMA循环搬运到一个数组里。

// 伪代码,基于标准外设库 ADC_InitStructure.ADC_ScanConvMode = ENABLE; ADC_InitStructure.ADC_ContinuousConvMode = ENABLE; ADC_InitStructure.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None; ADC_InitStructure.ADC_DataAlign = ADC_DataAlign_Right; ADC_InitStructure.ADC_NbrOfChannel = 2; ADC_Init(ADC1, &ADC_InitStructure); // 通道0测电池,通道17测VREFINT ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_17, 2, ADC_SampleTime_239Cycles5); ADC_DMACmd(ADC1, ENABLE); ADC_Cmd(ADC1, ENABLE);

VREFINT的通道号各系列不同,F1是17,F4是17,G0是13,具体查数据手册的“ADC channel selection”表。

4.3 计算VDD和电池电压

DMA搬回来的数组里,第一个是电池通道,第二个是VREFINT通道。计算顺序很重要:先用VREFINT算VDD,再用VDD算电池。

uint16_t adc_bat = adc_dma_buf[0]; uint16_t adc_vref = adc_dma_buf[1]; // 用校准值算VDD,单位mV uint32_t vdd_mv = (uint32_t)vrefint_cal * 4095 / adc_vref; // 算电池电压,单位mV,分压比R2/(R1+R2) uint32_t vbat_mv = (uint32_t)adc_bat * vdd_mv / 4095 * (R1 + R2) / R2;

这里有个细节:vrefint_cal是mV,adc_vref是ADC读数,算出来的vdd_mv也是mV。整个计算用32位整数,避免溢出。4095是12位满量程,如果你开了过采样或者用了16位ADC,这里要改。

4.4 滑动平均滤波与异常值剔除

ADC读数一定有噪声,直接算出来的电压会跳。我一般做两级处理:先剔除明显异常的值,再做滑动平均。

#define FILTER_LEN 8 static uint32_t vbat_buf[FILTER_LEN]; static uint8_t buf_idx = 0; uint32_t vbat_filter(uint32_t new_val) { // 如果新值和当前平均值差太多,直接丢弃 uint32_t sum = 0; for (int i = 0; i < FILTER_LEN; i++) sum += vbat_buf[i]; uint32_t avg = sum / FILTER_LEN; if (new_val > avg + 200 || new_val < avg - 200) { return avg; // 丢弃异常值 } vbat_buf[buf_idx] = new_val; buf_idx = (buf_idx + 1) % FILTER_LEN; sum = 0; for (int i = 0; i < FILTER_LEN; i++) sum += vbat_buf[i]; return sum / FILTER_LEN; }

200mV的阈值是根据我的分压比和电池特性调的,你可以根据实际情况改。滑动平均长度8,对应8次采样,如果采样率是10Hz,响应时间0.8秒,够快了。

4.5 电量分段映射

锂电池的放电曲线不是线性的,4.2V到3.7V之间掉得慢,3.7V到3.3V掉得快。直接线性映射百分比,会出现“前80%用很久,后20%瞬间没电”的体验。我一般做分段映射:

电池电压(mV)显示电量说明
≥4100100%满电
3900~410080%正常使用
3700~390050%中等
3500~370020%低电提醒
3300~35005%即将关机
<33000%关机保护

这个表是根据我用的某款锂聚合物电池的放电曲线实测出来的,不同电池会有差异,建议自己用电子负载测一遍。

5. 实操中踩过的坑与排查技巧

5.1 VREFINT读数跳动大怎么办

第一次调的时候,VREFINT的ADC读数在±30个LSB之间跳,算出来的VDD也跟着跳。排查下来有几个原因:一是采样时间太短,源阻抗高,采样保持电容没充满;二是ADC时钟太快,F1的ADC时钟不能超过14MHz,我一开始设了72MHz分频6,正好12MHz,没问题,但后来换了个板子分频设错,ADC直接跑飞;三是VREFINT通道使能后需要稳定时间,数据手册建议第一次转换后丢弃,我一般连续采8次,丢掉前2次,取后6次平均。

5.2 电池电压测出来偏高或偏低

如果测出来的电压和万用表差很多,先检查分压电阻的实际阻值。我买过一批标称10k的电阻,实际测出来9.7k,分压比就偏了。另外,ADC输入引脚如果有漏电流,也会导致读数偏差。STM32的ADC输入漏电流在nA级别,一般不影响,但如果引脚上接了其他东西,比如LED或者上拉电阻,就会分流。测量引脚要专用,不要复用。

5.3 低电量时读数突然掉到0

电池快没电的时候,内阻变大,ADC采样瞬间的电流会导致电压瞬间跌落,采到一个很低的値。我的处理是在ADC采样前先打开分压电阻的NMOS,延时1ms再采样,采完立刻关断。这样分压电阻只在采样瞬间耗电,电池内阻的影响也小了。另外,软件上做异常值剔除,连续3次低于关机阈值才真正关机,避免误判。

5.4 不同批次的芯片校准值差异

我做过一批100台的小设备,发现不同芯片的VREFINT校准值从1190mV到1215mV都有,跨度25mV。如果不读校准值,直接用1.20V算,VDD误差能到2%,电池电压误差更大。所以校准值必须读,这是精度的关键。

5.5 常见问题速查表

现象可能原因排查方法
VREFINT读数一直是0通道号配错查数据手册确认VREFINT通道
VDD算出来是0校准值地址读错确认芯片型号对应的校准地址
电池电压跳动大采样时间短/无滤波加长采样时间,加滑动平均
低电量误关机电池内阻导致瞬态跌落采样前延时,连续多次确认
满电显示不满分压电阻误差实测电阻值,修正分压比
温度变化后电量不准电阻温漂大换低温漂电阻,或做温度补偿

注意:VREFINT的校准值是在特定温度下测的,虽然带隙基准温漂小,但在极端温度下还是会有偏差。如果产品要在-40°C到85°C工作,建议做温度补偿,用内部温度传感器读温度,查表修正。

6. 进阶优化:让电量监测更稳更准

6.1 用过采样提升有效位数

STM32的ADC是12位,但通过过采样可以提高到14位甚至16位。原理是连续采16次,求和后右移2位,得到14位结果。这样VREFINT和电池电压的分辨率都提高了,小信号变化也能捕捉到。代价是采样时间变长,16次采样如果每次239.5周期,在12MHz ADC时钟下大概要3.2ms,对于电量监测来说完全可以接受。

// 过采样16次,右移2位得14位结果 uint32_t adc_oversample(uint8_t channel) { uint32_t sum = 0; for (int i = 0; i < 16; i++) { ADC_RegularChannelConfig(ADC1, channel, 1, ADC_SampleTime_239Cycles5); ADC_SoftwareStartConvCmd(ADC1, ENABLE); while (!ADC_GetFlagStatus(ADC1, ADC_FLAG_EOC)); sum += ADC_GetConversionValue(ADC1); } return sum >> 2; // 14位结果 }

6.2 动态调整采样率省电

电量变化很慢,没必要一直高频采样。我的做法是正常工作时每10秒测一次,低电量时每2秒测一次,关机前每0.5秒测一次。这样平均功耗可以压到很低。用RTC唤醒或者定时器中断来触发采样,采完就进低功耗模式。

6.3 结合电池内阻估算健康状态

电池用久了内阻会变大,同样的电压下,带载能力下降。我试过在采样瞬间打开一个已知负载(比如一个100Ω电阻),测带载前后的电压差,反推内阻。内阻超过初始值2倍时,提示电池老化。这个方法不需要额外硬件,但负载电阻要选好,太小了耗电,太大了压差测不准。

6.4 代码移植到其他STM32系列

F1到F4的移植主要是改校准值地址和VREFINT通道号。F4的ADC是12位,但参考电压计算方式一样。G0、G4系列有的型号VREFINT通道是13,校准值在OTP里,读法不同。H7系列ADC精度更高,但VREFINT原理一样。移植的时候先查参考手册的“Internal voltage reference”章节,确认校准值地址和通道号,再改宏定义就行。

我在实际项目里用这套方案做了三款产品,最长的已经跑了两年多,电量显示一直很稳。唯一一次出问题是因为分压电阻用了便宜的碳膜电阻,夏天高温时阻值漂移,导致电量显示偏高,换了金属膜电阻就好了。所以电阻别省那几分钱,选1%精度的,温漂50ppm以内的,整个方案的成本也就多一两毛。

最后分享一个小技巧:如果你觉得读校准值麻烦,可以在产线上用高精度电源给每台设备校准一次,把实际的VREFINT电压写进Flash,这样精度比出厂校准值还高。批量生产的话,这个校准步骤也就几秒钟的事。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/29 16:13:26

栈的三大经典应用:括号匹配、相邻消除与逆波兰表达式求值

刷算法题刷到代码随想录day11的栈与队列part2&#xff0c;也就是20.有效的括号、1047.删除字符串中的所有相邻重复项、150.逆波兰表达式求值这三道经典题时&#xff0c;我最大的感受是&#xff1a;栈终于开始干正事了。前面part1用栈实现队列、用队列实现栈&#xff0c;更多是结…

作者头像 李华
网站建设 2026/9/29 16:13:22

VirtualBox增强功能安装失败:内核头文件精准匹配方案

简介&#xff1a;本资源是一份针对Linux虚拟机用户&#xff08;尤其是CentOS/Ubuntu等RHEL系发行版初学者与运维实践者&#xff09;编写的VirtualBox增强功能安装实战指南&#xff0c;专注解决Guest Additions编译失败这一高频痛点问题。文档系统梳理了‘Building the main Gue…

作者头像 李华
网站建设 2026/9/29 16:12:46

Windows 11 24H2 下 S7-PLCSIM 驱动签名问题修复指南

1. 问题背景与影响范围 Windows 11 24H2 这个版本&#xff0c;微软在内核层面动了些东西&#xff0c;尤其是驱动签名强制策略和内核隔离相关的默认配置&#xff0c;导致一批老版本工业软件的虚拟驱动直接趴窝。S7-PLCSIM V5.0 就是重灾区之一。这个软件在自动化圈子里什么地位不…

作者头像 李华
网站建设 2026/9/29 16:11:20

C++ 2D射击游戏实战:TopDownShooter源码解析与避坑指南

简介&#xff1a;这是一份面向C游戏开发初学者与2D射击游戏爱好者的开源项目源码&#xff0c;实现了一个带视野遮挡效果的自上而下俯视射击玩法&#xff0c;适合用来学习游戏循环、碰撞检测与光影渲染等核心机制。压缩包共76个文件&#xff0c;约140KB&#xff0c;以cpp与h源码…

作者头像 李华
网站建设 2026/9/29 16:11:16

安卓系统镜像解包打包全链路:simg2img、make_ext4fs 与 mkuserimg.sh 实战

简介&#xff1a;这份资源是面向Android系统开发者、ROM制作人和设备调试者的已编译工具集&#xff0c;用于解决系统镜像解包、打包与格式转换等操作需求。包内共4个文件&#xff0c;包含make_ext4fs、simg2img、img2simg三个可执行程序及一个mkuserimg.sh脚本&#xff0c;压缩…

作者头像 李华
网站建设 2026/9/29 16:10:58

Unity接入华为HMS SDK实战:登录支付回调与避坑指南

简介&#xff1a;本资源是面向Unity开发者的华为HMS SDK接入示例工程&#xff0c;适合需要在华为设备上集成账号、游戏、推送等服务的移动游戏与应用开发者参考。压缩包共约2000个文件&#xff0c;以bin、info、class、meta、java、xml、png、jar、cs、dll等为主&#xff0c;涵…

作者头像 李华