From 16a61a087cd34a391aed870e8839ceb839400c7b Mon Sep 17 00:00:00 2001 From: Crazypedia Date: Thu, 9 Jul 2026 16:55:14 -0400 Subject: [PATCH] Add power device (battery + power-off) to lilygo-tdeck-max (#553) --- Devices/lilygo-tdeck-max/CMakeLists.txt | 2 +- .../lilygo-tdeck-max/Source/Configuration.cpp | 9 ++ .../Source/devices/TdeckmaxPower.cpp | 139 ++++++++++++++++++ .../Source/devices/TdeckmaxPower.h | 42 ++++++ 4 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 Devices/lilygo-tdeck-max/Source/devices/TdeckmaxPower.cpp create mode 100644 Devices/lilygo-tdeck-max/Source/devices/TdeckmaxPower.h diff --git a/Devices/lilygo-tdeck-max/CMakeLists.txt b/Devices/lilygo-tdeck-max/CMakeLists.txt index b241604b..53724a09 100644 --- a/Devices/lilygo-tdeck-max/CMakeLists.txt +++ b/Devices/lilygo-tdeck-max/CMakeLists.txt @@ -3,5 +3,5 @@ file(GLOB_RECURSE SOURCE_FILES Source/*.c*) idf_component_register( SRCS ${SOURCE_FILES} INCLUDE_DIRS "Source" - REQUIRES Tactility GDEQ031T10 TCA8418 driver + REQUIRES Tactility GDEQ031T10 TCA8418 BQ27220 driver ) diff --git a/Devices/lilygo-tdeck-max/Source/Configuration.cpp b/Devices/lilygo-tdeck-max/Source/Configuration.cpp index 2b96b870..228ba8d6 100644 --- a/Devices/lilygo-tdeck-max/Source/Configuration.cpp +++ b/Devices/lilygo-tdeck-max/Source/Configuration.cpp @@ -1,5 +1,8 @@ #include "devices/Display.h" #include "devices/TdeckmaxKeyboard.h" +#include "devices/TdeckmaxPower.h" + +#include #include #include @@ -82,6 +85,12 @@ static DeviceVector createDevices() { devices.push_back(keypad); devices.push_back(std::make_shared(keypad)); + // Battery metrics from the BQ27220 fuel gauge (0x55); power-off via the + // SY6970 charger (0x6A). Enables the launcher's power-off button. + auto gauge = std::make_shared(i2c); + devices.push_back(gauge); + devices.push_back(std::make_shared(gauge, i2c)); + return devices; } diff --git a/Devices/lilygo-tdeck-max/Source/devices/TdeckmaxPower.cpp b/Devices/lilygo-tdeck-max/Source/devices/TdeckmaxPower.cpp new file mode 100644 index 00000000..dd42ead5 --- /dev/null +++ b/Devices/lilygo-tdeck-max/Source/devices/TdeckmaxPower.cpp @@ -0,0 +1,139 @@ +#include "TdeckmaxPower.h" + +#include +#include + +constexpr auto* TAG = "TdeckmaxPower"; + +// SY6970 charger (newer T-Deck Max revisions; older boards use BQ25896 @ 0x6B). +// Register map is BQ25896-compatible for everything used here (see NOTES.md). +static constexpr uint8_t SY6970_ADDRESS = 0x6A; +static constexpr uint8_t SY6970_REG_03 = 0x03; // bit4 CHG_CONFIG: charge enable +static constexpr uint8_t SY6970_REG_04 = 0x04; // bits[6:0] ICHG: charge current, 64 mA/LSB +static constexpr uint8_t SY6970_REG_06 = 0x06; // bits[7:2] VREG: charge voltage, 3840 mV + n*16 mV +static constexpr uint8_t SY6970_REG_07 = 0x07; // bits[5:4] WATCHDOG: I2C watchdog timer +static constexpr uint8_t SY6970_REG_09 = 0x09; +static constexpr uint8_t SY6970_REG_0B = 0x0B; // read-only status +static constexpr uint8_t SY6970_CHG_CONFIG = 1 << 4; +static constexpr uint8_t SY6970_BATFET_DIS = 1 << 5; // Force BATFET off = ship mode + +static constexpr TickType_t I2C_TIMEOUT = pdMS_TO_TICKS(50); + +// Charge parameters from the vendor factory firmware (XPowersLib SY6970 init): +// watchdog off, 4288 mV target, 1024 mA fast-charge current. The I2C watchdog +// must be disabled first, otherwise the chip reverts custom register values to +// defaults after the watchdog expires. Input current limit (REG00) is left at +// the hardware PSEL/ILIM default. Non-fatal: charging still works on chip +// defaults if any write fails. +bool TdeckmaxPower::configureCharger() const { + if (i2c_controller_register8_reset_bits(i2c, SY6970_ADDRESS, SY6970_REG_07, 0x30, I2C_TIMEOUT) != ERROR_NONE) { + LOG_E(TAG, "Failed to disable SY6970 watchdog"); + return false; + } + + uint8_t reg06 = 0; + if (i2c_controller_register8_get(i2c, SY6970_ADDRESS, SY6970_REG_06, ®06, I2C_TIMEOUT) != ERROR_NONE || + i2c_controller_register8_set(i2c, SY6970_ADDRESS, SY6970_REG_06, (reg06 & 0x03) | (28 << 2), I2C_TIMEOUT) != ERROR_NONE) { // 3840 + 28*16 = 4288 mV + LOG_E(TAG, "Failed to set SY6970 charge voltage"); + return false; + } + + uint8_t reg04 = 0; + if (i2c_controller_register8_get(i2c, SY6970_ADDRESS, SY6970_REG_04, ®04, I2C_TIMEOUT) != ERROR_NONE || + i2c_controller_register8_set(i2c, SY6970_ADDRESS, SY6970_REG_04, (reg04 & 0x80) | 0x10, I2C_TIMEOUT) != ERROR_NONE) { // 16 * 64 = 1024 mA + LOG_E(TAG, "Failed to set SY6970 charge current"); + return false; + } + + LOG_I(TAG, "SY6970 configured: 4288 mV / 1024 mA, watchdog off"); + return true; +} + +bool TdeckmaxPower::isAllowedToCharge() const { + uint8_t reg03 = 0; + if (i2c_controller_register8_get(i2c, SY6970_ADDRESS, SY6970_REG_03, ®03, I2C_TIMEOUT) != ERROR_NONE) { + return false; + } + return (reg03 & SY6970_CHG_CONFIG) != 0; +} + +void TdeckmaxPower::setAllowedToCharge(bool canCharge) { + error_t result; + if (canCharge) { + result = i2c_controller_register8_set_bits(i2c, SY6970_ADDRESS, SY6970_REG_03, SY6970_CHG_CONFIG, I2C_TIMEOUT); + } else { + result = i2c_controller_register8_reset_bits(i2c, SY6970_ADDRESS, SY6970_REG_03, SY6970_CHG_CONFIG, I2C_TIMEOUT); + } + if (result != ERROR_NONE) { + LOG_E(TAG, "Failed to set SY6970 charge enable"); + } +} + +bool TdeckmaxPower::supportsMetric(MetricType type) const { + switch (type) { + using enum MetricType; + case IsCharging: + case Current: + case BatteryVoltage: + case ChargeLevel: + return true; + default: + return false; + } +} + +bool TdeckmaxPower::getMetric(MetricType type, MetricData& data) { + uint16_t u16 = 0; + int16_t s16 = 0; + switch (type) { + using enum MetricType; + case IsCharging: { + // REG0B CHRG_STAT bits[4:3]: 0 = not charging, 1 = pre-charge, + // 2 = fast charge, 3 = charge done. Only 1|2 count as charging — + // "done" must not (XPowersLib had this inverted once). Fall back to + // the fuel gauge's discharge flag if the charger doesn't answer. + uint8_t reg0b = 0; + if (i2c_controller_register8_get(i2c, SY6970_ADDRESS, SY6970_REG_0B, ®0b, I2C_TIMEOUT) == ERROR_NONE) { + uint8_t charge_status = (reg0b >> 3) & 0x03; + data.valueAsBool = charge_status == 1 || charge_status == 2; + return true; + } + Bq27220::BatteryStatus status; + if (gauge->getBatteryStatus(status)) { + data.valueAsBool = !status.reg.DSG; + return true; + } + return false; + } + case Current: + if (gauge->getCurrent(s16)) { + data.valueAsInt32 = s16; + return true; + } + return false; + case BatteryVoltage: + if (gauge->getVoltage(u16)) { + data.valueAsUint32 = u16; + return true; + } + return false; + case ChargeLevel: + if (gauge->getStateOfCharge(u16)) { + data.valueAsUint8 = u16; + return true; + } + return false; + default: + return false; + } +} + +void TdeckmaxPower::powerOff() { + // Ship mode: force the charger's BATFET off (REG09 bit5). Mirrors the vendor + // XPowersLib PowersSY6970::shutdown(). Note: this only fully powers the board + // down when running on battery with USB unplugged. + LOG_I(TAG, "Power off (SY6970 BATFET_DIS)"); + if (i2c_controller_register8_set_bits(i2c, SY6970_ADDRESS, SY6970_REG_09, SY6970_BATFET_DIS, pdMS_TO_TICKS(50)) != ERROR_NONE) { + LOG_E(TAG, "Failed to write SY6970 shutdown register"); + } +} diff --git a/Devices/lilygo-tdeck-max/Source/devices/TdeckmaxPower.h b/Devices/lilygo-tdeck-max/Source/devices/TdeckmaxPower.h new file mode 100644 index 00000000..b308e656 --- /dev/null +++ b/Devices/lilygo-tdeck-max/Source/devices/TdeckmaxPower.h @@ -0,0 +1,42 @@ +#pragma once + +#include "Tactility/hal/power/PowerDevice.h" + +#include +#include + +#include + +using tt::hal::power::PowerDevice; + +// Power device for the LilyGO T-Deck Max. Battery metrics come from the BQ27220 +// fuel gauge (I2C 0x55); charging status, charge control and power-off (ship +// mode, REG09 BATFET_DIS) use the SY6970 charger. Newer T-Deck Max revisions +// ship the SY6970 (0x6A) instead of the older BQ25896 (0x6B); this board has +// the SY6970 (confirmed by I2C scan). The two are register-compatible for +// everything used here. +class TdeckmaxPower final : public PowerDevice { + std::shared_ptr gauge; + ::Device* i2c; // i2c0 controller, used to reach the SY6970 charger + + bool configureCharger() const; + +public: + + TdeckmaxPower(const std::shared_ptr& gauge, ::Device* i2c) : gauge(gauge), i2c(i2c) { + configureCharger(); + } + + std::string getName() const final { return "T-Deck Max Power"; } + std::string getDescription() const final { return "Battery metrics (BQ27220), SY6970 charging and power-off"; } + + bool supportsMetric(MetricType type) const override; + bool getMetric(MetricType type, MetricData& data) override; + + bool supportsChargeControl() const override { return true; } + bool isAllowedToCharge() const override; + void setAllowedToCharge(bool canCharge) override; + + bool supportsPowerOff() const override { return true; } + void powerOff() override; +};