From 56968cdb93bfcfa87677b15377d3cdbe3b900b4b Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Mon, 2 Nov 2026 13:29:02 -0600 Subject: [PATCH 1/3] fix(m5stack-tab5): only enable charging when a battery is fitted (periodic speaker pop) The example enabled charging unconditionally at boot. With no NP-F550 in the bay (kit-dependent) the IP2326 keeps retrying to charge nothing and every retry is a step on the supply that the amplifier reproduces as a pop, every few seconds to a couple of minutes, idle or not. Verified on hardware: the same camera-on build with charging left disabled ran five minutes of camera / rotation / touch without a pop; the audio feed and the esp_video ISP controller's sensor writes were ruled out first. - BatteryStatus::is_present now means a battery is in the bay: the INA226 pack-line voltage is at least 4 V (an empty bay reads the charger's ~2 V open-circuit output, a 2S pack never reads below ~5 V). It used to be true whenever the read succeeded. With no battery, charge_percent is 0 and is_charging is false. - The example enables charging only when a battery is present. - Ina226::conversion_ready() (CVRF) and initialize_battery_monitoring() waits for the first averaged conversion (35 ms expected, 200 ms cap), so the first read after init is real instead of zeros. - README notes on the component and the example. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU --- components/ina226/include/ina226.hpp | 16 ++++++ components/m5stack-tab5/README.md | 4 +- components/m5stack-tab5/example/README.md | 6 ++- .../example/main/m5stack_tab5_example.cpp | 15 +++++- .../m5stack-tab5/include/m5stack-tab5.hpp | 5 +- components/m5stack-tab5/src/power.cpp | 51 ++++++++++++++++--- 6 files changed, 84 insertions(+), 13 deletions(-) diff --git a/components/ina226/include/ina226.hpp b/components/ina226/include/ina226.hpp index 22d2fced76..b3f77fe409 100644 --- a/components/ina226/include/ina226.hpp +++ b/components/ina226/include/ina226.hpp @@ -123,6 +123,20 @@ class Ina226 : public BasePeripheral { return read_u16_from_register((uint8_t)Reg::DIE_ID, ec); } + /// Whether a conversion has completed since the Mask/Enable register was + /// last read (CVRF). After initialize() the data registers hold zeros until + /// the first averaged conversion finishes (averaging x (bus + shunt + /// conversion time), e.g. 16 x 2.2 ms = 35 ms at the defaults), so poll this + /// before trusting the first reading. + /// @param ec Error code to capture any read errors + /// @return true once a conversion is complete; false before that or on error + /// @note Reading the flag clears it (datasheet), so it reports "at least one + /// conversion since the last call", not the state of the converter. + bool conversion_ready(std::error_code &ec) const { + uint16_t mask = read_u16_from_register((uint8_t)Reg::MASK_ENABLE, ec); + return !ec && (mask & MASK_ENABLE_CVRF) != 0; + } + // Engineering-unit helpers /// Read shunt voltage in volts @@ -259,6 +273,8 @@ class Ina226 : public BasePeripheral { MANUFACTURER_ID = 0xFE, // 0x5449 DIE_ID = 0xFF, // 0x2260 }; + // Mask/Enable register: conversion ready flag (bit 3) + static constexpr uint16_t MASK_ENABLE_CVRF = 1 << 3; bool init(const Config &c, std::error_code &ec) { std::lock_guard lock(base_mutex_); diff --git a/components/m5stack-tab5/README.md b/components/m5stack-tab5/README.md index 1be89d579a..b208de75b5 100644 --- a/components/m5stack-tab5/README.md +++ b/components/m5stack-tab5/README.md @@ -36,7 +36,9 @@ The `espp::M5StackTab5` component provides a singleton hardware abstraction for ### Power Management - Removable NP-F550 Li-ion battery - MP4560 buck-boost converter -- IP2326 charge management +- IP2326 charge management (`set_charging_enabled()`, off by default; only + enable it when `read_battery_status().is_present` is true, since the charger + retrying to charge an empty battery bay pops the speaker periodically) - INA226 real-time power monitoring - Multiple power modes for efficiency diff --git a/components/m5stack-tab5/example/README.md b/components/m5stack-tab5/example/README.md index a9a164f411..50a3dc592f 100644 --- a/components/m5stack-tab5/example/README.md +++ b/components/m5stack-tab5/example/README.md @@ -43,7 +43,11 @@ This example demonstrates the comprehensive functionality of the M5Stack Tab5 de dropped. It is logged from an ISR (`ESP_EARLY_LOGE`), which ignores the runtime log-level controls, so it cannot be silenced the way the CCM spam is without also hiding real errors - it is left as-is. -- **Battery Management**: INA226 power monitoring with charging control +- **Battery Management**: INA226 power monitoring with charging control. + Charging is enabled only when a battery is detected in the bay + (`BatteryStatus::is_present`): with the bay empty, the IP2326 charger keeps + retrying to charge nothing, and each retry is an audible pop on the speaker + (every few seconds to a couple of minutes, even when the board is idle). ### Communication Interfaces - **microSD Card**: File system support with SDIO interface diff --git a/components/m5stack-tab5/example/main/m5stack_tab5_example.cpp b/components/m5stack-tab5/example/main/m5stack_tab5_example.cpp index 205592bf33..7f29258638 100644 --- a/components/m5stack-tab5/example/main/m5stack_tab5_example.cpp +++ b/components/m5stack-tab5/example/main/m5stack_tab5_example.cpp @@ -205,8 +205,19 @@ extern "C" void app_main(void) { return; } - // enable charging - tab5.set_charging_enabled(true); + // Enable charging only when a battery is actually in the bay. The NP-F550 is + // optional (kit-dependent); with the bay empty, the IP2326 charger keeps + // retrying to charge nothing and every retry puts an audible pop on the + // speaker (every 5 s to 2 min, idle or not), while charging an inserted pack + // is silent. + if (auto battery = tab5.read_battery_status(); battery.is_present) { + logger.info("Battery detected ({:.2f} V), enabling charging", battery.voltage_v); + tab5.set_charging_enabled(true); + } else { + logger.warn("No battery detected ({:.2f} V on the pack line), leaving charging disabled", + battery.voltage_v); + tab5.set_charging_enabled(false); + } logger.info("Initializing sound..."); // initialize the sound diff --git a/components/m5stack-tab5/include/m5stack-tab5.hpp b/components/m5stack-tab5/include/m5stack-tab5.hpp index 52a99cbbc6..b15ae32021 100644 --- a/components/m5stack-tab5/include/m5stack-tab5.hpp +++ b/components/m5stack-tab5/include/m5stack-tab5.hpp @@ -168,7 +168,10 @@ class M5StackTab5 : public BaseComponent { float power_mw; ///< Battery power in milliwatts float charge_percent; ///< Estimated charge percentage (0-100) bool is_charging; ///< True if battery is charging - bool is_present; ///< True if battery is present + bool is_present; ///< True if a battery is in the bay (pack voltage reads at least 4 V; + ///< an empty bay reads the charger's ~2 V open-circuit output). + ///< Leave charging disabled when false: the IP2326 retrying to + ///< charge an empty bay puts a periodic pop on the speaker. }; /// Expansion port configuration diff --git a/components/m5stack-tab5/src/power.cpp b/components/m5stack-tab5/src/power.cpp index e8d1dbf8a6..abb994baee 100644 --- a/components/m5stack-tab5/src/power.cpp +++ b/components/m5stack-tab5/src/power.cpp @@ -1,3 +1,6 @@ +#include +#include + #include "m5stack-tab5.hpp" namespace espp { @@ -48,6 +51,28 @@ bool M5StackTab5::initialize_battery_monitoring() { return false; } + // The INA226's data registers read as zeros until its first averaged + // conversion completes (16 x 2.2 ms = 35 ms with the settings above), and a + // 0 V reading looks exactly like an empty battery bay to read_battery_status(). + // Wait for the first conversion so the first read after this returns is real. + { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(200); + bool ready = false; + while (!ready && std::chrono::steady_clock::now() < deadline) { + ready = battery_monitor_->conversion_ready(ec); + if (ec) { + break; + } + if (!ready) { + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + } + } + if (!ready) { + logger_.warn("Battery monitor (INA226) first conversion not ready after 200 ms{}{}", + ec ? ": " : "", ec ? ec.message() : ""); + } + } + battery_monitoring_initialized_ = true; // Initialize battery status with default values @@ -82,6 +107,11 @@ bool M5StackTab5::update_battery_status() { static constexpr float num_cells = 2.0f; static constexpr float pack_voltage_min = cell_voltage_min * num_cells; static constexpr float pack_voltage_max = cell_voltage_max * num_cells; + // The INA226 sits on the pack line. With no NP-F550 in the bay it reads the + // charger's open-circuit output (about 2 V, measured); a real 2S pack never + // reads below its protection cut-off (about 5 V), so anything under this is + // an empty bay, not a flat battery. + static constexpr float pack_present_min_volts = 4.0f; std::error_code ec; std::lock_guard lock(battery_mutex_); @@ -94,14 +124,19 @@ bool M5StackTab5::update_battery_status() { battery_status_.voltage_v = vbus; battery_status_.current_ma = current_a * 1000.0f; battery_status_.power_mw = power_w * 1000.0f; - // Basic SoC estimate from voltage (very rough placeholder) - float v = vbus; - float soc = (v - pack_voltage_min) / (pack_voltage_max - pack_voltage_min); - soc = std::clamp(soc, 0.0f, 1.0f); - battery_status_.charge_percent = soc * 100.0f; - // only charging if the bit says we are and the current is < -1.0 mA - battery_status_.is_charging = get_charging_status() && battery_status_.current_ma < -1.0f; - battery_status_.is_present = true; + battery_status_.is_present = vbus >= pack_present_min_volts; + if (battery_status_.is_present) { + // Basic SoC estimate from voltage (very rough placeholder) + float v = vbus; + float soc = (v - pack_voltage_min) / (pack_voltage_max - pack_voltage_min); + soc = std::clamp(soc, 0.0f, 1.0f); + battery_status_.charge_percent = soc * 100.0f; + // only charging if the bit says we are and the current is < -1.0 mA + battery_status_.is_charging = get_charging_status() && battery_status_.current_ma < -1.0f; + } else { + battery_status_.charge_percent = 0.0f; + battery_status_.is_charging = false; + } } else { logger_.warn("Battery status not updated: {}", ec.message()); battery_status_.voltage_v = 0.0f; From 06a68a9952b07db72638df25e5c8bedec48a79d0 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Mon, 2 Nov 2026 15:18:32 -0600 Subject: [PATCH 2/3] feat(m5stack-tab5): pause battery charging while the speaker plays With a battery fitted the enabled IP2326 pops the speaker on every audio transient, whatever the charge current (hardware A/B: clean with CHG_EN low; popping with it high both while charging and with the pack full and the charger idling with quick charge off). The playback task now reports per block whether it carried audio; the charger is switched off on the first audio block and back on CHARGING_RESUME_DELAY_MS (3 s) after the last, so consecutive clicks do not toggle it. set_charging_enabled() keeps the requested state and applies an enable made during playback when playback ends; set_charging_pause_on_playback_enabled() (default on) opts out. Verified by ear with the same logic in the example first. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU --- components/m5stack-tab5/README.md | 5 ++- components/m5stack-tab5/example/README.md | 4 ++ .../m5stack-tab5/include/m5stack-tab5.hpp | 34 +++++++++++++++ components/m5stack-tab5/src/audio.cpp | 7 +++- components/m5stack-tab5/src/m5stack-tab5.cpp | 6 +++ components/m5stack-tab5/src/power.cpp | 41 +++++++++++++++++++ 6 files changed, 95 insertions(+), 2 deletions(-) diff --git a/components/m5stack-tab5/README.md b/components/m5stack-tab5/README.md index b208de75b5..a5e5be261a 100644 --- a/components/m5stack-tab5/README.md +++ b/components/m5stack-tab5/README.md @@ -38,7 +38,10 @@ The `espp::M5StackTab5` component provides a singleton hardware abstraction for - MP4560 buck-boost converter - IP2326 charge management (`set_charging_enabled()`, off by default; only enable it when `read_battery_status().is_present` is true, since the charger - retrying to charge an empty battery bay pops the speaker periodically) + retrying to charge an empty battery bay pops the speaker periodically; the + enabled charger also pops on every speaker transient, so the BSP pauses + charging during playback and resumes it 3 s after the last sound, see + `set_charging_pause_on_playback_enabled()`) - INA226 real-time power monitoring - Multiple power modes for efficiency diff --git a/components/m5stack-tab5/example/README.md b/components/m5stack-tab5/example/README.md index 50a3dc592f..b633c27642 100644 --- a/components/m5stack-tab5/example/README.md +++ b/components/m5stack-tab5/example/README.md @@ -48,6 +48,10 @@ This example demonstrates the comprehensive functionality of the M5Stack Tab5 de (`BatteryStatus::is_present`): with the bay empty, the IP2326 charger keeps retrying to charge nothing, and each retry is an audible pop on the speaker (every few seconds to a couple of minutes, even when the board is idle). + With a battery fitted, the BSP pauses charging while the speaker plays and + resumes it 3 s after the last sound (the enabled charger pops on every + audio transient, whatever the charge current); the example's battery line + shows the charger switching off during a click or playback and back on. ### Communication Interfaces - **microSD Card**: File system support with SDIO interface diff --git a/components/m5stack-tab5/include/m5stack-tab5.hpp b/components/m5stack-tab5/include/m5stack-tab5.hpp index b15ae32021..c87733ce50 100644 --- a/components/m5stack-tab5/include/m5stack-tab5.hpp +++ b/components/m5stack-tab5/include/m5stack-tab5.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -602,8 +603,27 @@ class M5StackTab5 : public BaseComponent { /// Enable/disable battery charging (IP2326 CHG_EN on 0x44 P7) /// \param enable True to enable charging, false to disable /// \return true on success + /// \note This is the requested state. While speaker playback pauses charging + /// (see set_charging_pause_on_playback_enabled()), a request to enable + /// is remembered and applied when playback has stopped. bool set_charging_enabled(bool enable); + /// Whether speaker playback pauses battery charging (default: enabled). + /// With the IP2326 charger enabled, every audio transient on the speaker + /// comes with a pop, whatever the charge current (measured on the Tab5: + /// clean with CHG_EN low, popping with it high even when the pack is full + /// and the charger idles). So while audio is being played the charger is + /// switched off, and switched back on CHARGING_RESUME_DELAY_MS after the + /// last audio block; charging a pack between sounds is not affected. + /// Disabling this leaves the charger alone while audio plays. + /// \param enable True to pause charging during playback + void set_charging_pause_on_playback_enabled(bool enable); + + /// \return Whether speaker playback pauses battery charging + bool charging_pause_on_playback_enabled() const { + return charging_pause_on_playback_enabled_.load(std::memory_order_relaxed); + } + /// Switch the 5 V supply of the USB-A host jack (USB_5V_EN on the 0x44 IO /// expander). On by default after initialize_io_expanders(); turn it off and /// back on to power-cycle an attached device (e.g. so a device attached at @@ -979,6 +999,20 @@ class M5StackTab5 : public BaseComponent { std::atomic battery_monitoring_initialized_{false}; BatteryStatus battery_status_; std::mutex battery_mutex_; + // Charger vs. speaker (see set_charging_pause_on_playback_enabled()). The + // audio playback task reports whether each block carried audio; the charger + // is switched off on the first audio block and back on this long after the + // last one, so consecutive sounds do not toggle it. + static constexpr uint32_t CHARGING_RESUME_DELAY_MS = 3000; + /// Called by the playback task for every block; pauses / resumes charging. + void update_charging_for_audio(bool audio_active); + /// Resume charging now if playback paused it (teardown, feature disabled). + void resume_charging_after_audio(); + std::mutex charging_mutex_; + bool charging_requested_{false}; // last set_charging_enabled() request + bool charging_paused_for_audio_{false}; // CHG_EN held low by playback + std::atomic charging_pause_on_playback_enabled_{true}; + std::chrono::steady_clock::time_point audio_last_active_{}; // playback task only std::shared_ptr> battery_monitor_i2c_device_; std::shared_ptr battery_monitor_; // IO expanders on the internal I2C (addresses 0x43 and 0x44 per Tab5 design) diff --git a/components/m5stack-tab5/src/audio.cpp b/components/m5stack-tab5/src/audio.cpp index fb71f32bc2..ce2b437fb1 100644 --- a/components/m5stack-tab5/src/audio.cpp +++ b/components/m5stack-tab5/src/audio.cpp @@ -265,6 +265,8 @@ bool M5StackTab5::initialize_audio(uint32_t sample_rate, // play_audio()'s notifications: forget it before the task goes away audio_tx_feed_.detach_task(); audio_task_->stop(); + // the task may have paused charging on its first blocks + resume_charging_after_audio(); return false; } @@ -376,7 +378,7 @@ bool M5StackTab5::audio_task_callback(std::mutex &m, std::condition_variable &cv const size_t block = audio_tx_buffer.size(); uint8_t *tx_buf = audio_tx_buffer.data(); bool misaligned = false; - audio_tx_feed_.fill_block(tx_buf, block, misaligned); + const size_t audio_bytes = audio_tx_feed_.fill_block(tx_buf, block, misaligned); if (misaligned) { // Producers queue whole frames only (play_audio), so this is a bug // somewhere; the feeder reset the stream rather than leave a partial frame @@ -386,6 +388,9 @@ bool M5StackTab5::audio_task_callback(std::mutex &m, std::condition_variable &cv audio_tx_feed_.misaligned_bytes, audio_tx_feed_.misaligned_bytes % audio_tx_feed_.frame_bytes, audio_tx_feed_.frame_bytes); } + // the charger pops the speaker while it is enabled: hold it off during + // playback (see set_charging_pause_on_playback_enabled) + update_charging_for_audio(audio_bytes > 0); i2s_channel_write(audio_tx_handle, tx_buf, block, NULL, portMAX_DELAY); return false; } diff --git a/components/m5stack-tab5/src/m5stack-tab5.cpp b/components/m5stack-tab5/src/m5stack-tab5.cpp index 30b7953a12..33b9879b25 100644 --- a/components/m5stack-tab5/src/m5stack-tab5.cpp +++ b/components/m5stack-tab5/src/m5stack-tab5.cpp @@ -78,6 +78,12 @@ bool M5StackTab5::set_speaker_enabled(bool enable) { } bool M5StackTab5::set_charging_enabled(bool enable) { + std::lock_guard lock(charging_mutex_); + charging_requested_ = enable; + if (enable && charging_paused_for_audio_) { + // playback holds the charger off; the request is applied when it resumes + return true; + } return set_io_expander_output(0x44, IO44_BIT_CHG_EN, enable); } diff --git a/components/m5stack-tab5/src/power.cpp b/components/m5stack-tab5/src/power.cpp index abb994baee..2d072f005f 100644 --- a/components/m5stack-tab5/src/power.cpp +++ b/components/m5stack-tab5/src/power.cpp @@ -157,6 +157,47 @@ M5StackTab5::BatteryStatus M5StackTab5::read_battery_status() { return get_battery_status(); } +void M5StackTab5::set_charging_pause_on_playback_enabled(bool enable) { + charging_pause_on_playback_enabled_.store(enable, std::memory_order_relaxed); + if (!enable) { + resume_charging_after_audio(); + } +} + +void M5StackTab5::update_charging_for_audio(bool audio_active) { + // Runs on the audio playback task once per block (about 60 Hz). The IO + // expander write happens only on the two transitions; the common case is a + // clock read and a compare. + if (!charging_pause_on_playback_enabled_.load(std::memory_order_relaxed)) { + return; + } + const auto now = std::chrono::steady_clock::now(); + if (audio_active) { + audio_last_active_ = now; + std::lock_guard lock(charging_mutex_); + if (!charging_paused_for_audio_) { + charging_paused_for_audio_ = true; + if (charging_requested_) { + set_io_expander_output(0x44, IO44_BIT_CHG_EN, false); + } + } + return; + } + if (now - audio_last_active_ >= std::chrono::milliseconds(CHARGING_RESUME_DELAY_MS)) { + resume_charging_after_audio(); + } +} + +void M5StackTab5::resume_charging_after_audio() { + std::lock_guard lock(charging_mutex_); + if (charging_paused_for_audio_) { + charging_paused_for_audio_ = false; + if (charging_requested_) { + set_io_expander_output(0x44, IO44_BIT_CHG_EN, true); + } + } +} + void M5StackTab5::enable_battery_charging(bool enable) { // Control charging via IP2326 charge management IC This is done through the // PI4IOE5V6408 GPIO expander From 853ba443679e94ca959c87d03fd1bab9aa932a36 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Mon, 2 Nov 2026 15:18:32 -0600 Subject: [PATCH 3/3] refactor(m5stack-tab5): derive the INA226 first-conversion timeout from its settings Review follow-up on #876: Ina226::conversion_time_us() (and the averaging_count / per-setting helpers) state the data-register update time for a configuration; the BSP's wait for the first conversion uses a named poll interval and a timeout that is a multiple of that expected time, and the warning reports both. Also a double space in a comment. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01DFjjnq3XCTRAXENSxsCxJU --- components/ina226/include/ina226.hpp | 23 +++++++++++++++++++++-- components/m5stack-tab5/src/power.cpp | 26 ++++++++++++++++++-------- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/components/ina226/include/ina226.hpp b/components/ina226/include/ina226.hpp index b3f77fe409..d2d9a5a296 100644 --- a/components/ina226/include/ina226.hpp +++ b/components/ina226/include/ina226.hpp @@ -47,6 +47,25 @@ class Ina226 : public BasePeripheral { MS_8_244 = 7, }; + /// Number of samples averaged for an Avg setting + static constexpr uint32_t averaging_count(Avg avg) { + constexpr uint32_t counts[] = {1, 4, 16, 64, 128, 256, 512, 1024}; + return counts[static_cast(avg) & 0x7]; + } + + /// Duration of one conversion for a ConvTime setting, in microseconds + static constexpr uint32_t conversion_time_us(ConvTime ct) { + constexpr uint32_t us[] = {140, 204, 332, 588, 1100, 2116, 4156, 8244}; + return us[static_cast(ct) & 0x7]; + } + + /// Time for one averaged bus + shunt conversion pair (one update of the + /// data registers in continuous mode), in microseconds: averaging x (bus + + /// shunt conversion time). E.g. AVG_16 with MS_1_1 for both is 35,200 us. + static constexpr uint32_t conversion_time_us(Avg avg, ConvTime bus, ConvTime shunt) { + return averaging_count(avg) * (conversion_time_us(bus) + conversion_time_us(shunt)); + } + /// Operating mode (MODE bits 2..0) enum class Mode : uint16_t { POWER_DOWN = 0, @@ -126,8 +145,8 @@ class Ina226 : public BasePeripheral { /// Whether a conversion has completed since the Mask/Enable register was /// last read (CVRF). After initialize() the data registers hold zeros until /// the first averaged conversion finishes (averaging x (bus + shunt - /// conversion time), e.g. 16 x 2.2 ms = 35 ms at the defaults), so poll this - /// before trusting the first reading. + /// conversion time), see conversion_time_us()), so poll this before trusting + /// the first reading. /// @param ec Error code to capture any read errors /// @return true once a conversion is complete; false before that or on error /// @note Reading the flag clears it (datasheet), so it reports "at least one diff --git a/components/m5stack-tab5/src/power.cpp b/components/m5stack-tab5/src/power.cpp index 2d072f005f..bc9e844fbb 100644 --- a/components/m5stack-tab5/src/power.cpp +++ b/components/m5stack-tab5/src/power.cpp @@ -52,11 +52,20 @@ bool M5StackTab5::initialize_battery_monitoring() { } // The INA226's data registers read as zeros until its first averaged - // conversion completes (16 x 2.2 ms = 35 ms with the settings above), and a - // 0 V reading looks exactly like an empty battery bay to read_battery_status(). - // Wait for the first conversion so the first read after this returns is real. + // conversion completes (averaging x (bus + shunt conversion time): 35 ms with + // the settings above), and a 0 V reading looks exactly like an empty battery + // bay to read_battery_status(). Wait for the first conversion so the first + // read after this returns is real. The timeout is a generous multiple of the + // expected time so a slow I2C bus or a busy scheduler does not trip it. { - const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(200); + static constexpr uint32_t conversion_poll_ms = 5; + static constexpr uint32_t conversion_timeout_factor = 4; + const uint32_t expected_ms = + BatteryMonitor::conversion_time_us(cfg.averaging, cfg.bus_conv_time, cfg.shunt_conv_time) / + 1000; + const uint32_t timeout_ms = + std::max(conversion_timeout_factor * expected_ms, 10 * conversion_poll_ms); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeout_ms); bool ready = false; while (!ready && std::chrono::steady_clock::now() < deadline) { ready = battery_monitor_->conversion_ready(ec); @@ -64,12 +73,13 @@ bool M5StackTab5::initialize_battery_monitoring() { break; } if (!ready) { - std::this_thread::sleep_for(std::chrono::milliseconds(5)); + std::this_thread::sleep_for(std::chrono::milliseconds(conversion_poll_ms)); } } if (!ready) { - logger_.warn("Battery monitor (INA226) first conversion not ready after 200 ms{}{}", - ec ? ": " : "", ec ? ec.message() : ""); + logger_.warn("Battery monitor (INA226) first conversion not ready after {} ms (expected " + "{} ms){}{}", + timeout_ms, expected_ms, ec ? ": " : "", ec ? ec.message() : ""); } } @@ -131,7 +141,7 @@ bool M5StackTab5::update_battery_status() { float soc = (v - pack_voltage_min) / (pack_voltage_max - pack_voltage_min); soc = std::clamp(soc, 0.0f, 1.0f); battery_status_.charge_percent = soc * 100.0f; - // only charging if the bit says we are and the current is < -1.0 mA + // only charging if the bit says we are and the current is < -1.0 mA battery_status_.is_charging = get_charging_status() && battery_status_.current_ma < -1.0f; } else { battery_status_.charge_percent = 0.0f;