ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

树莓派RP2350搭配MAX17048电量计:MicroPython实现锂电池电量检测

树莓派RP2350搭配MAX17048电量计:MicroPython实现锂电池电量检测 这段时间给一个手持小设备做电源管理主控选了树莓派RP2350也就是Pico 2上那颗新MCU电池呢是常见的3.7V锂电池。设备要做电量显示最初我用电阻分压加ADC读电压、再换算成电量结果在低电量阶段误差大得离谱同一个电池放电电流大一点电压就掉到3.2V放一会又回弹。后来换上了MAX17048电量计I2C接口读电压和剩余电量都变成几行代码的事从接线到跑通实测下来确实5分钟就能看到数据。这篇文章把完整接线、原理、MicroPython代码和常见坑全记录下来适合正在做Pico 2项目、毕设电源管理或者想给DIY设备加电量显示的朋友参考。1. 为什么不用电阻分压加ADCMAX17048的定位1.1 电量测量的三种常见方案对比先别急着看代码我觉得有必要把为什么选MAX17048讲清楚。很多做嵌入式的小伙伴一开始都会想着“不就是测个电压嘛ADC加两个电阻就搞定了”我以前也是这么干的但踩过几次坑之后发现事情没那么简单。现在主流的电量测量方案大概有三类。第一类是纯电压读取法就是电阻分压加上单片机ADC查一个电压-电量对照表来换算。这个方法成本最低但是有两个很致命的问题锂电池的放电曲线并不是一条直线在3.7V到3.3V这段尤其陡峭电压稍微波动一点算出来的电量就偏差好几个百分点而且电池内阻会随着放电电流变化电机一转、屏幕一亮电压马上掉下去你读到的值根本不是真实的开路电压而是带负载的瞬时电压这时候算电量就是自欺欺人。第二类是库仑计法也叫电流积分法通过一颗精密采样电阻串联在电池回路里用ADC持续采样电流并积分从而得到充放电电量。这个方法精度不错但需要额外考虑采样电阻的温漂、放大电路的零点漂移BOM成本和PCB面积都上去了对于一个小型手持设备来说有点重。第三类就是MAX17048这类ModelGauge电量计方案。它不需要外置采样电阻芯片直接采集电池正端电压内部通过电压曲线、电压变化率以及电池老化模型来估算剩余电量。整体外围电路就是一个去耦电容。它把复杂拟合过程全部做在芯片内部对外提供I2C接口你读两个寄存器就能拿到电压和SOCState of Charge剩余电量百分比这也是它在消费电子产品里应用非常广的原因。方案精度外围成本适用场景电阻分压ADC较低受负载影响大最低玩具、对电量要求不高的设备库仑计较高高需要精密采样电阻高精度监测、容量分析MAX17048 ModelGauge中等偏上低只需一颗电容手持设备、低功耗设备1.2 我选择MAX17048的3个实际理由第一是成本低模块在市面上几块钱到十几块钱就能买到芯片本身封装也很小适合集成到自己的PCB里。第二是开发效率高I2C通信大家都很熟而且它提供的SOC寄存器直接就是百分比数据不需要自己查表拟合省掉了最麻烦的调试环节。第三是功耗低MAX17048的工作电流据说在微安级别对于电池供电的设备来说很友好不会因为加了电量计反而加快掉电。当然它也有不擅长的地方。ModelGauge本质上还是基于电压估算的如果你要求的是“绝对严谨的库仑级计量精度”它肯定不如带采样电阻的库仑计芯片。但是在大多数产品里用户只是想看到“还有多少电、还能用多久”一个不太离谱的数值MAX17048完全够用。至少在Pico 2这种级别的项目里它的精度和成本平衡得非常漂亮。2. 硬件准备与接线5分钟里最关键的1分钟2.1 物料清单和选型建议做这个实验不需要太多东西我列一下我实际用的物料。树莓派RP2350开发板也就是Raspberry Pi Pico 2一定注意别买成老的RP2040 Pico虽然代码逻辑一样但标题既然说了RP2350我们就用新的芯片。MAX17048电量计模块买那种已经把I2C上拉电阻做好的成品模块最省事很多模块还带引脚座直接杜邦线就能连。3.7V锂电池最好是带保护板的防止实验过程中过放。面包板、杜邦线若干如果模块没焊排针需要先焊一下。有条件的话准备一块万用表排查问题时会很有帮助。这里有个容易被忽略的点MAX17048的VCC工作电压范围一般是2.5V到4.5V所以可以直接接锂电池正极不需要额外稳压。它测的就是VCC引脚上的电池电压。但如果你的模块上带稳压电路那就要看模块说明书搞清楚VCC到底应该接电池还是接3.3V。2.2 接线表和接线要点接线非常简单我使用的是RP2350的I2C0控制器引脚配置为SDAGP0、SCLGP1。这个引脚不是焊死的MicroPython允许你把I2C功能映射到其他支持的GPIO上但GP0和GP1是最常用、最容易记的一组。MAX17048模块引脚接到哪里VCC锂电池正极GND锂电池负极同时接到Pico 2的GNDSDAPico 2的GP0SCLPico 2的GP1接线顺序我建议先接电源和地再接SDA和SCL这样不容易乱。特别要注意的是共地问题MAX17048的GND必须和RP2350的GND连在一起否则I2C信号根本没有参考电平通信是不可能正常的。我之前遇到过一次怎么扫描都找不到设备最后发现是电池负极和开发板没有共地两个地之间悬空总线完全瘫痪。还有一个细节是MAX17048模块上最好焊一个0.1uF的陶瓷电容在VCC和GND之间。很多成品模块已经带了但如果是自己搭芯片这个电容一定要加。原因很简单电量计芯片内部需要稳定的参考电压来做AD转换电源纹波太大会让读出来的电压值来回跳。加了去耦电容之后读数会稳定不少。2.3 I2C上拉电阻的两种常见情况I2C总线的工作原理是开漏输出也就是说设备只能把SDA和SCL拉低不能主动拉高所以必须外部接上拉电阻到电源。绝大多数MAX17048成品模块都自带4.7k或者10k的上拉电阻接上就能用。但如果你买的是裸芯片那就得自己处理上拉电阻。我建议在SDA和SCL上各接一个4.7k电阻另一端接到3.3V。为什么不建议直接拉到电池正极因为电池满电的时候电压能到4.2V而RP2350的GPIO耐压一般不推荐超过3.6V如果模块的上拉电阻是拉到电池端的SDA/SCL的高电平就会被拉到4.2V长时间运行对GPIO有损伤风险。有些模块自己做了电平转换那就没问题没有的话最好把上拉电阻拉到3.3V。不过由于RP2350的GPIO内部有钳位二极管短时间实验一般不会烧但长期产品设计不值得这么赌。3. 核心原理一次弄懂MAX17048的I2C寄存器3.1 MAX17048到底是怎么算出电量的既然用到了这个东西最好还是理解一下它的工作思路以后换芯片、调参数心里才有底。MAX17048用的是Maxim的ModelGauge算法它不需要采样电阻而是通过电池电压的变化来推断剩余电量。你可以这样理解锂电池在静置状态下电压和电量之间存在一条相对稳定的关系曲线这就是OCV开路电压曲线。但设备一旦开始工作电池内阻会让端电压下降而且下降的幅度和电流、温度、电池老化程度都有关系。如果单纯根据当前电压查表年轻电池和老化电池的结果完全不一样。MAX17048的做法是持续采样电压并结合电压变化率和历史数据来动态修正估算结果。启动后它其实会花一点时间“学习”当前电池的状态这也是为什么有些用户刚上电时发现SOC并不是很准用一段时间之后就稳了。对开发者来说这些过程都是芯片内部自动完成的我们只需要通过I2C读寄存器就行。最重要的两个寄存器是VCELL和SOC一个给电压一个给百分比。3.2 常用寄存器速查表MAX17048的寄存器基本都是16位也就是每次读两个字节高位在前。我整理了一份常用寄存器表方便大家对照使用。寄存器地址名称功能说明数据换算0x00STATUS状态寄存器包含低电量标志、SOC变化标志按位解析0x02VCELL电池电压读数原始值乘以1.25单位mV0x04SOC剩余电量原始值除以256单位%0x06VERSION芯片版本号只读0x0CCONFIG配置寄存器可设置报警阈值、休眠等按位解析VCELL的LSB最低有效位是1.25mV也就是说你读到一个16位原始值raw实际的电压毫伏数就是raw乘1.25。举个例子如果寄存器值是0x0F20十进制3872那电压就是3872乘以1.25等于4840mV也就是4.84V。不过正常锂电池满电也就4.2V左右所以读出来的值通常不会超过这个。SOC寄存器稍微特殊一点它的高字节是百分比整数部分低字节是百分比小数部分整体除以256就能得到一个小数形式的百分比。比如寄存器值0x6400十进制25600除以256就是100.0%表示电量满格。如果寄存器值0x3200十进制12800除以256就是50.0%。3.3 I2C地址和读写时序的坑MAX17048的7位I2C地址是0x36。这个地址在使用时有一个特别容易混淆的点MicroPython的readfrom_mem函数里我们传的是7位地址0x36但如果你用Linux系统的i2c-tools或者某些库可能会遇到要传8位地址的情况写地址是0x6C读地址是0x6D。其实0x6C就是0x36左移一位得到的0x6D就是左移一位再加1。很多新手扫不到设备就是因为地址形式搞错了。I2C读取一个16位寄存器的完整过程是这样的主机先发出START条件然后发送设备地址加写位也就是0x6C从机应答接着发送寄存器地址比如0x04从机应答然后主机发出重复起始条件Repeated START再发送设备地址加读位也就是0x6D从机应答最后主机连续读两个字节收到第二个字节后主机回复NACK并发送STOP条件。如果你用逻辑分析仪抓波形看到的就是这么一串。MicroPython的readfrom_mem函数把这套流程封装好了但明白原理对排查问题很有帮助。I2C总线为什么需要上拉电阻、为什么是开漏输出这里也顺带说一句。开漏输出意味着设备只能拉低总线不能主动输出高电平高电平完全靠上拉电阻提供。这样设计的最大好处是多个设备可以安全地共用总线不会因为一个设备输出高、另一个输出低而导致短路。通信双方通过控制SDA和SCL的拉低时序来传输数据逻辑上有点像几个水龙头共用一根排水管谁放水谁拉低不放水的时候管道靠上拉电阻维持“空管”状态。4. 实操环节RP2350上跑通MicroPython完整代码4.1 第一步给RP2350烧录MicroPython固件如果你手头的Pico 2还是全新的第一次使用需要烧录MicroPython固件。步骤很简单去MicroPython官网的Download页面找到针对RP2350或者Raspberry Pi Pico 2的.uf2文件下载下来。然后按住Pico 2板子上的BOOTSEL按键不放用USB线连接到电脑这时候电脑会出现一个名为RP2350的U盘盘符。把下载好的.uf2文件直接拖进这个U盘板子会自动重启MicroPython固件就烧录完成了。开发环境我推荐用Thonny界面简洁还能直接在编辑器里设置解释器为MicroPython (Raspberry Pi Pico 2)。如果你喜欢命令行也可以用串口终端连接板子上的虚拟串口直接进REPL交互环境。这一步其实非常快只要固件下载好了整个烧录过程也就十几秒。4.2 完整代码可直接复制运行下面是完整的MicroPython代码实现了I2C扫描、读取电压、读取SOC、循环打印和低电压提醒。代码里加了详细注释即使你之前没用过I2C照着敲也能跑起来。from machine import Pin, I2C import utime # MAX17048的7位I2C地址 MAX17048_ADDR 0x36 # 常用寄存器地址 REG_VCELL 0x02 # 电池电压寄存器的16位读数LSB 1.25mV REG_SOC 0x04 # 剩余电量寄存器除以256得到百分比 # 初始化I2C0SDAGP0SCLGP1400kHz i2c I2C(0, sclPin(1), sdaPin(0), freq400_000) def check_device(): 扫描I2C总线确认MAX17048是否在线 devices i2c.scan() if MAX17048_ADDR in devices: return True return False def read_vcell(): 读取电池电压返回单位V data i2c.readfrom_mem(MAX17048_ADDR, REG_VCELL, 2) raw int.from_bytes(data, big) # 原始值 * 1.25mV再转成V return raw * 1.25 / 1000.0 def read_soc(): 读取剩余电量百分比返回单位% data i2c.readfrom_mem(MAX17048_ADDR, REG_SOC, 2) raw int.from_bytes(data, big) # 高字节为整数百分比低字节为小数部分除以256得到百分比 return raw / 256.0 if not check_device(): print(未发现MAX17048请检查接线和供电) raise SystemExit print(MAX17048 连接正常) print(Vcell( V ) SOC( % )) while True: v read_vcell() soc read_soc() print({:.3f} {:.1f}.format(v, soc)) # 简单低电压提醒低于3.0V时打印警告 if v 3.0: print(警告电池电压过低) utime.sleep(1)代码逻辑不复杂check_device先通过i2c.scan()确认0x36地址在总线上read_vcell和read_soc分别读取两个关键寄存器并做了单位换算主循环每秒打印一次电压和电量同时判断电压是不是跌破了3.0V。这里要注意的是readfrom_mem的第三个参数是读取字节数对于VCELL和SOC都要读2字节因为它们是16位寄存器。如果只读1字节你拿到的就只有高字节SOC的小数部分和VCELL的完整分辨率都会丢。int.from_bytes(data, big)是以大端方式把字节数组拼成整数I2C设备寄存器基本都遵循大端。4.3 运行效果和常见输出解读接好线、烧好固件之后运行代码我这里的实际输出大概是这样的。MAX17048 连接正常 Vcell( V ) SOC( % ) 4.112 98.4 4.105 98.0 4.101 97.7你可以拿万用表测一下电池正负极的实际电压和代码里打印的Vcell对比正常情况下误差应该在几十毫伏以内。如果误差很大优先怀疑模块焊接或供电有问题。SOC会在充放电过程中缓慢变化不会像电压那样瞬间跳动这恰恰说明电量计做了一定平滑处理比直接读ADC要稳定得多。跑通这一步之后你就可以把代码集成到自己的项目里。比如加一个OLED屏幕显示电量百分比或者在电量低于20%时让蜂鸣器报警甚至把数据通过USB串口发到上位机画实时曲线。代码本身不需要大改核心就是读寄存器的三个函数业务逻辑想怎么加都行。5. 常见问题与排查实录帮你少走弯路5.1 I2C扫描不到0x36地址这是出现频率最高的问题没有之一。我总结了几个排查方向按顺序试基本都能解决。先看供电MAX17048必须要有VCC供电才能工作也就是锂电池要接在模块上很多模块没有电池就表现为扫描不到。然后用万用表量一下模块上的VCC和GND之间电压确认确实在2.5V到4.5V这个区间。再看接线SDA和SCL是不是接反了GP0和GP1有没有弄混。逻辑线交叉接反是最常见的新手错误因为很多模块排针顺序是VCC、GND、SDA、SCL但有些模块可能是VCC、SDA、SCL、GND拿到板子先对着丝印看清楚。接着看上拉电阻如果模块没带电阻而你又没外接那总线根本没有高电平设备再正常也扫描不到。用万用表量SDA或SCL对GND电压正常应该接近3.3V如果一直是0说明上拉电阻缺失或者SCL/SDA被拉死。最后别忘了排除地址格式问题。i2c.scan()返回的是7位地址所以你应该在里面找到0x36。如果你手里模块的资料上写了0x6C那是8位写地址换算成7位要右移一位也就是0x36。我见过好几个朋友在代码里写0x6C然后怎么扫都找不到其实不是设备问题是地址格式理解错了。5.2 SOC读数明显不准或者长时间卡在某个值如果你刚焊好板子第一次上电读取SOC发现它和实际电量对不上先别急着怀疑芯片坏了。ModelGauge算法有一个自学习过程它需要根据电池的充放电历史慢慢修正。我的建议是让电池完整充放电一两个循环SOC会逐渐趋向真实值。还有一种情况是电池带保护板而且保护板因为过放已经进入保护状态。这个时候你会量到电池有电压但无法正常放电SOC可能显示得奇奇怪怪。解决办法是换一块确认正常的电池或者对保护板做个重新激活。温度对SOC也有影响冬天在室外做测试电池电压会偏低SOC可能会比预期低一些。MAX17048内部虽然有一些温度修正逻辑但不同电池的特性差异还是会在极端温度下体现出来。如果你是做低温产品最好在实际使用温度下做一轮充放电校准然后把CONFIG寄存器里的RCOMP补偿值调整一下不过这个属于进阶调优正常室内项目可以不折腾。5.3 通信偶尔失败、读数卡死的处理方法如果代码运行一段时间后突然读不到数据或者程序卡在I2C读取上多半是总线进入了异常状态。比较常见的原因是设备地址占线或者时序被干扰。最简单的处理办法是给I2C初始化加个异常捕获每次读取失败就重新初始化一次总线这样比让程序死等要稳定得多。还有一个值得养成的习惯是降低I2C频率。400kHz是高速模式大多数情况下没问题但如果你的杜邦线很长或者面包板寄生电容太大400kHz的边沿会变差进而导致通信误码。这时候把freq改成100_000问题往往迎刃而解。I2C的通信速率对MAX17048这种电量计来说完全够用别为了追求高速而牺牲稳定性。对了调试I2C还有一个利器就是逻辑分析仪。网上那种几十块钱的8通道逻辑分析仪配Sigrok或者PulseView软件把SDA和SCL两根线夹上去就能直接解码出I2C的地址、寄存器号和数据。我在排查问题的时候最喜欢用它来确认主控到底有没有发对地址、设备有没有回ACK。有时候代码看起来正确但波形一抓就发现起始条件拼错了这一类问题用逻辑分析仪十秒钟就能定位比瞎改代码效率高得多。6. 项目扩展从读电量到完整低电量管理6.1 低电量报警的实现思路读完电压和SOC很多人下一步就想着做低电量保护。MAX17048的STATUS寄存器里有低电量标志位同时CONFIG寄存器可以配置报警阈值。比较简单的做法是在主循环里直接判断SOC小于20%就点亮一个LED小于10%就关掉一些外设。更严谨的做法是读取STATUS寄存器让芯片在电量跨过阈值时主动拉低ALRT引脚触发RP2350的外部中断。不过要注意MAX17048判断的报警阈值默认是32%还是多少来着不同配置不太一样具体要去看寄存器说明。我实际做的时候并没有依赖芯片的中断而是直接在主循环里轮询SOC然后根据电压值做双重判断。因为对于手持设备来说1秒钟的轮询周期已经很快了没必要为了省电去抠中断。我建议你在做低电量逻辑时不要只看电压值也不要只看SOC而是用SOC为主、电压为辅。SOC相对平滑适合做趋势判断电压响应快适合做紧急保护。比如SOC低于15%开始提醒用户电压低于3.1V直接强制关机这种组合逻辑会可靠很多。6.2 加上OLED显示电量给Pico 2外接一个SSD1306的0.96寸OLED显示效果会直观很多。MicroPython的ssd1306驱动是很成熟的开源库I2C地址一般是0x3C。你的RP2350已经初始化了I2C0你可以把OLED也挂到同一条I2C总线上因为I2C本身支持多设备MAX17048的0x36和OLED的0x3C不会冲突。接线就更简单了OLED的SDA和SCL分别接GP0和GP1VCC和GND接3.3V和地。然后代码里初始化两个设备循环读取MAX17048的数据再通过OLED的text方法把电压和电量画出来。如果你在画电池图标的时候注意一下坐标把百分比数字放在图标中间看起来就跟真实手机状态栏差不多了。这里有个小提示OLED屏幕刷新不要太猛每秒刷新一次就好刷新太快不仅肉眼分辨不出来还会浪费电。既然项目本身是做电源管理那么功耗意识从第一天开始就要有。6.3 多电池、多路电量计怎么做如果项目里有两块电池比如一个主电池加一个备用电池你可以让RP2350分时读取两个MAX17048。但要注意I2C地址冲突的问题因为MAX17048的地址固定是0x36你没法直接用软件区分两块电池。解决办法有两个。第一个办法是用RP2350的两个独立I2C控制器I2C0挂第一块电池的电量计I2C1挂第二块用两套SDA和SCL引脚分开读取互不干扰代码也只多几行初始化。第二个办法是给每块电池的电量计增加I2C开关比如用TCA9548A多路复用器选通不同通道再去读取适合需要接很多块电池的场景。对于大多数人来说第一块电池的电量计能不能稳定跑通才是重点。等你把单个MAX17048彻底吃透再考虑扩展多电池方案也不迟。这个芯片的I2C时序不复杂寄存器结构也清晰是个非常适合练手的外设。最后再分享一个我的实际感受MAX17048这种“接口简单但内部有算法”的芯片特别适合做嵌入式项目的电源管理模块。比起自己在ADC前端做滤波、建表、拟合曲线直接在I2C上读两个寄存器就能拿到稳定的SOC开发效率和可维护性都高非常多。你把通用的I2C读写封装成小函数以后换任何一块I2C设备都能复用。这篇内容里提到的地址格式、上拉电阻、共地这些问题其实也不只适用于MAX17048你在调其他I2C传感器时同样会碰到。先把这条路走通后面做低功耗设备、做电源管理都会顺手很多。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进