feat(stm32): Add STM32 ADC support to AnalogBatteryLevel (#9369)

Integrate STM32 battery monitoring into AnalogBatteryLevel, supporting
external GPIO ADC pins as well as internal VBAT channel.

Features:
- ADC reading using STM32 LL (Lower Layer) macros supporting external
  ADC channels and internal VBAT channel (AVBAT)
- ADC compensation using STM32 LL macros with factory-calibrated VREFINT
  (AVREF) for accurate voltage measurement
- LFP battery OCV curve for STM32WL using AVBAT (STM32 VDD absolute
  maximum supply voltage 3.9V, direct connection of Li-Po batteries is
  not supported)

Internal VBAT channel implemented in:
- Russell
- RAK3172

In these variants, ADC_MULTIPLIER = (1.01f * 3) = 3.30 as there is a
3:1 internal divider (DS13105 Rev 12 §5.3.21), and a bit of tolerance
as the actual 10% spec leads to readings much too high.


Assisted-by: Claude:sonnet-4-5

Signed-off-by: Andrew Yong <me@ndoo.sg>
This commit is contained in:
Andrew Yong
2026-04-16 10:58:54 +01:00
committed by GitHub
co-authored by GitHub
parent 0cab43fb43
commit 026213aab7
4 changed files with 54 additions and 5 deletions
+34 -5
View File
@@ -40,6 +40,22 @@
#include "concurrency/LockGuard.h"
#endif
#if defined(ARCH_STM32WL) && defined(BATTERY_PIN)
#include "stm32yyxx_ll_adc.h"
/* Analog read resolution */
#if defined(LL_ADC_RESOLUTION_12B)
#define LL_ADC_RESOLUTION LL_ADC_RESOLUTION_12B
#define BATTERY_SENSE_RESOLUTION_BITS 12
#elif defined(LL_ADC_DS_DATA_WIDTH_12_BIT)
#define LL_ADC_RESOLUTION LL_ADC_DS_DATA_WIDTH_12_BIT
#define BATTERY_SENSE_RESOLUTION_BITS 12
#else
#error "ADC resolution could not be defined!"
#endif
#define ADC_RANGE (1 << BATTERY_SENSE_RESOLUTION_BITS)
#endif
#if defined(DEBUG_HEAP_MQTT) && !MESHTASTIC_EXCLUDE_MQTT
#include "mqtt/MQTT.h"
#include "target_specific.h"
@@ -328,11 +344,17 @@ class AnalogBatteryLevel : public HasBatteryLevel
float scaled = 0;
battery_adcEnable();
#ifdef ARCH_ESP32 // ADC block for espressif platforms
#ifdef ARCH_STM32WL
// STM32 ADC with VREFINT runtime calibration
Vref = __LL_ADC_CALC_VREFANALOG_VOLTAGE(analogRead(AVREF), LL_ADC_RESOLUTION);
raw = analogRead(BATTERY_PIN);
scaled = __LL_ADC_CALC_DATA_TO_VOLTAGE(Vref, raw, LL_ADC_RESOLUTION);
scaled *= operativeAdcMultiplier;
#elif defined(ARCH_ESP32) // ADC block for espressif platforms
raw = espAdcRead();
scaled = esp_adc_cal_raw_to_voltage(raw, adc_characs);
scaled *= operativeAdcMultiplier;
#else // block for all other platforms
#else // block for all other platforms
#ifdef ARCH_NRF52
concurrency::LockGuard saadcGuard(concurrency::nrf52SaadcLock);
#endif
@@ -530,6 +552,11 @@ class AnalogBatteryLevel : public HasBatteryLevel
bool initial_read_done = false;
float last_read_value = (OCV[NUM_OCV_POINTS - 1] * NUM_CELLS);
uint32_t last_read_time_ms = 0;
#ifdef ARCH_STM32WL
// 3300mV placeholder for STM32 errata where VREFINT factory calibration may be missing
// (e.g. STM32U0, see DS14756 Rev 3 §2.4.1 "VREFINT offset")
uint32_t Vref = 3300;
#endif
#if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && defined(HAS_RAKPROT)
@@ -639,7 +666,9 @@ bool Power::analogInit()
#define BATTERY_SENSE_RESOLUTION_BITS 10
#endif
#ifdef ARCH_ESP32 // ESP32 needs special analog stuff
#ifdef ARCH_STM32WL
analogReadResolution(BATTERY_SENSE_RESOLUTION_BITS);
#elif defined(ARCH_ESP32) // ESP32 needs special analog stuff
#ifndef ADC_WIDTH // max resolution by default
static const adc_bits_width_t width = ADC_WIDTH_BIT_12;
@@ -649,7 +678,7 @@ bool Power::analogInit()
#ifndef BAT_MEASURE_ADC_UNIT // ADC1
adc1_config_width(width);
adc1_config_channel_atten(adc_channel, atten);
#else // ADC2
#else // ADC2
adc2_config_channel_atten(adc_channel, atten);
#ifndef CONFIG_IDF_TARGET_ESP32S3
// ADC2 wifi bug workaround
@@ -679,7 +708,7 @@ bool Power::analogInit()
// NRF52 ADC init moved to powerHAL_init in nrf52 platform
#ifndef ARCH_ESP32
#if !defined(ARCH_ESP32) && !defined(ARCH_STM32WL)
analogReadResolution(BATTERY_SENSE_RESOLUTION_BITS);
#endif