Battery Fuel Gauge Design Notes: MAX17048 vs Software Coulomb Counting
A dedicated fuel gauge IC costs $1.20 and handles SOC estimation better than any coulomb-counting firmware I've written. Why I stopped rolling my own and what I gave up.

The MAX17048 (I2C fuel gauge, ~$1.20 at 1k qty) estimates SOC better than any coulomb-counting firmware I've shipped—and it frees 8–12 KB flash plus a timer ISR I used for current integration. I stopped rolling my own on portable products unless we need cell-level visibility or the BOM truly cannot absorb one more IC.
What software coulomb counting gave us
Original approach on STM32L051:
- 10 mΩ shunt → INA226 (I2C) at 16 samples/s
- Integrate
I * dtinto mAh consumed - SOC =
(full_charge_mAh - consumed) / full_charge_mAh - Full charge learned on charger disconnect at low current threshold
Problems in production:
- SOC drift: 8–15% error after 40 cycles without manual recalibration
- Full-capacity aging: never updated automatically; support tickets "dies at 20% reported"
- Sleep current: INA226 always on cost 180 µA; bad for 500 mAh pouch cell
Firmware complexity: ~600 lines plus NVM wear for calibration constants.
MAX17048 integration
// STM32 HAL I2C — MAX17048 default addr 0x36
#define REG_VCELL 0x02 /* 78.125 µV/LSB */
#define REG_SOC 0x04 /* 1/256 % per LSB */
#define REG_MODE 0x06
#define REG_CONFIG 0x0C
float max17048_read_soc(I2C_HandleTypeDef *hi2c) {
uint8_t reg = REG_SOC;
uint8_t buf[2];
HAL_I2C_Master_Transmit(hi2c, 0x36 << 1, ®, 1, 100);
HAL_I2C_Master_Receive(hi2c, 0x36 << 1, buf, 2, 100);
return buf[0] + buf[1] / 256.0f;
}
Layout: place gauge within 10 mm of cell tabs per Maxim layout guide (AN6028). Our first spin had gauge 30 mm away on flex—SOC lagged 4% under load until reroute.
Comparison (single-cell LiPo 500 mAh, 100 cycles bench)
| Method | SOC error (RMSE) | Active extra current | FW footprint |
|---|---|---|---|
| Coulomb count + INA226 | 9.2% | 180 µA | ~12 KB |
| MAX17048 | 3.1% | 15 µA | ~1.5 KB |
| MAX17048 + QuickStart after full charge | 2.4% | 15 µA | ~2 KB |
MAX17048 uses ModelGauge m3—voltage curve + compensation, not true coulomb integration. Under pulsed load (motor spikes), brief SOC dips look pessimistic; acceptable for UX bar, annoying for fuel-critical medical (wrong domain for this part).
What I gave up
- Per-cell balancing insight—single-cell only; pack needs BQ76952 class monitor
- Coulomb audit trail for warranty disputes—gauge SOC is estimate, not legal-grade metrology
- Custom learning for exotic chemistries—LFP needs different IC (e.g. LTC2943 pairing)
Power path sequencing still matters—see power sequencing design notes. Gauge reads nonsense if PMIC enables rail out of order.
USB-C PD front-end confusion caused charge-current misreporting once—USB-C PD confusion notes documents that rabbit hole.
When I still software-count
- Shunt already present for overcurrent protection (same INA226 dual purpose)
- Supercap or NiMH where voltage curve flat
- BOM veto on $1.20 (consumer disposable at 100k qty—math changes)
What I'd do next
- Production test: log SOC vs terminal voltage at known load points; reject assemblies with >5% mismatch at fixture.
- Evaluate MAX17260 for multi-cell pack next SKU.
- Expose
SOC+VCELLin telemetry—field support correlates user complaints faster.
Sleep and hibernate behavior
MAX17048 draws ~15 µA active; entering hibernate mode (REG_MODE bit) drops further but delays first SOC read after wake—200 ms settle we measured. Firmware waits after I2C wake before trusting SOC for low-battery shutdown decision.
Multi chemistry caveat
We tested only single-cell LiPo 3.7 V nominal. LFP flat voltage curve breaks modelgauge assumptions—do not use MAX17048 on LFP without validating error budget; consider coulomb counting with LFP-specific OCV table instead.
QuickStart and learning cycle
MAX17048 QuickStart (REG_MODE 0x4000 pulse) after full charge calibrates model—skip it on first boot from factory and SOC wanders until first customer full cycle. We run QuickStart in fixture after 4.2 V constant-voltage charge at 0.1C; adds 45 min to test but eliminates week-one support calls.
I2C clock stretching
Gauge stretches SCL during internal math (~500 µs bursts). STM32 I2C at 400 kHz handled it; bit-banged GPIO I2C on cost-reduced variant did not—reads returned stuck SOC. Hardware I2C or lower clock mandatory.
Fixture calibration drift
Fuel gauge SOC at end-of-line matched bench meter within 3% for six months, then drifted when fixture charge cable resistance increased—0.15 Ω added enough IR drop to trigger early QuickStart. Include charge path impedance in fixture PM checklist, not just gauge IC.
Temperature compensation table
Optional LUT from gauge SOC error vs temperature improved cold-start UX 2% RMSE in chamber—not required for consumer SKU, mandatory for medical-adjacent pilot we declined after BOM review.
Low-SOC shutdown hysteresis
Shutdown at 5% SOC with 2% hysteresis on restart prevented oscillation at boundary—gauge pessimism meant device stayed off until USB attached; UX copy updated to explain "reserve battery" behavior honestly.
Buy the gauge IC. Spend firmware time on features customers pay for, not reinventing Integrate(current, dt).
Manish Bookreader
Electronics enthusiast, Embedded Systems Expert, Linux/Networking programmer, and Software Engineer passionate about AI, electronics, books, and cooking.

