From 8fa7ae40a767fb2762a5dec5c220409fcf09eca1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20G=C3=B6ttgens?= Date: Mon, 18 May 2026 23:41:41 +0200 Subject: [PATCH] enable Charging Indicator --- src/Power.cpp | 15 +++ src/power/SGM41562.cpp | 109 +++++++++++++++++++++ src/power/SGM41562.h | 105 ++++++++++++++++++++ variants/nrf52840/t-impulse-plus/variant.h | 6 ++ 4 files changed, 235 insertions(+) create mode 100644 src/power/SGM41562.cpp create mode 100644 src/power/SGM41562.h diff --git a/src/Power.cpp b/src/Power.cpp index f752e9461..ef1143d02 100644 --- a/src/Power.cpp +++ b/src/Power.cpp @@ -23,6 +23,7 @@ #include "main.h" #include "meshUtils.h" #include "power/PowerHAL.h" +#include "power/SGM41562.h" #include "sleep.h" #if defined(ARCH_PORTDUINO) @@ -453,6 +454,10 @@ class AnalogBatteryLevel : public HasBatteryLevel /// source virtual bool isVbusIn() override { +#ifdef HAS_SGM41562 + if (sgm41562 && sgm41562->refresh()) + return sgm41562->isInputPowerGood(); +#endif #ifdef EXT_PWR_DETECT #if defined(HELTEC_CAPSULE_SENSOR_V3) || defined(HELTEC_SENSOR_HUB) // if external powered that pin will be pulled down @@ -483,6 +488,10 @@ class AnalogBatteryLevel : public HasBatteryLevel /// we can't be smart enough to say 'full'? virtual bool isCharging() override { +#ifdef HAS_SGM41562 + if (sgm41562 && sgm41562->refresh()) + return sgm41562->isCharging(); +#endif #if HAS_TELEMETRY && !MESHTASTIC_EXCLUDE_ENVIRONMENTAL_SENSOR && defined(HAS_RAKPROT) && !defined(HAS_PMU) if (hasRAK()) { return (rak9154Sensor.isCharging()) ? OptTrue : OptFalse; @@ -697,6 +706,12 @@ bool Power::analogInit() */ bool Power::setup() { +#ifdef HAS_SGM41562 + // Initialize the charger early so AnalogBatteryLevel can read charging + // state from it. The charger does not provide battery voltage / percent — + // those still come from the platform ADC via analogInit() below. + initSGM41562(SGM41562_WIRE); +#endif bool found = false; if (axpChipInit()) { found = true; diff --git a/src/power/SGM41562.cpp b/src/power/SGM41562.cpp new file mode 100644 index 000000000..4f6acf406 --- /dev/null +++ b/src/power/SGM41562.cpp @@ -0,0 +1,109 @@ +#include "SGM41562.h" + +#ifdef HAS_SGM41562 + +#include + +SGM41562 *sgm41562 = nullptr; + +bool initSGM41562(TwoWire &wire) +{ + if (sgm41562) + return true; + sgm41562 = new SGM41562(); + if (!sgm41562->begin(wire)) { + delete sgm41562; + sgm41562 = nullptr; + return false; + } + return true; +} + +bool SGM41562::readReg(uint8_t reg, uint8_t &value) +{ + wire_->beginTransmission(address_); + wire_->write(reg); + if (wire_->endTransmission(false) != 0) + return false; + if (wire_->requestFrom((int)address_, 1) != 1) + return false; + value = wire_->read(); + return true; +} + +bool SGM41562::writeReg(uint8_t reg, uint8_t value) +{ + wire_->beginTransmission(address_); + wire_->write(reg); + wire_->write(value); + return wire_->endTransmission() == 0; +} + +bool SGM41562::updateReg(uint8_t reg, uint8_t mask, uint8_t value) +{ + uint8_t cur; + if (!readReg(reg, cur)) + return false; + cur = (cur & ~mask) | (value & mask); + return writeReg(reg, cur); +} + +bool SGM41562::begin(TwoWire &wire, uint8_t address) +{ + wire_ = &wire; + address_ = address; + + uint8_t id; + if (!readReg(REG_DEVICE_ID, id)) { + LOG_WARN("SGM41562: I2C read failed at 0x%02X", address_); + return false; + } + if (id != DEVICE_ID_EXPECTED) { + LOG_WARN("SGM41562: unexpected device ID 0x%02X (expected 0x%02X)", id, DEVICE_ID_EXPECTED); + return false; + } + LOG_INFO("SGM41562: detected at 0x%02X (id 0x%02X)", address_, id); + + // Mirror the vendor reference init sequence: PCB OTP off, NTC off, + // watchdog off, charger enabled. These match LilyGo's stock firmware + // for the T-Impulse Plus. + delay(120); + writeReg(REG_SYS_VOLTAGE_REG, 0xB7); + writeReg(REG_MISC_OP_CONTROL, 0x40); + writeReg(REG_CHARGE_TERM_TIMER, 0x1A); + writeReg(REG_POWER_ON_CFG, 0xA4); + + return refresh(); +} + +bool SGM41562::refresh() +{ + uint32_t now = millis(); + if (lastRefreshMs_ != 0 && (now - lastRefreshMs_) < 250) + return true; // cached + lastRefreshMs_ = now == 0 ? 1 : now; + + uint8_t status, fault; + if (!readReg(REG_SYSTEM_STATUS, status)) + return false; + if (!readReg(REG_FAULT, fault)) + return false; + + chargeStatus_ = static_cast((status >> SYS_STATUS_CHRG_SHIFT) & SYS_STATUS_CHRG_MASK); + inputPowerGood_ = (status & SYS_STATUS_PG) != 0; + thermalReg_ = (status & SYS_STATUS_THERM_REG) != 0; + faultMask_ = fault & 0x3F; // bits [7:6] are enter_ship_time config, not faults + return true; +} + +bool SGM41562::setChargeEnable(bool enable) +{ + return updateReg(REG_POWER_ON_CFG, POWER_ON_CFG_CHG_DISABLE, enable ? 0x00 : POWER_ON_CFG_CHG_DISABLE); +} + +bool SGM41562::setShippingModeEnable(bool enable) +{ + return updateReg(REG_MISC_OP_CONTROL, MISC_OP_SHIPPING_MODE, enable ? MISC_OP_SHIPPING_MODE : 0x00); +} + +#endif // HAS_SGM41562 diff --git a/src/power/SGM41562.h b/src/power/SGM41562.h new file mode 100644 index 000000000..a75f781df --- /dev/null +++ b/src/power/SGM41562.h @@ -0,0 +1,105 @@ +#pragma once + +#include "configuration.h" + +#ifdef HAS_SGM41562 + +#include +#include + +// SG Micro SGM41562 — single-cell Li-ion buck charger, I²C-controlled, no +// fuel gauge. This driver exposes status (charging / input good / fault), +// charge enable, and shipping-mode control. Battery voltage/percent still +// come from the platform ADC path; the charger is plumbed in as a +// side-channel for isCharging()/isVbusIn() in AnalogBatteryLevel. +// +// Reference: SGM41562 datasheet (Cmd map + bit fields cross-verified against +// LilyGo's `Cpp_Bus_Driver::Sgm41562xx` driver, which is what their vendor +// example for this board uses). + +#ifndef SGM41562_ADDR +#define SGM41562_ADDR 0x03 // Per datasheet — unusual but correct +#endif + +#ifndef SGM41562_WIRE +#define SGM41562_WIRE Wire1 // Most boards put the PMU on the secondary bus +#endif + +class SGM41562 +{ + public: + enum class ChargeStatus : uint8_t { + NotCharging = 0b00, + Precharge = 0b01, + FastCharge = 0b10, + ChargeDone = 0b11, + }; + + bool begin(TwoWire &wire, uint8_t address = SGM41562_ADDR); + + // Re-read the system status + fault registers. Throttled internally to + // at most one I²C transaction per 250 ms — call as often as you like. + bool refresh(); + + // Status — cached from the most recent refresh(). + ChargeStatus chargeStatus() const { return chargeStatus_; } + bool isCharging() const + { + return chargeStatus_ == ChargeStatus::Precharge || chargeStatus_ == ChargeStatus::FastCharge; + } + bool isChargeDone() const { return chargeStatus_ == ChargeStatus::ChargeDone; } + bool isInputPowerGood() const { return inputPowerGood_; } + bool isThermalRegulation() const { return thermalReg_; } + uint8_t faultMask() const { return faultMask_; } + + // Control. + bool setChargeEnable(bool enable); + bool setShippingModeEnable(bool enable); + + private: + TwoWire *wire_ = nullptr; + uint8_t address_ = SGM41562_ADDR; + uint32_t lastRefreshMs_ = 0; + + ChargeStatus chargeStatus_ = ChargeStatus::NotCharging; + bool inputPowerGood_ = false; + bool thermalReg_ = false; + uint8_t faultMask_ = 0; + + bool readReg(uint8_t reg, uint8_t &value); + bool writeReg(uint8_t reg, uint8_t value); + bool updateReg(uint8_t reg, uint8_t mask, uint8_t value); + + // SGM41562 register addresses + static constexpr uint8_t REG_INPUT_SOURCE = 0x00; + static constexpr uint8_t REG_POWER_ON_CFG = 0x01; + static constexpr uint8_t REG_CHARGE_CURRENT = 0x02; + static constexpr uint8_t REG_DISCHARGE_TERM_CURRENT = 0x03; + static constexpr uint8_t REG_CHARGE_VOLTAGE = 0x04; + static constexpr uint8_t REG_CHARGE_TERM_TIMER = 0x05; + static constexpr uint8_t REG_MISC_OP_CONTROL = 0x06; + static constexpr uint8_t REG_SYS_VOLTAGE_REG = 0x07; + static constexpr uint8_t REG_SYSTEM_STATUS = 0x08; + static constexpr uint8_t REG_FAULT = 0x09; + static constexpr uint8_t REG_I2C_ADDR_MISC = 0x0A; + static constexpr uint8_t REG_DEVICE_ID = 0x0B; + + // Bit positions in REG_POWER_ON_CFG. + static constexpr uint8_t POWER_ON_CFG_CHG_DISABLE = 0x08; // bit 3: 1 = charging disabled + // Bit positions in REG_MISC_OP_CONTROL. + static constexpr uint8_t MISC_OP_SHIPPING_MODE = 0x20; // bit 5: 1 = enter shipping mode + // Bit positions in REG_SYSTEM_STATUS. + static constexpr uint8_t SYS_STATUS_CHRG_SHIFT = 3; + static constexpr uint8_t SYS_STATUS_CHRG_MASK = 0x03; + static constexpr uint8_t SYS_STATUS_PG = 0x02; // bit 1: input power good + static constexpr uint8_t SYS_STATUS_THERM_REG = 0x01; // bit 0: thermal regulation + + static constexpr uint8_t DEVICE_ID_EXPECTED = 0x04; +}; + +extern SGM41562 *sgm41562; + +// Lazy-instantiate the global on the supplied wire. Returns true on success. +bool initSGM41562(TwoWire &wire); + +#endif // HAS_SGM41562 diff --git a/variants/nrf52840/t-impulse-plus/variant.h b/variants/nrf52840/t-impulse-plus/variant.h index 4c789d7ab..0b5d2e22d 100644 --- a/variants/nrf52840/t-impulse-plus/variant.h +++ b/variants/nrf52840/t-impulse-plus/variant.h @@ -157,6 +157,12 @@ static const uint8_t SCL = PIN_WIRE_SCL; // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ #define HAS_ICM20948 +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +// Charger (SGM41562 on Wire1 @ 0x03) +// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +#define HAS_SGM41562 +#define SGM41562_WIRE Wire1 + // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ // Compatibility Definitions // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━