Skip to content

Commit 44ffbad

Browse files
committed
feat(stm32wl): support reading Vbat from internal ADC channel
Add support for reading battery voltage using the STM32WL internal VBAT ADC channel. **src/Power.cpp** **Global additions** - ADC configuration macros and STM32 LL ADC helper includes - Global variable `Vref` to store the measured ADC reference voltage (in mV), used to scale all subsequent VBAT readings - `stm32wlLevel` singleton instance **New class: `Stm32wlBatteryLevel` (extends `HasBatteryLevel`)** - `getBatteryPercent()` Identical implementation to `AnalogBatteryLevel::getBatteryPercent()`, mapping voltage to SoC using the OCV curve. - `runOnce()` Performs a one-time ADC read of the internal reference voltage (VREFINT) to determine the effective ADC reference. - `getBattVoltage()` Reads the STM32WL internal VBAT ADC channel and applies the required ×3 scaling to compensate for the internal 1/3 divider. - `isBatteryConnect()` Always returns true, as VBAT is internally tied to VDD when the battery supplies the MCU. **Power integration** - Adds `Power::stm32wlInit()` as a one-time probe and inserts it ahead of `analogInit()` so STM32WL internal VBAT is preferred when available. **Possible regressions** - None expected. `stm32wlInit()` is compiled as a stub unless both `ARCH_STM32WL` and `BATTERY_PIN == AVBAT` are defined. --- **src/power.h** Selects an appropriate battery chemistry curve for STM32WL internal VBAT usage. - Adds an LFP OCV curve when `ARCH_STM32WL` and `BATTERY_PIN == AVBAT` are defined - STM32WL VBAT/VDD absolute maximum is 4 V, so Li-ion/LiPo (NMC) cells are not suitable for direct VBAT operation. **Possible regressions** - None expected. OCV selection only changes when `ARCH_STM32WL` and `BATTERY_PIN == AVBAT` are defined. --- **variants/stm32/rak3172/variant.h** Enables STM32WL internal VBAT sensing for the RAK3172 target. **Possible regressions** - None expected. The RAK3172 variant previously had no battery voltage reporting. **Hardware tested** - Verified on the Russell board (RAK3172-based). --- **variants/stm32/russell/variant.h** Enables STM32WL internal VBAT sensing for the Russell target. **Possible regressions** - None expected. Battery voltage reporting was not previously implemented. **Hardware tested** - Verified on the Russell board. --- **variants/stm32/stm32.ini** Reduces flash usage for STM32WL targets close to the size limit. - Comments out `PIO_FRAMEWORK_ARDUINO_NANOLIB_FLOAT_PRINTF` - Documents that it is only required when debug logging is enabled **Possible regressions** - None expected. All existing STM32WL variants are built with `DEBUG_MUTE`. Signed-off-by: Andrew Yong <me@ndoo.sg>
1 parent 63a97a5 commit 44ffbad

5 files changed

Lines changed: 133 additions & 1 deletion

File tree

src/Power.cpp

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,21 @@
3535
#include "nrfx_power.h"
3636
#endif
3737

38+
#if defined(ARCH_STM32WL) && BATTERY_PIN == AVBAT
39+
#include "stm32yyxx_ll_adc.h"
40+
41+
/* Analog read resolution */
42+
#if defined(LL_ADC_RESOLUTION_12B)
43+
#define LL_ADC_RESOLUTION LL_ADC_RESOLUTION_12B
44+
#define BATTERY_SENSE_RESOLUTION_BITS 12
45+
#elif defined(LL_ADC_DS_DATA_WIDTH_12_BIT)
46+
#define LL_ADC_RESOLUTION LL_ADC_DS_DATA_WIDTH_12_BIT
47+
#else
48+
#error "ADC resolution could not be defined!"
49+
#endif
50+
#define ADC_RANGE 4096
51+
#endif
52+
3853
#if defined(DEBUG_HEAP_MQTT) && !MESHTASTIC_EXCLUDE_MQTT
3954
#include "mqtt/MQTT.h"
4055
#include "target_specific.h"
@@ -698,6 +713,8 @@ bool Power::setup()
698713
found = true;
699714
} else if (meshSolarInit()) {
700715
found = true;
716+
} else if (stm32wlInit()) {
717+
found = true;
701718
} else if (analogInit()) {
702719
found = true;
703720
}
@@ -1602,6 +1619,110 @@ bool Power::meshSolarInit()
16021619
}
16031620
#endif
16041621

