鋰電池電量檢測)
這段時間給一個手持小設(shè)備做電源管理主控選了樹莓派RP2350也就是Pico 2上那顆新MCU電池呢是常見的3.7V鋰電池。設(shè)備要做電量顯示最初我用電阻分壓加ADC讀電壓、再換算成電量結(jié)果在低電量階段誤差大得離譜同一個電池放電電流大一點電壓就掉到3.2V放一會又回彈。后來換上了MAX17048電量計I2C接口讀電壓和剩余電量都變成幾行代碼的事從接線到跑通實測下來確實5分鐘就能看到數(shù)據(jù)。這篇文章把完整接線、原理、MicroPython代碼和常見坑全記錄下來適合正在做Pico 2項目、畢設(shè)電源管理或者想給DIY設(shè)備加電量顯示的朋友參考。1. 為什么不用電阻分壓加ADCMAX17048的定位1.1 電量測量的三種常見方案對比先別急著看代碼我覺得有必要把為什么選MAX17048講清楚。很多做嵌入式的小伙伴一開始都會想著“不就是測個電壓嘛ADC加兩個電阻就搞定了”我以前也是這么干的但踩過幾次坑之后發(fā)現(xiàn)事情沒那么簡單?,F(xiàn)在主流的電量測量方案大概有三類。第一類是純電壓讀取法就是電阻分壓加上單片機ADC查一個電壓-電量對照表來換算。這個方法成本最低但是有兩個很致命的問題鋰電池的放電曲線并不是一條直線在3.7V到3.3V這段尤其陡峭電壓稍微波動一點算出來的電量就偏差好幾個百分點而且電池內(nèi)阻會隨著放電電流變化電機一轉(zhuǎn)、屏幕一亮電壓馬上掉下去你讀到的值根本不是真實的開路電壓而是帶負載的瞬時電壓這時候算電量就是自欺欺人。第二類是庫侖計法也叫電流積分法通過一顆精密采樣電阻串聯(lián)在電池回路里用ADC持續(xù)采樣電流并積分從而得到充放電電量。這個方法精度不錯但需要額外考慮采樣電阻的溫漂、放大電路的零點漂移BOM成本和PCB面積都上去了對于一個小型手持設(shè)備來說有點重。第三類就是MAX17048這類ModelGauge電量計方案。它不需要外置采樣電阻芯片直接采集電池正端電壓內(nèi)部通過電壓曲線、電壓變化率以及電池老化模型來估算剩余電量。整體外圍電路就是一個去耦電容。它把復(fù)雜擬合過程全部做在芯片內(nèi)部對外提供I2C接口你讀兩個寄存器就能拿到電壓和SOCState of Charge剩余電量百分比這也是它在消費電子產(chǎn)品里應(yīng)用非常廣的原因。方案精度外圍成本適用場景電阻分壓ADC較低受負載影響大最低玩具、對電量要求不高的設(shè)備庫侖計較高高需要精密采樣電阻高精度監(jiān)測、容量分析MAX17048 ModelGauge中等偏上低只需一顆電容手持設(shè)備、低功耗設(shè)備1.2 我選擇MAX17048的3個實際理由第一是成本低模塊在市面上幾塊錢到十幾塊錢就能買到芯片本身封裝也很小適合集成到自己的PCB里。第二是開發(fā)效率高I2C通信大家都很熟而且它提供的SOC寄存器直接就是百分比數(shù)據(jù)不需要自己查表擬合省掉了最麻煩的調(diào)試環(huán)節(jié)。第三是功耗低MAX17048的工作電流據(jù)說在微安級別對于電池供電的設(shè)備來說很友好不會因為加了電量計反而加快掉電。當然它也有不擅長的地方。ModelGauge本質(zhì)上還是基于電壓估算的如果你要求的是“絕對嚴謹?shù)膸靵黾売嬃烤取彼隙ú蝗鐜Р蓸与娮璧膸靵鲇嬓酒5窃诖蠖鄶?shù)產(chǎn)品里用戶只是想看到“還有多少電、還能用多久”一個不太離譜的數(shù)值MAX17048完全夠用。至少在Pico 2這種級別的項目里它的精度和成本平衡得非常漂亮。2. 硬件準備與接線5分鐘里最關(guān)鍵的1分鐘2.1 物料清單和選型建議做這個實驗不需要太多東西我列一下我實際用的物料。樹莓派RP2350開發(fā)板也就是Raspberry Pi Pico 2一定注意別買成老的RP2040 Pico雖然代碼邏輯一樣但標題既然說了RP2350我們就用新的芯片。MAX17048電量計模塊買那種已經(jīng)把I2C上拉電阻做好的成品模塊最省事很多模塊還帶引腳座直接杜邦線就能連。3.7V鋰電池最好是帶保護板的防止實驗過程中過放。面包板、杜邦線若干如果模塊沒焊排針需要先焊一下。有條件的話準備一塊萬用表排查問題時會很有幫助。這里有個容易被忽略的點MAX17048的VCC工作電壓范圍一般是2.5V到4.5V所以可以直接接鋰電池正極不需要額外穩(wěn)壓。它測的就是VCC引腳上的電池電壓。但如果你的模塊上帶穩(wěn)壓電路那就要看模塊說明書搞清楚VCC到底應(yīng)該接電池還是接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信號根本沒有參考電平通信是不可能正常的。我之前遇到過一次怎么掃描都找不到設(shè)備最后發(fā)現(xiàn)是電池負極和開發(fā)板沒有共地兩個地之間懸空總線完全癱瘓。還有一個細節(jié)是MAX17048模塊上最好焊一個0.1uF的陶瓷電容在VCC和GND之間。很多成品模塊已經(jīng)帶了但如果是自己搭芯片這個電容一定要加。原因很簡單電量計芯片內(nèi)部需要穩(wěn)定的參考電壓來做AD轉(zhuǎn)換電源紋波太大會讓讀出來的電壓值來回跳。加了去耦電容之后讀數(shù)會穩(wěn)定不少。2.3 I2C上拉電阻的兩種常見情況I2C總線的工作原理是開漏輸出也就是說設(shè)備只能把SDA和SCL拉低不能主動拉高所以必須外部接上拉電阻到電源。絕大多數(shù)MAX17048成品模塊都自帶4.7k或者10k的上拉電阻接上就能用。但如果你買的是裸芯片那就得自己處理上拉電阻。我建議在SDA和SCL上各接一個4.7k電阻另一端接到3.3V。為什么不建議直接拉到電池正極因為電池滿電的時候電壓能到4.2V而RP2350的GPIO耐壓一般不推薦超過3.6V如果模塊的上拉電阻是拉到電池端的SDA/SCL的高電平就會被拉到4.2V長時間運行對GPIO有損傷風險。有些模塊自己做了電平轉(zhuǎn)換那就沒問題沒有的話最好把上拉電阻拉到3.3V。不過由于RP2350的GPIO內(nèi)部有鉗位二極管短時間實驗一般不會燒但長期產(chǎn)品設(shè)計不值得這么賭。3. 核心原理一次弄懂MAX17048的I2C寄存器3.1 MAX17048到底是怎么算出電量的既然用到了這個東西最好還是理解一下它的工作思路以后換芯片、調(diào)參數(shù)心里才有底。MAX17048用的是Maxim的ModelGauge算法它不需要采樣電阻而是通過電池電壓的變化來推斷剩余電量。你可以這樣理解鋰電池在靜置狀態(tài)下電壓和電量之間存在一條相對穩(wěn)定的關(guān)系曲線這就是OCV開路電壓曲線。但設(shè)備一旦開始工作電池內(nèi)阻會讓端電壓下降而且下降的幅度和電流、溫度、電池老化程度都有關(guān)系。如果單純根據(jù)當前電壓查表年輕電池和老化電池的結(jié)果完全不一樣。MAX17048的做法是持續(xù)采樣電壓并結(jié)合電壓變化率和歷史數(shù)據(jù)來動態(tài)修正估算結(jié)果。啟動后它其實會花一點時間“學習”當前電池的狀態(tài)這也是為什么有些用戶剛上電時發(fā)現(xiàn)SOC并不是很準用一段時間之后就穩(wěn)了。對開發(fā)者來說這些過程都是芯片內(nèi)部自動完成的我們只需要通過I2C讀寄存器就行。最重要的兩個寄存器是VCELL和SOC一個給電壓一個給百分比。3.2 常用寄存器速查表MAX17048的寄存器基本都是16位也就是每次讀兩個字節(jié)高位在前。我整理了一份常用寄存器表方便大家對照使用。寄存器地址名稱功能說明數(shù)據(jù)換算0x00STATUS狀態(tài)寄存器包含低電量標志、SOC變化標志按位解析0x02VCELL電池電壓讀數(shù)原始值乘以1.25單位mV0x04SOC剩余電量原始值除以256單位%0x06VERSION芯片版本號只讀0x0CCONFIG配置寄存器可設(shè)置報警閾值、休眠等按位解析VCELL的LSB最低有效位是1.25mV也就是說你讀到一個16位原始值raw實際的電壓毫伏數(shù)就是raw乘1.25。舉個例子如果寄存器值是0x0F20十進制3872那電壓就是3872乘以1.25等于4840mV也就是4.84V。不過正常鋰電池滿電也就4.2V左右所以讀出來的值通常不會超過這個。SOC寄存器稍微特殊一點它的高字節(jié)是百分比整數(shù)部分低字節(jié)是百分比小數(shù)部分整體除以256就能得到一個小數(shù)形式的百分比。比如寄存器值0x6400十進制25600除以256就是100.0%表示電量滿格。如果寄存器值0x3200十進制12800除以256就是50.0%。3.3 I2C地址和讀寫時序的坑MAX17048的7位I2C地址是0x36。這個地址在使用時有一個特別容易混淆的點MicroPython的readfrom_mem函數(shù)里我們傳的是7位地址0x36但如果你用Linux系統(tǒng)的i2c-tools或者某些庫可能會遇到要傳8位地址的情況寫地址是0x6C讀地址是0x6D。其實0x6C就是0x36左移一位得到的0x6D就是左移一位再加1。很多新手掃不到設(shè)備就是因為地址形式搞錯了。I2C讀取一個16位寄存器的完整過程是這樣的主機先發(fā)出START條件然后發(fā)送設(shè)備地址加寫位也就是0x6C從機應(yīng)答接著發(fā)送寄存器地址比如0x04從機應(yīng)答然后主機發(fā)出重復(fù)起始條件Repeated START再發(fā)送設(shè)備地址加讀位也就是0x6D從機應(yīng)答最后主機連續(xù)讀兩個字節(jié)收到第二個字節(jié)后主機回復(fù)NACK并發(fā)送STOP條件。如果你用邏輯分析儀抓波形看到的就是這么一串。MicroPython的readfrom_mem函數(shù)把這套流程封裝好了但明白原理對排查問題很有幫助。I2C總線為什么需要上拉電阻、為什么是開漏輸出這里也順帶說一句。開漏輸出意味著設(shè)備只能拉低總線不能主動輸出高電平高電平完全靠上拉電阻提供。這樣設(shè)計的最大好處是多個設(shè)備可以安全地共用總線不會因為一個設(shè)備輸出高、另一個輸出低而導致短路。通信雙方通過控制SDA和SCL的拉低時序來傳輸數(shù)據(jù)邏輯上有點像幾個水龍頭共用一根排水管誰放水誰拉低不放水的時候管道靠上拉電阻維持“空管”狀態(tài)。4. 實操環(huán)節(jié)RP2350上跑通MicroPython完整代碼4.1 第一步給RP2350燒錄MicroPython固件如果你手頭的Pico 2還是全新的第一次使用需要燒錄MicroPython固件。步驟很簡單去MicroPython官網(wǎng)的Download頁面找到針對RP2350或者Raspberry Pi Pico 2的.uf2文件下載下來。然后按住Pico 2板子上的BOOTSEL按鍵不放用USB線連接到電腦這時候電腦會出現(xiàn)一個名為RP2350的U盤盤符。把下載好的.uf2文件直接拖進這個U盤板子會自動重啟MicroPython固件就燒錄完成了。開發(fā)環(huán)境我推薦用Thonny界面簡潔還能直接在編輯器里設(shè)置解釋器為MicroPython (Raspberry Pi Pico 2)。如果你喜歡命令行也可以用串口終端連接板子上的虛擬串口直接進REPL交互環(huán)境。這一步其實非??熘灰碳螺d好了整個燒錄過程也就十幾秒。4.2 完整代碼可直接復(fù)制運行下面是完整的MicroPython代碼實現(xiàn)了I2C掃描、讀取電壓、讀取SOC、循環(huán)打印和低電壓提醒。代碼里加了詳細注釋即使你之前沒用過I2C照著敲也能跑起來。from machine import Pin, I2C import utime # MAX17048的7位I2C地址 MAX17048_ADDR 0x36 # 常用寄存器地址 REG_VCELL 0x02 # 電池電壓寄存器的16位讀數(shù)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再轉(zhuǎn)成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) # 高字節(jié)為整數(shù)百分比低字節(jié)為小數(shù)部分除以256得到百分比 return raw / 256.0 if not check_device(): print(未發(fā)現(xiàn)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)代碼邏輯不復(fù)雜check_device先通過i2c.scan()確認0x36地址在總線上read_vcell和read_soc分別讀取兩個關(guān)鍵寄存器并做了單位換算主循環(huán)每秒打印一次電壓和電量同時判斷電壓是不是跌破了3.0V。這里要注意的是readfrom_mem的第三個參數(shù)是讀取字節(jié)數(shù)對于VCELL和SOC都要讀2字節(jié)因為它們是16位寄存器。如果只讀1字節(jié)你拿到的就只有高字節(jié)SOC的小數(shù)部分和VCELL的完整分辨率都會丟。int.from_bytes(data, big)是以大端方式把字節(jié)數(shù)組拼成整數(shù)I2C設(shè)備寄存器基本都遵循大端。4.3 運行效果和常見輸出解讀接好線、燒好固件之后運行代碼我這里的實際輸出大概是這樣的。MAX17048 連接正常 Vcell( V ) SOC( % ) 4.112 98.4 4.105 98.0 4.101 97.7你可以拿萬用表測一下電池正負極的實際電壓和代碼里打印的Vcell對比正常情況下誤差應(yīng)該在幾十毫伏以內(nèi)。如果誤差很大優(yōu)先懷疑模塊焊接或供電有問題。SOC會在充放電過程中緩慢變化不會像電壓那樣瞬間跳動這恰恰說明電量計做了一定平滑處理比直接讀ADC要穩(wěn)定得多。跑通這一步之后你就可以把代碼集成到自己的項目里。比如加一個OLED屏幕顯示電量百分比或者在電量低于20%時讓蜂鳴器報警甚至把數(shù)據(jù)通過USB串口發(fā)到上位機畫實時曲線。代碼本身不需要大改核心就是讀寄存器的三個函數(shù)業(yè)務(wù)邏輯想怎么加都行。5. 常見問題與排查實錄幫你少走彎路5.1 I2C掃描不到0x36地址這是出現(xiàn)頻率最高的問題沒有之一。我總結(jié)了幾個排查方向按順序試基本都能解決。先看供電MAX17048必須要有VCC供電才能工作也就是鋰電池要接在模塊上很多模塊沒有電池就表現(xiàn)為掃描不到。然后用萬用表量一下模塊上的VCC和GND之間電壓確認確實在2.5V到4.5V這個區(qū)間。再看接線SDA和SCL是不是接反了GP0和GP1有沒有弄混。邏輯線交叉接反是最常見的新手錯誤因為很多模塊排針順序是VCC、GND、SDA、SCL但有些模塊可能是VCC、SDA、SCL、GND拿到板子先對著絲印看清楚。接著看上拉電阻如果模塊沒帶電阻而你又沒外接那總線根本沒有高電平設(shè)備再正常也掃描不到。用萬用表量SDA或SCL對GND電壓正常應(yīng)該接近3.3V如果一直是0說明上拉電阻缺失或者SCL/SDA被拉死。最后別忘了排除地址格式問題。i2c.scan()返回的是7位地址所以你應(yīng)該在里面找到0x36。如果你手里模塊的資料上寫了0x6C那是8位寫地址換算成7位要右移一位也就是0x36。我見過好幾個朋友在代碼里寫0x6C然后怎么掃都找不到其實不是設(shè)備問題是地址格式理解錯了。5.2 SOC讀數(shù)明顯不準或者長時間卡在某個值如果你剛焊好板子第一次上電讀取SOC發(fā)現(xiàn)它和實際電量對不上先別急著懷疑芯片壞了。ModelGauge算法有一個自學習過程它需要根據(jù)電池的充放電歷史慢慢修正。我的建議是讓電池完整充放電一兩個循環(huán)SOC會逐漸趨向真實值。還有一種情況是電池帶保護板而且保護板因為過放已經(jīng)進入保護狀態(tài)。這個時候你會量到電池有電壓但無法正常放電SOC可能顯示得奇奇怪怪。解決辦法是換一塊確認正常的電池或者對保護板做個重新激活。溫度對SOC也有影響冬天在室外做測試電池電壓會偏低SOC可能會比預(yù)期低一些。MAX17048內(nèi)部雖然有一些溫度修正邏輯但不同電池的特性差異還是會在極端溫度下體現(xiàn)出來。如果你是做低溫產(chǎn)品最好在實際使用溫度下做一輪充放電校準然后把CONFIG寄存器里的RCOMP補償值調(diào)整一下不過這個屬于進階調(diào)優(yōu)正常室內(nèi)項目可以不折騰。5.3 通信偶爾失敗、讀數(shù)卡死的處理方法如果代碼運行一段時間后突然讀不到數(shù)據(jù)或者程序卡在I2C讀取上多半是總線進入了異常狀態(tài)。比較常見的原因是設(shè)備地址占線或者時序被干擾。最簡單的處理辦法是給I2C初始化加個異常捕獲每次讀取失敗就重新初始化一次總線這樣比讓程序死等要穩(wěn)定得多。還有一個值得養(yǎng)成的習慣是降低I2C頻率。400kHz是高速模式大多數(shù)情況下沒問題但如果你的杜邦線很長或者面包板寄生電容太大400kHz的邊沿會變差進而導致通信誤碼。這時候把freq改成100_000問題往往迎刃而解。I2C的通信速率對MAX17048這種電量計來說完全夠用別為了追求高速而犧牲穩(wěn)定性。對了調(diào)試I2C還有一個利器就是邏輯分析儀。網(wǎng)上那種幾十塊錢的8通道邏輯分析儀配Sigrok或者PulseView軟件把SDA和SCL兩根線夾上去就能直接解碼出I2C的地址、寄存器號和數(shù)據(jù)。我在排查問題的時候最喜歡用它來確認主控到底有沒有發(fā)對地址、設(shè)備有沒有回ACK。有時候代碼看起來正確但波形一抓就發(fā)現(xiàn)起始條件拼錯了這一類問題用邏輯分析儀十秒鐘就能定位比瞎改代碼效率高得多。6. 項目擴展從讀電量到完整低電量管理6.1 低電量報警的實現(xiàn)思路讀完電壓和SOC很多人下一步就想著做低電量保護。MAX17048的STATUS寄存器里有低電量標志位同時CONFIG寄存器可以配置報警閾值。比較簡單的做法是在主循環(huán)里直接判斷SOC小于20%就點亮一個LED小于10%就關(guān)掉一些外設(shè)。更嚴謹?shù)淖龇ㄊ亲x取STATUS寄存器讓芯片在電量跨過閾值時主動拉低ALRT引腳觸發(fā)RP2350的外部中斷。不過要注意MAX17048判斷的報警閾值默認是32%還是多少來著不同配置不太一樣具體要去看寄存器說明。我實際做的時候并沒有依賴芯片的中斷而是直接在主循環(huán)里輪詢SOC然后根據(jù)電壓值做雙重判斷。因為對于手持設(shè)備來說1秒鐘的輪詢周期已經(jīng)很快了沒必要為了省電去摳中斷。我建議你在做低電量邏輯時不要只看電壓值也不要只看SOC而是用SOC為主、電壓為輔。SOC相對平滑適合做趨勢判斷電壓響應(yīng)快適合做緊急保護。比如SOC低于15%開始提醒用戶電壓低于3.1V直接強制關(guān)機這種組合邏輯會可靠很多。6.2 加上OLED顯示電量給Pico 2外接一個SSD1306的0.96寸OLED顯示效果會直觀很多。MicroPython的ssd1306驅(qū)動是很成熟的開源庫I2C地址一般是0x3C。你的RP2350已經(jīng)初始化了I2C0你可以把OLED也掛到同一條I2C總線上因為I2C本身支持多設(shè)備MAX17048的0x36和OLED的0x3C不會沖突。接線就更簡單了OLED的SDA和SCL分別接GP0和GP1VCC和GND接3.3V和地。然后代碼里初始化兩個設(shè)備循環(huán)讀取MAX17048的數(shù)據(jù)再通過OLED的text方法把電壓和電量畫出來。如果你在畫電池圖標的時候注意一下坐標把百分比數(shù)字放在圖標中間看起來就跟真實手機狀態(tài)欄差不多了。這里有個小提示OLED屏幕刷新不要太猛每秒刷新一次就好刷新太快不僅肉眼分辨不出來還會浪費電。既然項目本身是做電源管理那么功耗意識從第一天開始就要有。6.3 多電池、多路電量計怎么做如果項目里有兩塊電池比如一個主電池加一個備用電池你可以讓RP2350分時讀取兩個MAX17048。但要注意I2C地址沖突的問題因為MAX17048的地址固定是0x36你沒法直接用軟件區(qū)分兩塊電池。解決辦法有兩個。第一個辦法是用RP2350的兩個獨立I2C控制器I2C0掛第一塊電池的電量計I2C1掛第二塊用兩套SDA和SCL引腳分開讀取互不干擾代碼也只多幾行初始化。第二個辦法是給每塊電池的電量計增加I2C開關(guān)比如用TCA9548A多路復(fù)用器選通不同通道再去讀取適合需要接很多塊電池的場景。對于大多數(shù)人來說第一塊電池的電量計能不能穩(wěn)定跑通才是重點。等你把單個MAX17048徹底吃透再考慮擴展多電池方案也不遲。這個芯片的I2C時序不復(fù)雜寄存器結(jié)構(gòu)也清晰是個非常適合練手的外設(shè)。最后再分享一個我的實際感受MAX17048這種“接口簡單但內(nèi)部有算法”的芯片特別適合做嵌入式項目的電源管理模塊。比起自己在ADC前端做濾波、建表、擬合曲線直接在I2C上讀兩個寄存器就能拿到穩(wěn)定的SOC開發(fā)效率和可維護性都高非常多。你把通用的I2C讀寫封裝成小函數(shù)以后換任何一塊I2C設(shè)備都能復(fù)用。這篇內(nèi)容里提到的地址格式、上拉電阻、共地這些問題其實也不只適用于MAX17048你在調(diào)其他I2C傳感器時同樣會碰到。先把這條路走通后面做低功耗設(shè)備、做電源管理都會順手很多。