mirror of
https://github.com/liquidraver/ZephCore.git
synced 2026-09-01 21:08:19 +00:00
battery: centralized LiPo OCV curve + ADC multiplier corrections
Add a board-aware battery SOC system replacing the two independent
linear approximations that existed in the UI helpers.
New helpers/battery_curve.{h,c}: a 21-point (5% step) OCV lookup table
with integer linear interpolation. The default generic LiPo curve is a
weak symbol — any board can override it by dropping a battery_curve.c
into its board directory. CMakeLists.txt selects the board-specific file
when present, falling back to the generic.
Board.h gains getBattPercent() (default 0); ZephyrBoard implements it
via battery_curve_lookup().
Board-specific curves added for boards with measured cell data:
t1000_e, rak_wismesh_tag, thinknode_m6, sensecap_solar, wio_tracker_l1.
ADC multiplier corrections applied across all nRF52840 boards:
- Boards using a correctly-derived 3600×ratio formula get +0.5% to
compensate for nRF SAADC gain error (7200→7236, 6300→6332, etc.)
- rak_wismesh_tag, rak3401_1watt, gat562_30s had multipliers copied
from Arduino's 3.0V AREF formula; corrected to 3600×1.73×1.005=6259
- xiao_nrf52840 (10911) left unchanged — empirically calibrated value
above the theoretical, assumed already correct for that hardware
get adc.multiplier now reports current mV reading and the board's
curve 100% target, making field calibration self-guiding.
This commit is contained in:
@@ -436,6 +436,21 @@ target_sources(app PRIVATE
|
||||
helpers/fatal_reboot.c
|
||||
)
|
||||
|
||||
# ========== Battery Curve Selection ==========
|
||||
# Board-specific battery_curve.c wins over the generic default.
|
||||
# To add a curve for a board, create boards/<platform>/<board>/battery_curve.c
|
||||
# without __weak — the linker prefers the non-weak generic only as a fallback.
|
||||
file(GLOB_RECURSE _BOARD_BATTERY_CURVE
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/boards/*/${BOARD_BASE}/battery_curve.c")
|
||||
if(_BOARD_BATTERY_CURVE)
|
||||
list(GET _BOARD_BATTERY_CURVE 0 _BOARD_BATTERY_CURVE)
|
||||
message(STATUS "ZephCore Battery: board-specific curve (${_BOARD_BATTERY_CURVE})")
|
||||
target_sources(app PRIVATE "${_BOARD_BATTERY_CURVE}")
|
||||
else()
|
||||
message(STATUS "ZephCore Battery: generic LiPo curve")
|
||||
target_sources(app PRIVATE helpers/battery_curve.c)
|
||||
endif()
|
||||
|
||||
# Native-Linux (native_sim) runtime setup: force real-time clock mode so the
|
||||
# simulated clock tracks wall time (required for the real SX126x radio's
|
||||
# BUSY/DIO1 timing). Bakes in the equivalent of the --rt command-line flag.
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
|
||||
#include "ZephyrBoard.h"
|
||||
#include "battery_curve.h"
|
||||
#include <zephyr/kernel.h>
|
||||
#include <zephyr/sys/reboot.h>
|
||||
#include <zephyr/drivers/sensor.h>
|
||||
@@ -146,6 +147,11 @@ uint16_t ZephyrBoard::getBattMilliVolts()
|
||||
#endif
|
||||
}
|
||||
|
||||
uint8_t ZephyrBoard::getBattPercent()
|
||||
{
|
||||
return battery_curve_lookup(&battery_curve_default, getBattMilliVolts());
|
||||
}
|
||||
|
||||
bool ZephyrBoard::setAdcMultiplier(float multiplier)
|
||||
{
|
||||
#if DT_NODE_EXISTS(DT_PATH(zephyr_user)) && \
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace mesh {
|
||||
class ZephyrBoard : public MainBoard {
|
||||
public:
|
||||
uint16_t getBattMilliVolts() override;
|
||||
uint8_t getBattPercent() override;
|
||||
float getMCUTemperature() override;
|
||||
bool setAdcMultiplier(float multiplier) override;
|
||||
float getAdcMultiplier() const override;
|
||||
|
||||
@@ -146,7 +146,7 @@
|
||||
/* Battery sense: AIN3 = P0.05 with the GAT562 divider (~1.73x) */
|
||||
zephyr,user {
|
||||
io-channels = <&adc 3>;
|
||||
vbat-mv-multiplier = <6232>; /* mv = raw * multiplier / 4096 */
|
||||
vbat-mv-multiplier = <6259>; /* 1.73:1 divider, 3.6V ref; corrected from Arduino 3.0V AREF + nRF SAADC gain */
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -182,7 +182,7 @@
|
||||
* voltage divider ratio = 4.9 (ADC_MULTIPLIER from variant.h).
|
||||
* multiplier = 3600 * 4.9 = 17640
|
||||
*/
|
||||
vbat-mv-multiplier = <17640>;
|
||||
vbat-mv-multiplier = <17728>; /* 4.9:1 divider, 3.6V ref; +0.5% for nRF SAADC gain error */
|
||||
};
|
||||
|
||||
aliases {
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
/* Battery ADC channel */
|
||||
zephyr,user {
|
||||
io-channels = <&adc 7>;
|
||||
vbat-mv-multiplier = <10650>; /* 1M/510k divider (2.96x), 3.6V ref */
|
||||
vbat-mv-multiplier = <10709>; /* 2.96:1 divider, 3.6V ref; +0.5% for nRF SAADC gain error */
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -190,7 +190,7 @@
|
||||
* with ADC_REF_INTERNAL (0.6V) × gain 1/6 × divider 2.0. */
|
||||
zephyr,user {
|
||||
io-channels = <&adc 2>;
|
||||
vbat-mv-multiplier = <7200>;
|
||||
vbat-mv-multiplier = <7236>; /* 2:1 divider, 3.6V ref; +0.5% for nRF SAADC gain error */
|
||||
};
|
||||
|
||||
/* ---- E-Paper display (MIPI DBI SPI) ----
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
/* Battery ADC: P0.31 (AIN7), 150K+150K voltage divider (2:1) */
|
||||
zephyr,user {
|
||||
io-channels = <&adc 7>;
|
||||
vbat-mv-multiplier = <7200>;
|
||||
vbat-mv-multiplier = <7236>; /* 2:1 divider, 3.6V ref; +0.5% for nRF SAADC gain error */
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
/* Battery ADC: P0.31 (AIN7), 2:1 voltage devider, 3.6V ref */
|
||||
zephyr,user {
|
||||
io-channels = <&adc 7>;
|
||||
vbat-mv-multiplier = <7200>;
|
||||
vbat-mv-multiplier = <7236>; /* 2:1 divider, 3.6V ref; +0.5% for nRF SAADC gain error */
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
* vbat-mv-multiplier = round(3 * 1.73 * 1.187 * 1000) = 6161 */
|
||||
zephyr,user {
|
||||
io-channels = <&adc 3>;
|
||||
vbat-mv-multiplier = <6161>;
|
||||
vbat-mv-multiplier = <6259>; /* 1.73:1 divider, 3.6V ref; corrected from Arduino 3.0V AREF + nRF SAADC gain */
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* RAK WisMesh Tag single-cell LiPo OCV curve — 21 points at 5% steps.
|
||||
* 11-point base linearly interpolated to 5% resolution.
|
||||
* Index 0 = 100% (4160 mV), index 20 = 0% (2990 mV).
|
||||
*
|
||||
* Notable shape: very compressed upper range (100% = 4160 mV, not 4190),
|
||||
* flat plateau 3.72–3.76 V, steep cliff below 3.62 V to 2990 mV cutoff.
|
||||
*/
|
||||
|
||||
#include "battery_curve.h"
|
||||
|
||||
static const uint16_t ocv_rak_wismesh_tag[21] = {
|
||||
4160, 4090, 4020, 3980, 3940, /* 100 .. 80% */
|
||||
3905, 3870, 3840, 3810, 3785, /* 75 .. 55% */
|
||||
3760, 3750, 3740, 3730, 3720, /* 50 .. 30% */
|
||||
3700, 3680, 3650, 3620, 3305, /* 25 .. 5% */
|
||||
2990, /* 0% */
|
||||
};
|
||||
|
||||
const battery_curve_t battery_curve_default = {
|
||||
.ocv_mv = ocv_rak_wismesh_tag,
|
||||
.num_points = 21,
|
||||
.num_cells = 1,
|
||||
};
|
||||
@@ -138,7 +138,7 @@
|
||||
* applied to 12-bit average with default ref (GAIN_1_6 + 0.6V internal = 3.6V FS). */
|
||||
zephyr,user {
|
||||
io-channels = <&adc 3>;
|
||||
vbat-mv-multiplier = <6161>;
|
||||
vbat-mv-multiplier = <6259>; /* 1.73:1 divider, 3.6V ref; corrected from Arduino 3.0V AREF + nRF SAADC gain */
|
||||
};
|
||||
|
||||
/* ---- Buzzer on PWM0 ---- */
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* SenseCAP Solar single-cell LiPo OCV curve — 21 points at 5% steps.
|
||||
* 11-point base linearly interpolated to 5% resolution.
|
||||
* Index 0 = 100% (4200 mV), index 20 = 0% (2786 mV).
|
||||
*
|
||||
* Notable shape: steeper overall slope than a standard LiPo, consistent with
|
||||
* a high-impedance cell measured under load rather than at true OCV. Low
|
||||
* cutoff (2786 mV) reflects the solar node's extended discharge budget.
|
||||
*/
|
||||
|
||||
#include "battery_curve.h"
|
||||
|
||||
static const uint16_t ocv_sensecap_solar[21] = {
|
||||
4200, 4093, 3986, 3954, 3922, /* 100 .. 80% */
|
||||
3867, 3812, 3773, 3734, 3689, /* 75 .. 55% */
|
||||
3645, 3586, 3527, 3473, 3420, /* 50 .. 30% */
|
||||
3350, 3281, 3184, 3087, 2936, /* 25 .. 5% */
|
||||
2786, /* 0% */
|
||||
};
|
||||
|
||||
const battery_curve_t battery_curve_default = {
|
||||
.ocv_mv = ocv_sensecap_solar,
|
||||
.num_points = 21,
|
||||
.num_cells = 1,
|
||||
};
|
||||
@@ -91,7 +91,7 @@
|
||||
zephyr,user {
|
||||
io-channels = <&adc 7>;
|
||||
/* Zephyr: ADC_GAIN_1_6 + ADC_REF_INTERNAL = 3600mV full-scale; 1:3 voltage divider → 3600*3 */
|
||||
vbat-mv-multiplier = <10800>;
|
||||
vbat-mv-multiplier = <10854>; /* 3:1 divider, 3.6V ref; +0.5% for nRF SAADC gain error */
|
||||
};
|
||||
|
||||
aliases {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* T1000-E single-cell LiPo OCV curve — 21 points at 5% steps.
|
||||
* 11-point base linearly interpolated to 5% resolution.
|
||||
* Index 0 = 100% (4190 mV), index 20 = 0% (3100 mV).
|
||||
*
|
||||
* Notable shape: very flat plateau 3.72–3.82 V, steep cliff below 3.64 V.
|
||||
*/
|
||||
|
||||
#include "battery_curve.h"
|
||||
|
||||
static const uint16_t ocv_t1000_e[21] = {
|
||||
4190, 4116, 4042, 3999, 3957, /* 100 .. 80% */
|
||||
3921, 3885, 3852, 3820, 3798, /* 75 .. 55% */
|
||||
3776, 3761, 3746, 3735, 3725, /* 50 .. 30% */
|
||||
3710, 3696, 3670, 3644, 3372, /* 25 .. 5% */
|
||||
3100, /* 0% */
|
||||
};
|
||||
|
||||
const battery_curve_t battery_curve_default = {
|
||||
.ocv_mv = ocv_t1000_e,
|
||||
.num_points = 21,
|
||||
.num_cells = 1,
|
||||
};
|
||||
@@ -102,7 +102,7 @@
|
||||
/* Battery ADC channel reference for ZephyrBoard */
|
||||
zephyr,user {
|
||||
io-channels = <&adc 0>;
|
||||
vbat-mv-multiplier = <7200>; /* 2:1 voltage divider, 3.6V ref */
|
||||
vbat-mv-multiplier = <7236>; /* 2:1 divider, 3.6V ref; +0.5% for nRF SAADC gain error */
|
||||
};
|
||||
|
||||
/* Buzzer on PWM0 channel 0 (P0.25) with enable on P1.05 */
|
||||
|
||||
@@ -151,7 +151,7 @@
|
||||
/* 150K+150K voltage divider (2:1) on AIN2 (P0.04) */
|
||||
zephyr,user {
|
||||
io-channels = <&adc 2>;
|
||||
vbat-mv-multiplier = <7200>;
|
||||
vbat-mv-multiplier = <7236>; /* 2:1 divider, 3.6V ref; +0.5% for nRF SAADC gain error */
|
||||
};
|
||||
|
||||
/* ---- Buzzer on PWM0 ---- */
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
* With GAIN_1_6 + internal ref (3600mV full scale) × 2 = 7200 multiplier. */
|
||||
zephyr,user {
|
||||
io-channels = <&adc 3>;
|
||||
vbat-mv-multiplier = <7200>;
|
||||
vbat-mv-multiplier = <7236>; /* 2:1 divider, 3.6V ref; +0.5% for nRF SAADC gain error */
|
||||
};
|
||||
|
||||
/* Buzzer on PWM0 channel 0 (P0.23, PIN_BUZZER=23) */
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* ThinkNode M6 single-cell LiPo OCV curve — 21 points at 5% steps.
|
||||
* 11-point base linearly interpolated to 5% resolution.
|
||||
* Index 0 = 100% (4080 mV), index 20 = 0% (3450 mV).
|
||||
*
|
||||
* Notable shape: lower full-charge ceiling (4080 mV) suggesting a conservative
|
||||
* charge termination; smooth linear discharge with no sharp cliff at the bottom.
|
||||
*/
|
||||
|
||||
#include "battery_curve.h"
|
||||
|
||||
static const uint16_t ocv_thinknode_m6[21] = {
|
||||
4080, 4035, 3990, 3962, 3935, /* 100 .. 80% */
|
||||
3907, 3880, 3852, 3825, 3797, /* 75 .. 55% */
|
||||
3770, 3742, 3715, 3687, 3660, /* 50 .. 30% */
|
||||
3632, 3605, 3577, 3550, 3500, /* 25 .. 5% */
|
||||
3450, /* 0% */
|
||||
};
|
||||
|
||||
const battery_curve_t battery_curve_default = {
|
||||
.ocv_mv = ocv_thinknode_m6,
|
||||
.num_points = 21,
|
||||
.num_cells = 1,
|
||||
};
|
||||
@@ -94,7 +94,7 @@
|
||||
* With GAIN_1_6 + internal ref (3600mV full scale) × 1.75 = 6300 multiplier. */
|
||||
zephyr,user {
|
||||
io-channels = <&adc 4>;
|
||||
vbat-mv-multiplier = <6300>;
|
||||
vbat-mv-multiplier = <6332>; /* 1.75:1 divider, 3.6V ref; +0.5% for nRF SAADC gain error */
|
||||
};
|
||||
|
||||
/* Board power enable (PIN_PWR_EN = P0.27) — must be HIGH for peripherals */
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*
|
||||
* Wio Tracker L1 battery curve — single-cell LiPo, 21 points at 5% steps.
|
||||
*
|
||||
* 100% = 4190 mV: observed charge ceiling on this board after the ADC
|
||||
* multiplier correction (vbat-mv-multiplier = 7236). Voltages above this
|
||||
* clamp to 100% in the lookup.
|
||||
*
|
||||
* The discharge shape below 4190 mV follows the generic LiPo profile.
|
||||
* Replace this table with measured discharge data for the specific cell
|
||||
* installed if a more accurate curve is needed.
|
||||
*/
|
||||
|
||||
#include "battery_curve.h"
|
||||
|
||||
static const uint16_t ocv_wio_tracker_l1[21] = {
|
||||
4190, 4120, 4050, 4020, 3990, /* 100 .. 80% */
|
||||
3940, 3890, 3845, 3800, 3760, /* 75 .. 55% */
|
||||
3720, 3675, 3630, 3580, 3530, /* 50 .. 30% */
|
||||
3475, 3420, 3360, 3300, 3200, /* 25 .. 5% */
|
||||
3100, /* 0% */
|
||||
};
|
||||
|
||||
const battery_curve_t battery_curve_default = {
|
||||
.ocv_mv = ocv_wio_tracker_l1,
|
||||
.num_points = 21,
|
||||
.num_cells = 1,
|
||||
};
|
||||
@@ -126,7 +126,7 @@
|
||||
/* Battery ADC channel reference for ZephyrBoard */
|
||||
zephyr,user {
|
||||
io-channels = <&adc 7>;
|
||||
vbat-mv-multiplier = <7200>; /* 2:1 voltage divider, 3.6V ref */
|
||||
vbat-mv-multiplier = <7236>; /* 2:1 divider, 3.6V ref; +0.5% for nRF SAADC gain error */
|
||||
};
|
||||
|
||||
/* Buzzer on PWM0 channel 0 (P1.00) - no enable pin needed */
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
#include "CommonCLI.h"
|
||||
#include "battery_curve.h"
|
||||
#include <helpers/TxtDataHelpers.h>
|
||||
#include <helpers/AdvertDataHelpers.h>
|
||||
#include <adapters/board/ZephyrBoard.h>
|
||||
@@ -507,7 +508,14 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
|
||||
if (adc_mult == 0.0f) {
|
||||
strcpy(reply, "Error: unsupported by this board");
|
||||
} else {
|
||||
snprintf(reply, CLI_REPLY_SIZE, "> %.3f", (double)adc_mult);
|
||||
uint16_t mv = _board->getBattMilliVolts();
|
||||
uint16_t target_mv = battery_curve_default.ocv_mv[0];
|
||||
if (mv > 0) {
|
||||
snprintf(reply, CLI_REPLY_SIZE, "> %.3f (%u mV, target >= %u mV for 100%%)",
|
||||
(double)adc_mult, mv, target_mv);
|
||||
} else {
|
||||
snprintf(reply, CLI_REPLY_SIZE, "> %.3f (no ADC reading)", (double)adc_mult);
|
||||
}
|
||||
}
|
||||
} else if (memcmp(config, "rxduty", 6) == 0) {
|
||||
snprintf(reply, CLI_REPLY_SIZE, "> %d", (int)_prefs->rx_duty_cycle);
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#include "battery_curve.h"
|
||||
|
||||
/*
|
||||
* Generic single-cell LiPo OCV curve — 21 points at 5% steps.
|
||||
* Derived from measured LiPo discharge data; 11-point base expanded to
|
||||
* 21 points by linear interpolation for better knee resolution.
|
||||
*
|
||||
* Index 0 = 100% (4190 mV), index 20 = 0% (3100 mV).
|
||||
* 100% is 4190 mV, not 4200 mV — reflects observed charge ceiling across
|
||||
* multiple boards after the charger transitions to maintenance mode.
|
||||
*/
|
||||
static const uint16_t ocv_generic[21] = {
|
||||
4190, 4120, 4050, 4020, 3990, /* 100 .. 80% */
|
||||
3940, 3890, 3845, 3800, 3760, /* 75 .. 55% */
|
||||
3720, 3675, 3630, 3580, 3530, /* 50 .. 30% */
|
||||
3475, 3420, 3360, 3300, 3200, /* 25 .. 5% */
|
||||
3100, /* 0% */
|
||||
};
|
||||
|
||||
__attribute__((weak))
|
||||
const battery_curve_t battery_curve_default = {
|
||||
.ocv_mv = ocv_generic,
|
||||
.num_points = 21,
|
||||
.num_cells = 1,
|
||||
};
|
||||
|
||||
uint8_t battery_curve_lookup(const battery_curve_t *curve, uint16_t mv)
|
||||
{
|
||||
if (mv == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint16_t cell_mv = mv / curve->num_cells;
|
||||
uint8_t n = curve->num_points;
|
||||
|
||||
if (cell_mv >= curve->ocv_mv[0]) {
|
||||
return 100;
|
||||
}
|
||||
if (cell_mv <= curve->ocv_mv[n - 1]) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Each segment spans 100/(n-1) percent. For n=21 this is exactly 5. */
|
||||
uint8_t seg_pct = (uint8_t)(100 / (n - 1));
|
||||
|
||||
for (uint8_t i = 1; i < n; i++) {
|
||||
if (cell_mv >= curve->ocv_mv[i]) {
|
||||
uint8_t pct_low = (uint8_t)(100 - (uint16_t)i * 100 / (n - 1));
|
||||
uint32_t num = (uint32_t)(cell_mv - curve->ocv_mv[i]) * seg_pct;
|
||||
uint16_t den = curve->ocv_mv[i - 1] - curve->ocv_mv[i];
|
||||
return (uint8_t)(pct_low + num / den);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
const uint16_t *ocv_mv; /* descending — index 0 = 100%, last index = 0% */
|
||||
uint8_t num_points;
|
||||
uint8_t num_cells; /* 1 for single-cell packs (almost always 1) */
|
||||
} battery_curve_t;
|
||||
|
||||
/* Default curve — __weak so a board-specific battery_curve.c can override it. */
|
||||
extern const battery_curve_t battery_curve_default;
|
||||
|
||||
/* Returns 0–100 %. Returns 0 if mv == 0 (no battery / ADC absent). */
|
||||
uint8_t battery_curve_lookup(const battery_curve_t *curve, uint16_t mv);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -17,6 +17,7 @@ namespace mesh {
|
||||
class MainBoard {
|
||||
public:
|
||||
virtual uint16_t getBattMilliVolts() = 0;
|
||||
virtual uint8_t getBattPercent() { return 0; }
|
||||
virtual float getMCUTemperature() { return NAN; }
|
||||
virtual bool setAdcMultiplier(float multiplier) { (void)multiplier; return false; }
|
||||
virtual float getAdcMultiplier() const { return 0.0f; }
|
||||
|
||||
Reference in New Issue
Block a user