1622+
#if defined(ARCH_STM32WL) && BATTERY_PIN == AVBAT
1623+
1624+
/**
1625+
* STM32WL internal VBAT ADC channel
1626+
*/
1627+
1628+
// 3300mV is a placeholder value in case of future errata e.g. STM32U0
1629+
uint32_t Vref = 3300;
1630+
1631+
class Stm32wlBatteryLevel : public HasBatteryLevel
1632+
{
1633+
1634+
public:
1635+
/**
1636+
* Battery state of charge, from 0 to 100 or -1 for unknown
1637+
* Copied wholesale from AnalogBatteryLevel
1638+
*/
1639+
virtual int getBatteryPercent() override
1640+
{
1641+
float v = getBattVoltage();
1642+
1643+
if (v < noBatVolt)
1644+
return -1; // If voltage is super low assume no battery installed
1645+
1646+
float battery_SOC = 0.0;
1647+
uint16_t voltage = v / NUM_CELLS; // single cell voltage (average)
1648+
for (int i = 0; i < NUM_OCV_POINTS; i++) {
1649+
if (OCV[i] <= voltage) {
1650+
if (i == 0) {
1651+
battery_SOC = 100.0; // 100% full
1652+
} else {
1653+
// interpolate between OCV[i] and OCV[i-1]
1654+
battery_SOC = (float)100.0 / (NUM_OCV_POINTS - 1.0) *
1655+
(NUM_OCV_POINTS - 1.0 - i + ((float)voltage - OCV[i]) / (OCV[i - 1] - OCV[i]));
1656+
}
1657+
break;
1658+
}
1659+
}
1660+
#if defined(BATTERY_CHARGING_INV)
1661+
// bit of trickery to show 99% up until the charge finishes
1662+
if (!digitalRead(BATTERY_CHARGING_INV) && battery_SOC > 99)
1663+
battery_SOC = 99;
1664+
#endif
1665+
return clamp((int)(battery_SOC), 0, 100);
1666+
}
1667+
1668+
/**
1669+
* Read VREF in mV once, used to scale all subsequent VBAT readings
1670+
*/
1671+
bool runOnce()
1672+
{
1673+
#ifdef __LL_ADC_CALC_VREFANALOG_VOLTAGE
1674+
Vref = __LL_ADC_CALC_VREFANALOG_VOLTAGE(analogRead(AVREF), LL_ADC_RESOLUTION);
1675+
#else
1676+
Vref = VREFINT * ADC_RANGE / analogRead(AVREF); // ADC sample to mV
1677+
#endif
1678+
return true;
1679+
}
1680+
1681+
/**
1682+
* Read VBAT in mV
1683+
*/
1684+
virtual uint16_t getBattVoltage() override
1685+
{
1686+
// VBAT pin is internally connected to a bridge divider by three (DS13105§3.18.3)
1687+
return 3 * __LL_ADC_CALC_DATA_TO_VOLTAGE(Vref, analogRead(BATTERY_PIN), LL_ADC_RESOLUTION);
1688+
}
1689+
1690+
/**
1691+
* If BATTERY_PIN = AVBAT, assume a battery is present (otherwise don't define it!)
1692+
*/
1693+
virtual bool isBatteryConnect() override { return true; }
1694+
1695+
private:
1696+
const uint16_t OCV[NUM_OCV_POINTS] = {OCV_ARRAY};
1697+
const float chargingVolt = (OCV[0] + 10) * NUM_CELLS;
1698+
const float noBatVolt = (OCV[NUM_OCV_POINTS - 1] - 500) * NUM_CELLS;
1699+
};
1700+
1701+
Stm32wlBatteryLevel stm32wlLevel;
1702+
1703+
/**
1704+
* Init the STM32WL internal VBAT ADC channel
1705+
*/
1706+
bool Power::stm32wlInit()
1707+
{
1708+
bool result = stm32wlLevel.runOnce();
1709+
LOG_DEBUG("Power::stm32wlInit is %s", result ? "ready" : "not ready yet");
1710+
if (!result)
1711+
return false;
1712+
batteryLevel = &stm32wlLevel;
1713+
return true;
1714+
}
1715+
1716+
#else
1717+
/**
1718+
* The STM32WL battery level sensor is unavailable - default to AnalogBatteryLevel
1719+
*/
1720+
bool Power::stm32wlInit()
1721+
{
1722+
return false;
1723+
}
1724+
#endif
1725+
16051726
#ifdef HAS_SERIAL_BATTERY_LEVEL
16061727
#include <SoftwareSerial.h>
16071728

