It is table-driven, not a simple full-range linear fit.
We read the battery voltage in millivolts, then convert it to state-of-charge using an open-circuit-voltage table. Between adjacent table entries, the code does linear interpolation.
For the Wio Tracker L1, the OCV table is:
| Charge | Voltage |
|---|---|
| 100% | 4200 mV |
| 90% | 3876 mV |
| 80% | 3826 mV |
| 70% | 3763 mV |
| 60% | 3713 mV |
| 50% | 3660 mV |
| 40% | 3573 mV |
| 30% | 3485 mV |
| 20% | 3422 mV |
| 10% | 3359 mV |
| 0% | 3300 mV |
The actual conversion source is:
static int getBatteryPercent(uint16_t batteryMillivolts) {
if (batteryMillivolts == 0) {
return -1;
}
const uint16_t *ocv = getBatteryProfile();
uint16_t noBatteryMillivolts = (ocv[NUM_OCV_POINTS - 1] - 500) * NUM_CELLS;
if (batteryMillivolts < noBatteryMillivolts) {
return -1;
}
float batterySoc = 0.0f;
uint16_t cellMillivolts = batteryMillivolts / NUM_CELLS;
for (int i = 0; i < NUM_OCV_POINTS; i++) {
if (ocv[i] <= cellMillivolts) {
if (i == 0) {
batterySoc = 100.0f;
} else {
batterySoc = 100.0f / (NUM_OCV_POINTS - 1.0f) *
(NUM_OCV_POINTS - 1.0f - i +
((float)cellMillivolts - ocv[i]) / (ocv[i - 1] - ocv[i]));
}
break;
}
}
int result = (int)batterySoc;
if (result < 0) result = 0;
if (result > 100) result = 100;
return result;
}For Wio Tracker L1 specifically:
#define OCV_ARRAY 4200, 3876, 3826, 3763, 3713, 3660, 3573, 3485, 3422, 3359, 3300The Wio Tracker L1 voltage reader averages ADC samples, converts the raw ADC value to millivolts with the ADC multiplier, caches readings for 5 seconds, and applies a simple exponential moving average:
uint16_t getBattMilliVolts() override {
static uint32_t last_read_time_ms = 0;
static float last_filtered_mv = 0.0f;
static bool initial_read_done = false;
uint32_t now = millis();
if (initial_read_done && (now - last_read_time_ms < 5000)) {
return (uint16_t)last_filtered_mv;
}
last_read_time_ms = now;
analogReadResolution(12);
analogReference(AR_INTERNAL);
delay(1);
uint32_t sum = 0;
for (int i = 0; i < 15; i++) {
sum += analogRead(PIN_VBAT_READ);
}
uint16_t raw_avg = sum / 15;
float scaled_mv = (raw_avg * getAdcMultiplier() * AREF_VOLTAGE * 1000.0f) / 4096.0f;
if (!initial_read_done) {
last_filtered_mv = scaled_mv;
initial_read_done = true;
} else {
last_filtered_mv += (scaled_mv - last_filtered_mv) * 0.5f;
}
return (uint16_t)last_filtered_mv;
}