From d37b401033b18dd87b8a3748a5863b4470713c55 Mon Sep 17 00:00:00 2001 From: liquidraver <504870+liquidraver@users.noreply.github.com> Date: Wed, 3 Jun 2026 13:21:06 +0200 Subject: [PATCH] battery: centralized LiPo OCV curve + ADC multiplier corrections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- zephcore/CMakeLists.txt | 15 +++++ zephcore/adapters/board/ZephyrBoard.cpp | 6 ++ zephcore/adapters/board/ZephyrBoard.h | 1 + .../boards/nrf52840/gat562_30s/gat562_30s.dts | 2 +- .../heltec_t114/heltec_t114_nrf52840.dts | 2 +- .../ikoka_nano_30dbm/ikoka_nano_30dbm.dts | 2 +- .../nrf52840/lilygo_techo/lilygo_techo.dts | 2 +- .../promicro_lr2021/promicro_lr2021.dts | 2 +- .../promicro_sx1262/promicro_sx1262.dts | 2 +- .../nrf52840/rak3401_1watt/rak3401_1watt.dts | 2 +- .../nrf52840/rak_wismesh_tag/battery_curve.c | 26 ++++++++ .../rak_wismesh_tag/rak_wismesh_tag.dts | 2 +- .../nrf52840/sensecap_solar/battery_curve.c | 27 +++++++++ .../sensecap_solar_nrf52840.dts | 2 +- .../boards/nrf52840/t1000_e/battery_curve.c | 25 ++++++++ .../nrf52840/t1000_e/t1000_e_nrf52840.dts | 2 +- .../nrf52840/thinknode_m1/thinknode_m1.dts | 2 +- .../nrf52840/thinknode_m3/thinknode_m3.dts | 2 +- .../nrf52840/thinknode_m6/battery_curve.c | 26 ++++++++ .../nrf52840/thinknode_m6/thinknode_m6.dts | 2 +- .../nrf52840/wio_tracker_l1/battery_curve.c | 29 +++++++++ .../wio_tracker_l1_nrf52840.dts | 2 +- zephcore/helpers/CommonCLI.cpp | 10 +++- zephcore/helpers/battery_curve.c | 60 +++++++++++++++++++ zephcore/helpers/battery_curve.h | 27 +++++++++ zephcore/include/mesh/Board.h | 1 + 26 files changed, 266 insertions(+), 15 deletions(-) create mode 100644 zephcore/boards/nrf52840/rak_wismesh_tag/battery_curve.c create mode 100644 zephcore/boards/nrf52840/sensecap_solar/battery_curve.c create mode 100644 zephcore/boards/nrf52840/t1000_e/battery_curve.c create mode 100644 zephcore/boards/nrf52840/thinknode_m6/battery_curve.c create mode 100644 zephcore/boards/nrf52840/wio_tracker_l1/battery_curve.c create mode 100644 zephcore/helpers/battery_curve.c create mode 100644 zephcore/helpers/battery_curve.h diff --git a/zephcore/CMakeLists.txt b/zephcore/CMakeLists.txt index 95b74ab..b97e706 100644 --- a/zephcore/CMakeLists.txt +++ b/zephcore/CMakeLists.txt @@ -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///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. diff --git a/zephcore/adapters/board/ZephyrBoard.cpp b/zephcore/adapters/board/ZephyrBoard.cpp index a273cb4..91c4c20 100644 --- a/zephcore/adapters/board/ZephyrBoard.cpp +++ b/zephcore/adapters/board/ZephyrBoard.cpp @@ -3,6 +3,7 @@ */ #include "ZephyrBoard.h" +#include "battery_curve.h" #include #include #include @@ -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)) && \ diff --git a/zephcore/adapters/board/ZephyrBoard.h b/zephcore/adapters/board/ZephyrBoard.h index c95723d..d4e625e 100644 --- a/zephcore/adapters/board/ZephyrBoard.h +++ b/zephcore/adapters/board/ZephyrBoard.h @@ -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; diff --git a/zephcore/boards/nrf52840/gat562_30s/gat562_30s.dts b/zephcore/boards/nrf52840/gat562_30s/gat562_30s.dts index 3930f07..308372f 100644 --- a/zephcore/boards/nrf52840/gat562_30s/gat562_30s.dts +++ b/zephcore/boards/nrf52840/gat562_30s/gat562_30s.dts @@ -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 */ }; }; diff --git a/zephcore/boards/nrf52840/heltec_t114/heltec_t114_nrf52840.dts b/zephcore/boards/nrf52840/heltec_t114/heltec_t114_nrf52840.dts index f369acf..6ab81a8 100644 --- a/zephcore/boards/nrf52840/heltec_t114/heltec_t114_nrf52840.dts +++ b/zephcore/boards/nrf52840/heltec_t114/heltec_t114_nrf52840.dts @@ -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 { diff --git a/zephcore/boards/nrf52840/ikoka_nano_30dbm/ikoka_nano_30dbm.dts b/zephcore/boards/nrf52840/ikoka_nano_30dbm/ikoka_nano_30dbm.dts index 3e375d6..7a8112a 100644 --- a/zephcore/boards/nrf52840/ikoka_nano_30dbm/ikoka_nano_30dbm.dts +++ b/zephcore/boards/nrf52840/ikoka_nano_30dbm/ikoka_nano_30dbm.dts @@ -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 */ }; }; diff --git a/zephcore/boards/nrf52840/lilygo_techo/lilygo_techo.dts b/zephcore/boards/nrf52840/lilygo_techo/lilygo_techo.dts index 9039941..1ace875 100644 --- a/zephcore/boards/nrf52840/lilygo_techo/lilygo_techo.dts +++ b/zephcore/boards/nrf52840/lilygo_techo/lilygo_techo.dts @@ -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) ---- diff --git a/zephcore/boards/nrf52840/promicro_lr2021/promicro_lr2021.dts b/zephcore/boards/nrf52840/promicro_lr2021/promicro_lr2021.dts index 3f82d97..0634698 100644 --- a/zephcore/boards/nrf52840/promicro_lr2021/promicro_lr2021.dts +++ b/zephcore/boards/nrf52840/promicro_lr2021/promicro_lr2021.dts @@ -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 */ }; }; diff --git a/zephcore/boards/nrf52840/promicro_sx1262/promicro_sx1262.dts b/zephcore/boards/nrf52840/promicro_sx1262/promicro_sx1262.dts index 481565b..9e3a500 100644 --- a/zephcore/boards/nrf52840/promicro_sx1262/promicro_sx1262.dts +++ b/zephcore/boards/nrf52840/promicro_sx1262/promicro_sx1262.dts @@ -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 */ }; }; diff --git a/zephcore/boards/nrf52840/rak3401_1watt/rak3401_1watt.dts b/zephcore/boards/nrf52840/rak3401_1watt/rak3401_1watt.dts index 6074818..cd572d8 100644 --- a/zephcore/boards/nrf52840/rak3401_1watt/rak3401_1watt.dts +++ b/zephcore/boards/nrf52840/rak3401_1watt/rak3401_1watt.dts @@ -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 */ }; }; diff --git a/zephcore/boards/nrf52840/rak_wismesh_tag/battery_curve.c b/zephcore/boards/nrf52840/rak_wismesh_tag/battery_curve.c new file mode 100644 index 0000000..24acafd --- /dev/null +++ b/zephcore/boards/nrf52840/rak_wismesh_tag/battery_curve.c @@ -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, +}; diff --git a/zephcore/boards/nrf52840/rak_wismesh_tag/rak_wismesh_tag.dts b/zephcore/boards/nrf52840/rak_wismesh_tag/rak_wismesh_tag.dts index b031944..34752a1 100644 --- a/zephcore/boards/nrf52840/rak_wismesh_tag/rak_wismesh_tag.dts +++ b/zephcore/boards/nrf52840/rak_wismesh_tag/rak_wismesh_tag.dts @@ -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 ---- */ diff --git a/zephcore/boards/nrf52840/sensecap_solar/battery_curve.c b/zephcore/boards/nrf52840/sensecap_solar/battery_curve.c new file mode 100644 index 0000000..e360b11 --- /dev/null +++ b/zephcore/boards/nrf52840/sensecap_solar/battery_curve.c @@ -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, +}; diff --git a/zephcore/boards/nrf52840/sensecap_solar/sensecap_solar_nrf52840.dts b/zephcore/boards/nrf52840/sensecap_solar/sensecap_solar_nrf52840.dts index 64a2dca..cc6b4f1 100644 --- a/zephcore/boards/nrf52840/sensecap_solar/sensecap_solar_nrf52840.dts +++ b/zephcore/boards/nrf52840/sensecap_solar/sensecap_solar_nrf52840.dts @@ -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 { diff --git a/zephcore/boards/nrf52840/t1000_e/battery_curve.c b/zephcore/boards/nrf52840/t1000_e/battery_curve.c new file mode 100644 index 0000000..68f11ca --- /dev/null +++ b/zephcore/boards/nrf52840/t1000_e/battery_curve.c @@ -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, +}; diff --git a/zephcore/boards/nrf52840/t1000_e/t1000_e_nrf52840.dts b/zephcore/boards/nrf52840/t1000_e/t1000_e_nrf52840.dts index 412fa29..892565c 100644 --- a/zephcore/boards/nrf52840/t1000_e/t1000_e_nrf52840.dts +++ b/zephcore/boards/nrf52840/t1000_e/t1000_e_nrf52840.dts @@ -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 */ diff --git a/zephcore/boards/nrf52840/thinknode_m1/thinknode_m1.dts b/zephcore/boards/nrf52840/thinknode_m1/thinknode_m1.dts index 8e12d43..64a8321 100644 --- a/zephcore/boards/nrf52840/thinknode_m1/thinknode_m1.dts +++ b/zephcore/boards/nrf52840/thinknode_m1/thinknode_m1.dts @@ -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 ---- */ diff --git a/zephcore/boards/nrf52840/thinknode_m3/thinknode_m3.dts b/zephcore/boards/nrf52840/thinknode_m3/thinknode_m3.dts index 64922e4..37cc1cb 100644 --- a/zephcore/boards/nrf52840/thinknode_m3/thinknode_m3.dts +++ b/zephcore/boards/nrf52840/thinknode_m3/thinknode_m3.dts @@ -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) */ diff --git a/zephcore/boards/nrf52840/thinknode_m6/battery_curve.c b/zephcore/boards/nrf52840/thinknode_m6/battery_curve.c new file mode 100644 index 0000000..9207d03 --- /dev/null +++ b/zephcore/boards/nrf52840/thinknode_m6/battery_curve.c @@ -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, +}; diff --git a/zephcore/boards/nrf52840/thinknode_m6/thinknode_m6.dts b/zephcore/boards/nrf52840/thinknode_m6/thinknode_m6.dts index 6a75627..05c3d56 100644 --- a/zephcore/boards/nrf52840/thinknode_m6/thinknode_m6.dts +++ b/zephcore/boards/nrf52840/thinknode_m6/thinknode_m6.dts @@ -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 */ diff --git a/zephcore/boards/nrf52840/wio_tracker_l1/battery_curve.c b/zephcore/boards/nrf52840/wio_tracker_l1/battery_curve.c new file mode 100644 index 0000000..ff72c15 --- /dev/null +++ b/zephcore/boards/nrf52840/wio_tracker_l1/battery_curve.c @@ -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, +}; diff --git a/zephcore/boards/nrf52840/wio_tracker_l1/wio_tracker_l1_nrf52840.dts b/zephcore/boards/nrf52840/wio_tracker_l1/wio_tracker_l1_nrf52840.dts index ceb634b..5fc25fb 100644 --- a/zephcore/boards/nrf52840/wio_tracker_l1/wio_tracker_l1_nrf52840.dts +++ b/zephcore/boards/nrf52840/wio_tracker_l1/wio_tracker_l1_nrf52840.dts @@ -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 */ diff --git a/zephcore/helpers/CommonCLI.cpp b/zephcore/helpers/CommonCLI.cpp index 414d8c6..9f80202 100644 --- a/zephcore/helpers/CommonCLI.cpp +++ b/zephcore/helpers/CommonCLI.cpp @@ -4,6 +4,7 @@ */ #include "CommonCLI.h" +#include "battery_curve.h" #include #include #include @@ -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); diff --git a/zephcore/helpers/battery_curve.c b/zephcore/helpers/battery_curve.c new file mode 100644 index 0000000..f5122ca --- /dev/null +++ b/zephcore/helpers/battery_curve.c @@ -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; +} diff --git a/zephcore/helpers/battery_curve.h b/zephcore/helpers/battery_curve.h new file mode 100644 index 0000000..e0d83f1 --- /dev/null +++ b/zephcore/helpers/battery_curve.h @@ -0,0 +1,27 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include + +#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 diff --git a/zephcore/include/mesh/Board.h b/zephcore/include/mesh/Board.h index 59baa2c..435fe56 100644 --- a/zephcore/include/mesh/Board.h +++ b/zephcore/include/mesh/Board.h @@ -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; }