The best way to validate embedded algorithms isn't with perfect hardware—it's with perfect software running on imperfect hardware. This insight comes from Aliaksandr Liapin's experience shipping coulomb counting functionality in his open-source iBattery SDK while his breadboard prototypes were still throwing tantrums.
Coulomb counting is the backbone of battery management systems. It estimates State of Charge (SoC) by integrating current over time—essentially keeping a running tally of electrons flowing in and out of your battery. The math is deceptively simple:
1float update_soc(float prev_soc, float current_ma, float capacity_ah, float dt_s) {
2 float delta_ah = (current_ma / 1000.0f) * (dt_s / 3600.0f); // mA*s to Ah
3 float new_soc = prev_soc + (delta_ah / capacity_ah) * 100.0f;
4 return fminf(fmaxf(new_soc, 0.0f), 100.0f); // Clamp 0-100%
5}But simple math doesn't mean simple implementation. Real coulomb counting has to handle current sensor noise, timing jitter, temperature drift, and the gnarly reality that battery capacity degrades over time. Each of these factors can introduce 1-2% error, and they compound.
The Hardware Trap
Most embedded developers fall into the same trap: waiting for hardware to be "ready" before implementing algorithms. The logic seems sound—how can you test battery algorithms without reliable current measurements? But this creates a dependency chain that kills momentum.
Liapin flipped this approach. Instead of waiting for his breadboard current sensing to stabilize, he implemented the full coulomb counting algorithm using simulated data. This meant:
- Algorithm validation happened in software where debugging is fast and reproducible
- Edge cases could be tested systematically without waiting for specific battery conditions
- The SDK could ship while hardware iterations continued in parallel
- Users could experiment with the API before building hardware
<> The discipline of shipping software that works—even when dependent hardware doesn't—forces you to build better abstractions and more robust error handling./>
This isn't just about battery management. It's about recognizing that software and hardware have different iteration speeds. Software can be refactored in minutes; hardware revisions take weeks.
Building Coulomb Counting for Reality
The real art in coulomb counting isn't the integration—it's handling the errors. Professional implementations need several layers of sophistication:
Sensor Calibration: Current sensors drift, especially at low currents where precision matters most. You need periodic zero-current calibration and gain correction.
1typedef struct {
2 float offset_ma; // Zero-current offset
3 float gain_factor; // Calibration multiplier
4 float noise_floor; // Minimum meaningful current
5} current_calibration_t;
6
7float calibrate_current(float raw_current, current_calibration_t* cal) {
8 float corrected = (raw_current - cal->offset_ma) * cal->gain_factor;
9 return (fabsf(corrected) < cal->noise_floor) ? 0.0f : corrected;
10}Capacity Tracking: Battery capacity isn't constant. It degrades with age, varies with temperature, and changes based on discharge rate. A robust implementation tracks effective capacity over multiple cycles.
Reset Logic: Coulomb counting accumulates errors over time. The algorithm needs periodic "anchor points" where SoC can be reset to known values—typically when the battery reaches full charge or complete discharge.
The Broader Lesson
Liapin's approach reveals something important about embedded development: the biggest risk isn't shipping imperfect code, it's not shipping at all. Hardware delays are inevitable. Component shortages happen. PCB revisions take time. But algorithms can be perfected in software.
This principle extends beyond battery management:
- Motor control algorithms can be validated with simulated encoder feedback
- Sensor fusion can be tested with recorded datasets
- Communication protocols can be debugged with mock interfaces
- Control loops can be tuned with plant models
The key is building proper abstraction layers. Your algorithm should work with an interface to current measurements, not direct ADC reads. This lets you swap between real sensors and simulated data seamlessly.
1typedef struct {
2 float (*read_current_ma)(void);
3 float (*read_voltage_mv)(void);
4 uint32_t (*get_timestamp_ms)(void);
5} battery_hal_t;
6
7// Algorithm works with any HAL implementation
8void battery_update(battery_state_t* state, battery_hal_t* hal) {
9 float current = hal->read_current_ma();
10 float voltage = hal->read_voltage_mv();
11 uint32_t now = hal->get_timestamp_ms();
12
13 // Coulomb counting logic here...
14}Why This Matters
The embedded world moves slower than web development, but it doesn't have to move that much slower. By decoupling algorithms from hardware dependencies, you can:
Accelerate development cycles by 2-3x, especially during prototyping phases where hardware is unstable.
Enable better testing with reproducible scenarios that would be impossible to create with physical hardware.
Foster community adoption by letting users experiment without hardware investments.
Reduce integration risk by validating core logic before hardware complexity enters the picture.
Liapin's coulomb counting story isn't really about battery algorithms—it's about the discipline of shipping incrementally, even when dependencies aren't ready. In embedded development, where hardware constraints often drive architecture decisions, this discipline is the difference between products that ship and prototypes that never leave the lab.
The next time you're tempted to wait for hardware to stabilize before implementing your algorithm, remember: perfect software on imperfect hardware beats no software at all.

