feat(stm32): Add STM32 ADC support to AnalogBatteryLevel#9369
feat(stm32): Add STM32 ADC support to AnalogBatteryLevel#9369Stary2001 merged 1 commit intomeshtastic:developfrom
Conversation
|
Relocate to a more appropriate separate child class since this code really should not be mucking with the usual voltage divider code. |
44ffbad to
d7d7ad8
Compare
|
Tested on RAK3172 in a RAK19007, seems to work and returns 3.3V. |
|
Whoops, going to reduce that terribly long commit message and force-push |
|
Don't close :( |
|
I’ll test this on a few variants I have around the house, like the Wio E5
dev kit, then report back.
I don’t really see any issue with it as-is but further comments from other
STM32WL users welcome.
…On Fri, Mar 27, 2026 at 19:40 Ben Meadors ***@***.***> wrote:
Reopened #9369 <#9369>.
—
Reply to this email directly, view it on GitHub
<#9369?email_source=notifications&email_token=AACAFAAFGHUKI3WOBBCIJ6L4SZSENA5CNFSNUABQM5UWIORPF5TWS5BNNB2WEL2JONZXKZKFOZSW45CON52GSZTJMNQXI2LPNYXTEMZZGY4TEMBRHEZDJJTSMVQXG33OUZQXK5DIN5ZKKZLWMVXHJLDGN5XXIZLSL5RWY2LDNM#event-23969201924>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AACAFAB47SW2OVODELBZPXL4SZSENAVCNFSM6AAAAACSH46N3WVHI2DSMVQWIX3LMV45UABCJFZXG5LFIV3GK3TUJZXXI2LGNFRWC5DJN5XDWMRTHE3DSMRQGE4TENA>
.
You are receiving this because you authored the thread.Message ID:
***@***.***>
|
There was a problem hiding this comment.
Pull request overview
Adds STM32WL-specific battery voltage sensing by reading the MCU’s internal VBAT ADC channel, and prefers this sensor over the generic analog divider approach when available.
Changes:
- Add a new STM32WL
HasBatteryLevelimplementation that reads VBAT via internal ADC channels and integrates it intoPower::setup(). - Select an LFP-oriented default OCV curve when using STM32WL internal VBAT sensing.
- Enable
BATTERY_PIN = AVBATfor the Russell and RAK3172 STM32WL variants; reduce STM32WL flash usage by disabling nanolib float printf unless debug logs are enabled.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| variants/stm32/stm32.ini | Disables PIO_FRAMEWORK_ARDUINO_NANOLIB_FLOAT_PRINTF by default for STM32WL to save flash (kept available for non-muted debug). |
| variants/stm32/russell/variant.h | Enables internal VBAT sensing via BATTERY_PIN AVBAT and adds a sample (commented) primary-cell OCV curve. |
| variants/stm32/rak3172/variant.h | Enables internal VBAT sensing via BATTERY_PIN AVBAT. |
| src/power.h | Chooses an LFP default OCV curve when using STM32WL internal VBAT sensing; adds stm32wlInit() declaration. |
| src/Power.cpp | Implements Stm32wlBatteryLevel and wires stm32wlInit() into the sensor selection order ahead of analogInit(). |
|
I’ll follow up on the review and rework if necessary. Thanks.
…On Fri, Mar 27, 2026 at 19:50 Copilot ***@***.***> wrote:
***@***.**** commented on this pull request.
Pull request overview
Adds STM32WL-specific battery voltage sensing by reading the MCU’s
internal VBAT ADC channel, and prefers this sensor over the generic analog
divider approach when available.
*Changes:*
- Add a new STM32WL HasBatteryLevel implementation that reads VBAT via
internal ADC channels and integrates it into Power::setup().
- Select an LFP-oriented default OCV curve when using STM32WL internal
VBAT sensing.
- Enable BATTERY_PIN = AVBAT for the Russell and RAK3172 STM32WL
variants; reduce STM32WL flash usage by disabling nanolib float printf
unless debug logs are enabled.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and
generated 5 comments.
Show a summary per file
File Description
variants/stm32/stm32.ini Disables
PIO_FRAMEWORK_ARDUINO_NANOLIB_FLOAT_PRINTF by default for STM32WL to save
flash (kept available for non-muted debug).
variants/stm32/russell/variant.h Enables internal VBAT sensing via BATTERY_PIN
AVBAT and adds a sample (commented) primary-cell OCV curve.
variants/stm32/rak3172/variant.h Enables internal VBAT sensing via BATTERY_PIN
AVBAT.
src/power.h Chooses an LFP default OCV curve when using STM32WL internal
VBAT sensing; adds stm32wlInit() declaration.
src/Power.cpp Implements Stm32wlBatteryLevel and wires stm32wlInit() into
the sensor selection order ahead of analogInit().
------------------------------
In src/Power.cpp
<#9369?email_source=notifications&email_token=AACAFAHUCYJLWINL3UKRPNT4SZTG5A5CNFSNUABKM5UWIORPF5TWS5BNNB2WEL2QOVWGYUTFOF2WK43UKJSXM2LFO4XTIMBSGA3DIMRWGE3KM4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2K64DSL5ZGK5TJMV3V6Y3MNFRWW#discussion_r3000549926>
:
> +#ifdef __LL_ADC_CALC_VREFANALOG_VOLTAGE
+ Vref = __LL_ADC_CALC_VREFANALOG_VOLTAGE(analogRead(AVREF), LL_ADC_RESOLUTION);
+#else
+ Vref = VREFINT * ADC_RANGE / analogRead(AVREF); // ADC sample to mV
The fallback VREF calculation (VREFINT * ADC_RANGE / analogRead(AVREF))
can divide by zero if analogRead(AVREF) returns 0 (ADC not ready /
channel not enabled). Even if this path is rarely compiled, it should guard
against a 0 read and return false (so stm32wlInit() can fall back) or
keep the previous Vref value.
⬇️ Suggested change
-#ifdef __LL_ADC_CALC_VREFANALOG_VOLTAGE
- Vref = __LL_ADC_CALC_VREFANALOG_VOLTAGE(analogRead(AVREF), LL_ADC_RESOLUTION);
-#else
- Vref = VREFINT * ADC_RANGE / analogRead(AVREF); // ADC sample to mV
+ int avrefSample = analogRead(AVREF);
+ if (avrefSample == 0) {
+ // ADC not ready or channel not enabled; keep previous Vref and signal failure
+ return false;
+ }
+#ifdef __LL_ADC_CALC_VREFANALOG_VOLTAGE
+ Vref = __LL_ADC_CALC_VREFANALOG_VOLTAGE(avrefSample, LL_ADC_RESOLUTION);
+#else
+ Vref = VREFINT * ADC_RANGE / avrefSample; // ADC sample to mV
------------------------------
In src/Power.cpp
<#9369?email_source=notifications&email_token=AACAFADV3YJ7PLMECHY6ZGD4SZTG7A5CNFSNUABKM5UWIORPF5TWS5BNNB2WEL2QOVWGYUTFOF2WK43UKJSXM2LFO4XTIMBSGA3DIMRWGE3KM4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2K64DSL5ZGK5TJMV3V6Y3MNFRWW#discussion_r3000549969>
:
> + virtual uint16_t getBattVoltage() override
+ {
+ // VBAT pin is internally connected to a bridge divider by three (DS13105§3.18.3)
+ return 3 * __LL_ADC_CALC_DATA_TO_VOLTAGE(Vref, analogRead(BATTERY_PIN), LL_ADC_RESOLUTION);
+ }
Unlike AnalogBatteryLevel::getBattVoltage(), the STM32WL implementation
does no throttling/filtering of ADC reads. Because Power::runOnce()
initially runs with the thread's default period (0ms) until statusHandler
is initialized, this can result in rapid repeated analogRead() calls
during boot. Consider adding the same minimum read interval / smoothing
used by AnalogBatteryLevel (or otherwise rate-limit VBAT/VREFINT reads).
------------------------------
In src/Power.cpp
<#9369?email_source=notifications&email_token=AACAFADV3YJ7PLMECHY6ZGD4SZTG7A5CNFSNUABKM5UWIORPF5TWS5BNNB2WEL2QOVWGYUTFOF2WK43UKJSXM2LFO4XTIMBSGA3DIMRWGE3KM4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2K64DSL5ZGK5TJMV3V6Y3MNFRWW#discussion_r3000549984>
:
> + {
+#ifdef __LL_ADC_CALC_VREFANALOG_VOLTAGE
+ Vref = __LL_ADC_CALC_VREFANALOG_VOLTAGE(analogRead(AVREF), LL_ADC_RESOLUTION);
+#else
+ Vref = VREFINT * ADC_RANGE / analogRead(AVREF); // ADC sample to mV
+#endif
Stm32wlBatteryLevel uses __LL_ADC_CALC_* macros with LL_ADC_RESOLUTION_12B,
but this path never calls
analogReadResolution(BATTERY_SENSE_RESOLUTION_BITS) (that only happens in
analogInit(), which is skipped when stm32wlInit() succeeds). If the
Arduino core default resolution is not 12-bit, both the VREFINT calibration
and VBAT conversion will be scaled incorrectly. Set the analog read
resolution before calling analogRead(AVREF) / analogRead(AVBAT) (e.g., in
stm32wlInit() or Stm32wlBatteryLevel::runOnce()).
------------------------------
In src/Power.cpp
<#9369?email_source=notifications&email_token=AACAFADV3YJ7PLMECHY6ZGD4SZTG7A5CNFSNUABKM5UWIORPF5TWS5BNNB2WEL2QOVWGYUTFOF2WK43UKJSXM2LFO4XTIMBSGA3DIMRWGE3KM4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2K64DSL5ZGK5TJMV3V6Y3MNFRWW#discussion_r3000550000>
:
> + bool runOnce()
+ {
+#ifdef __LL_ADC_CALC_VREFANALOG_VOLTAGE
+ Vref = __LL_ADC_CALC_VREFANALOG_VOLTAGE(analogRead(AVREF), LL_ADC_RESOLUTION);
+#else
+ Vref = VREFINT * ADC_RANGE / analogRead(AVREF); // ADC sample to mV
+#endif
+ return true;
+ }
+
+ /**
+ * Read VBAT in mV
+ */
+ virtual uint16_t getBattVoltage() override
+ {
+ // VBAT pin is internally connected to a bridge divider by three (DS13105§3.18.3)
+ return 3 * __LL_ADC_CALC_DATA_TO_VOLTAGE(Vref, analogRead(BATTERY_PIN), LL_ADC_RESOLUTION);
+ }
Vref is measured once and then reused for all future VBAT reads. If VBAT
is tied to (or tracks) VDD, the VBAT ADC reading is a ratio against VDD and
will not reflect supply changes; using a cached Vref will make the
computed battery voltage effectively constant after boot. To keep VBAT
readings accurate as VDD/battery changes, recompute Vref (via a
VREFINT/AVREF read) whenever you read VBAT, or at least periodically
alongside VBAT sampling.
------------------------------
In src/Power.cpp
<#9369?email_source=notifications&email_token=AACAFADV3YJ7PLMECHY6ZGD4SZTG7A5CNFSNUABKM5UWIORPF5TWS5BNNB2WEL2QOVWGYUTFOF2WK43UKJSXM2LFO4XTIMBSGA3DIMRWGE3KM4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2K64DSL5ZGK5TJMV3V6Y3MNFRWW#discussion_r3000550025>
:
> +#else
+#error "ADC resolution could not be defined!"
+#endif
+#define ADC_RANGE 4096
In the LL_ADC_DS_DATA_WIDTH_12_BIT branch, BATTERY_SENSE_RESOLUTION_BITS
is not defined, but the STM32WL VBAT code still assumes a 12-bit scale (
ADC_RANGE is also hard-coded to 4096). Define
BATTERY_SENSE_RESOLUTION_BITS (and ensure ADC_RANGE matches) for this
branch as well so the resolution configuration stays consistent across
STM32 LL variants.
⬇️ Suggested change
-#else
-#error "ADC resolution could not be defined!"
-#endif
-#define ADC_RANGE 4096
+#define BATTERY_SENSE_RESOLUTION_BITS 12
+#else
+#error "ADC resolution could not be defined!"
+#endif
+#define ADC_RANGE (1 << BATTERY_SENSE_RESOLUTION_BITS)
—
Reply to this email directly, view it on GitHub
<#9369?email_source=notifications&email_token=AACAFAC4IC4VELMLOBUTTPD4SZTG7A5CNFSNUABKM5UWIORPF5TWS5BNNB2WEL2QOVWGYUTFOF2WK43UKJSXM2LFO4XTIMBSGA3DIMRWGE3KM4TFMFZW63VGMF2XI2DPOKSWK5TFNZ2L24DSL5ZGK5TJMV3V63TPORUWM2LDMF2GS33OONPWG3DJMNVQ#pullrequestreview-4020642616>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AACAFAC7JYILWKDSO5PVQDL4SZTG7AVCNFSM6AAAAACSH46N3WVHI2DSMVQWIX3LMV43YUDVNRWFEZLROVSXG5CSMV3GSZLXHM2DAMRQGY2DENRRGY>
.
You are receiving this because you authored the thread.Message ID:
***@***.***>
|
78b2da6 to
5b7d59a
Compare
|
|
CI errors look like HTTP errors? |
2b162e6 to
978ff4b
Compare
|
A new class seems to be the wrong way to go, along the way I had to re-implement EXT_PWR_DETECT, EXT_CHG_DETECT etc. In the end, just implementing the correct STM32 patterns within AnalogBatteryLevel looks like the way to go. Reworked from scratch and force-pushed. Updating the PR text shortly. |
|
Ignore the bad timestamps from the ongoing RTC work, but so far so good, after using AnalogBatteryLevel implementation and the CHRG/DONE pins on CN3158 (with #10140 to directly connect these to STM32WLE GPIO)
|
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. Signed-off-by: Andrew Yong <me@ndoo.sg> Assisted-by: Claude:sonnet-4-5
|
Reducing the extra ADC_MULTIPLIER tolerance to 1% as the readings come in way too high now with the full 10% tolerance. |
|
Tested on wio-e5 custom board, seems OK with analog reference and VBAT paths. |
Per reply in Discord, set it to 1.01f *3 in variants where it's implemented. Future variant makers must remember to do this when using AVBAT channel. It’s intentionally not done in code just in case there is any use case for a custom multiplier even on AVBAT. |
|


Integrate STM32 battery monitoring into AnalogBatteryLevel, supporting external GPIO ADC pins as well as internal VBAT channel.
Features:
Internal VBAT channel implemented in:
In these variants, ADC_MULTIPLIER = (1.01f * 3) = 3.30; This is due to the 3:1 internal divider with a 10% tolerance (DS13105 Rev 12 §5.3.21).
🤝 Attestations