src/power.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,13 @@
1515

1616
// Device specific curves go in variant.h
1717
#ifndef OCV_ARRAY
18+
#if defined(ARCH_STM32WL) && BATTERY_PIN == AVBAT
19+
// STM32 VDD/VBAT absolute maximum is 4V so use an LFP curve
20+
#define OCV_ARRAY 3650, 3400, 3340, 3320, 3300, 3280, 3270, 3260, 3240, 3200, 2500
21+
#else
1822
#define OCV_ARRAY 4190, 4050, 3990, 3890, 3800, 3720, 3630, 3530, 3420, 3300, 3100
1923
#endif
24+
#endif
2025

2126
/*Note: 12V lead acid is 6 cells, most board accept only 1 cell LiIon/LiPo*/
2227
#ifndef NUM_CELLS
@@ -109,6 +114,8 @@ class Power : private concurrency::OSThread
109114
bool lipoChargerInit();
110115
/// Setup a meshSolar battery sensor
111116
bool meshSolarInit();
117+
/// Setup an STM32WL battery sensor
118+
bool stm32wlInit();
112119
/// Setup a serial battery sensor
113120
bool serialBatteryInit();
114121

variants/stm32/rak3172/variant.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ Do not expect a working Meshtastic device with this target.
1616
#define LED_PIN PA0 // Green LED
1717
#define LED_STATE_ON 1
1818

19+
#define BATTERY_PIN AVBAT
20+
1921
#define RAK3172
2022

2123
#endif

variants/stm32/russell/variant.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
// #define EXT_CHRG_DETECT PA5
1414
// #define EXT_PWR_DETECT PA4
1515

16+
#define BATTERY_PIN AVBAT
17+
1618
// Bosch Sensortec BME280
1719
#define HAS_SENSOR 1
1820

variants/stm32/stm32.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ build_flags =
2727
-DMESHTASTIC_EXCLUDE_TZ=1 ; Exclude TZ to save some flash space.
2828
-DSERIAL_RX_BUFFER_SIZE=256 ; For GPS - the default of 64 is too small.
2929
-DHAS_SCREEN=0 ; Always disable screen for STM32, it is not supported.
30-
-DPIO_FRAMEWORK_ARDUINO_NANOLIB_FLOAT_PRINTF ; This is REQUIRED for at least traceroute debug prints - without it the length ends up uninitialized.
30+
#-DPIO_FRAMEWORK_ARDUINO_NANOLIB_FLOAT_PRINTF ; This is REQUIRED if DEBUG_MUTE is undef - without it the length ends up uninitialized.
3131
-DDEBUG_MUTE ; You can #undef DEBUG_MUTE in certain source files if you need the logs.
3232
-fmerge-all-constants
3333
-ffunction-sections

0 commit comments

Comments
 (0)