← How-Tos
electronics Aug 7, 2026 ◑ 6 views ◯ 5 min read

Battery Fuel Gauge ICs for Lithium Projects: MAX17048, BQ27441, and Coulomb Counting Explained

batteryfuel gaugemax17048bq27441coulomb countinglithiumlipoli ionstate of chargei2cesp32arduino

Every lithium-powered project eventually asks the same question: how much battery is actually left? A raw voltage reading gets you a rough guess at best — lithium chemistries hold a stubbornly flat voltage curve through most of their discharge, so a cell can read 3.7V at 80% charge and 3.7V at 30% charge, and the same battery under load sags differently depending on temperature and discharge rate. If your ESP32 weather station or handheld tool reports "battery OK" right up until it browns out, you're reading voltage when you should be counting coulombs. This guide covers the two most common fuel gauge ICs makers reach for — the simple MAX17048 and the full-featured BQ27441 — how they work, how to wire and read them, and when a $2 voltage divider is actually good enough.

Why Voltage Alone Lies to You

A single-cell Li-ion or LiPo discharge curve is steep at the very top and very bottom of its range and nearly flat in the middle — often within 100-150mV across 20% to 80% state of charge. Add in the voltage sag caused by internal resistance under load (worse as the cell ages or gets cold), and a simple analog read on a voltage divider can be off by 20-30% of actual remaining capacity. That's fine for a "low battery" warning LED. It's not fine for a project that needs to know it has 45 minutes of runtime left before shutting down gracefully, or a device you're trying to keep off a dead-battery restart loop.

Fuel gauge ICs solve this two different ways. The MAX17048 uses a proprietary algorithm (ModelGauge) that combines voltage tracking with an internal model of lithium discharge behavior — no shunt resistor needed. The BQ27441 does true coulomb counting: it measures current in and out of the cell through a tiny sense resistor and integrates it over time, continuously learning the pack's actual capacity as it cycles. Both report state of charge as a clean percentage over I2C, which is what you actually want in code.

MAX17048: Minimal Wiring, Good Enough for Most Projects

The MAX17048 (and its 2-cell sibling, the MAX17049) is the easiest fuel gauge to add to a project because it needs almost nothing external — no sense resistor, no calibration cycle, just four wires.

MAX17048 PinConnects To VDD3.3V (ESP32/Arduino logic rail) GNDCommon ground SDAESP32 GPIO21 (or your I2C SDA) SCLESP32 GPIO22 (or your I2C SCL) CELL+Battery positive (through the same tap point as your charge circuit)

Most breakout boards (Adafruit's MAX17048 board is the common one) already include the pull-up resistors on SDA/SCL, so you can wire it directly onto the same I2C bus as other sensors. In Arduino code with the Adafruit_MAX1704X library:

#include <Adafruit_MAX1704X.h> Adafruit_MAX17048 lipo; void setup() { Serial.begin(115200); if (!lipo.begin()) { Serial.println("MAX17048 not found"); while (1) delay(10); } } void loop() { float voltage = lipo.cellVoltage(); float percent = lipo.cellPercent(); Serial.printf("Batt: %.2fV %.1f%%\n", voltage, percent); delay(2000); }

The chip also exposes an alert pin you can wire to an ESP32 GPIO and configure for a low-battery interrupt (default threshold 32%, adjustable in registers), which is a cleaner way to trigger a graceful shutdown than polling in your main loop.

BQ27441: True Coulomb Counting for Projects That Need Accuracy

The BQ27441 is a step up in both accuracy and complexity. It sits between the battery and the load, measuring current through an internal or external sense resistor (typically 10mΩ on breakout boards like SparkFun's), and integrates that current over time to track exactly how many mAh have gone in and out. Because it "learns" the pack's actual full-charge capacity across cycles rather than relying purely on a voltage model, it stays accurate as the battery ages — something the MAX17048 can't do since it has no idea how much current is actually flowing.

Setup requires telling the chip your battery's design capacity and (ideally) its taper current for correct chemistry modeling. Using SparkFun's BQ27441 Arduino library:

#include <SparkFunBQ27441.h> #define BATTERY_CAPACITY 2000 // mAh, from your cell's datasheet void setup() { Serial.begin(115200); lipo.begin(); lipo.setCapacity(BATTERY_CAPACITY); } void loop() { Serial.printf("SoC: %d%% Voltage: %dmV Current: %dmA FullCap: %dmAh\n", lipo.soc(), lipo.voltage(), lipo.current(AVG), lipo.capacity(FULL)); delay(2000); }

For best long-term accuracy, run the pack through at least one full charge-to-full-discharge cycle after first power-on so the gauge's learning algorithm can correct its internal capacity estimate — right out of the box it's working from the design capacity you gave it, which drifts from reality as the cell ages.

Choosing Between Them

MAX17048BQ27441 Wiring complexity4 wires, no sense resistorBattery routed through gauge, sense resistor AccuracyGood (~2-3% typical)Better, improves with cycling (~1%) Ages with batteryNo — fixed modelYes — learns actual capacity Setup effortNoneSet design capacity, ideally cycle once Good forSensor nodes, quick projects, deep-sleep IoTTools, power banks, anything users rely on for runtime

For an ESP32 sensor node reporting battery percentage back to Home Assistant a few times a day, the MAX17048 is plenty and dramatically simpler to wire. For a project where someone is actively relying on a runtime estimate — a portable power station, a handheld device, anything where "how much time do I have left" matters — the BQ27441's coulomb counting is worth the extra wiring.

Common Pitfalls

Once you've got real state-of-charge data instead of a raw voltage guess, it's worth feeding it into whatever power-management logic your project already has — pairing this with deep sleep scheduling (see our ESP32 deep sleep guide) gets you both accurate runtime reporting and the battery life to make that runtime worth reporting.