diff --git a/drivers/fuel_gauge/ltc2959/CMakeLists.txt b/drivers/fuel_gauge/ltc2959/CMakeLists.txt new file mode 100644 index 000000000000..9be2a23df981 --- /dev/null +++ b/drivers/fuel_gauge/ltc2959/CMakeLists.txt @@ -0,0 +1,3 @@ +zephyr_library() +zephyr_include_directories(${CMAKE_CURRENT_SOURCE_DIR}) +zephyr_library_sources(ltc2959.c) diff --git a/drivers/fuel_gauge/ltc2959/Kconfig b/drivers/fuel_gauge/ltc2959/Kconfig new file mode 100644 index 000000000000..8a60728ab779 --- /dev/null +++ b/drivers/fuel_gauge/ltc2959/Kconfig @@ -0,0 +1,25 @@ +# Copyright (c) 2025 Nathan Winslow +# SPDX-License-Identifier: Apache-2.0 + +menu "LTC2959 Configuration" + +config FUEL_GAUGE_LTC2959 + + depends on DT_HAS_ADI_LTC2959_ENABLED + bool "LTC2959 Fuel Gauge" + default y + select I2C + help + Enable the LTC2959 fuel gauge driver from Analog Devices. + + +config FUEL_GAUGE_LTC2959_RSENSE_MOHMS + + depends on FUEL_GAUGE_LTC2959 + int "Sense resistor value in milliohms" + default 80 + help + Set the value of the RSENSE resistor in milliohms. + This is used to calculate the current and charge resolution. + +endmenu diff --git a/drivers/fuel_gauge/ltc2959/ltc2959.c b/drivers/fuel_gauge/ltc2959/ltc2959.c new file mode 100644 index 000000000000..50e59f051c25 --- /dev/null +++ b/drivers/fuel_gauge/ltc2959/ltc2959.c @@ -0,0 +1,559 @@ +#include "ltc2959.h" +#include "zephyr/device.h" +#include "zephyr/drivers/i2c.h" +#include +#include +#include +#include + +#define DT_DRV_COMPAT adi_ltc2959 + +LOG_MODULE_REGISTER(LTC2959); + +static int ltc2959_read16(const struct device *dev, uint8_t reg, uint16_t *value) +{ + uint8_t buf[2]; + const struct ltc2959_config *cfg = dev->config; + + int ret = i2c_burst_read_dt(&cfg->i2c, reg, buf, sizeof(buf)); + if (ret < 0) { + LOG_ERR("Failed to read 16-bit register 0x%02X", reg); + return ret; + } + + *value = sys_get_be16(buf); + return 0; +} + +static int ltc2959_read32(const struct device *dev, uint8_t reg, uint32_t *value) +{ + uint8_t buf[4]; + const struct ltc2959_config *cfg = dev->config; + + int ret = i2c_burst_read_dt(&cfg->i2c, reg, buf, sizeof(buf)); + if (ret < 0) { + LOG_ERR("Failed to read 32-bit register 0x%02X", reg); + return ret; + } + + *value = sys_get_be32(buf); + return 0; +} + +static int ltc2959_get_adc_mode(const struct device *dev, uint8_t* mode) +{ + const struct ltc2959_config* cfg = dev->config; + return i2c_reg_read_byte_dt(&cfg->i2c, LTC2959_REG_ADC_CONTROL, mode); +} + +static int ltc2959_set_adc_mode(const struct device *dev, uint8_t mode) +{ + const struct ltc2959_config *cfg = dev->config; + + int ret = i2c_reg_write_byte_dt(&cfg->i2c, LTC2959_REG_ADC_CONTROL, mode); + if (ret < 0) { + LOG_ERR("Failed to set ADC mode: 0x%02x", mode); + return ret; + } + return 0; +} + +static int ltc2959_get_cc_config(const struct device *dev, uint8_t *value) +{ + const struct ltc2959_config *cfg = dev->config; + return i2c_reg_read_byte_dt(&cfg->i2c, LTC2959_REG_CC_CONTROL, value); +} + +static int ltc2959_set_cc_config(const struct device *dev, uint8_t mask) +{ + const struct ltc2959_config *cfg = dev->config; + mask &= LTC2959_CC_WRITABLE_MASK ; + mask |= LTC2959_CC_RESERVED_VALUE; + LOG_INF("setting cc to: 0x%02X", mask); + return i2c_reg_write_byte_dt(&cfg->i2c, LTC2959_REG_CC_CONTROL, mask); +} + +static int ltc2959_write_acr(const struct device *dev, uint32_t value) +{ + const struct ltc2959_config *cfg = dev->config; + uint8_t buf[4] = { + (value >> 24) & 0xFF, + (value >> 16) & 0xFF, + (value >> 8) & 0xFF, + value & 0xFF + }; + return i2c_burst_write_dt(&cfg->i2c, LTC2959_REG_ACC_CHARGE_3, buf, sizeof(buf)); +} + +static int ltc2959_hold_accumulator(const struct device *dev, bool hold) +{ + uint8_t ctrl; + int ret = ltc2959_get_cc_config(dev, &ctrl); + if (ret < 0) { + return ret; + } + ctrl = hold ? (ctrl | LTC2959_CC_HOLD_BIT) : (ctrl & ~LTC2959_CC_HOLD_BIT); + return ltc2959_set_cc_config(dev, ctrl); +} + +static int ltc2959_get_gpio_voltage_uv(const struct device *dev, int32_t *value_uv) +{ + uint8_t ctrl; + uint16_t raw; + int ret; + ltc2959_get_adc_mode(dev, &ctrl); + + ret = ltc2959_read16(dev, LTC2959_REG_GPIO_VOLTAGE_MSB, &raw); + if (ret < 0) { + return ret; + } + + int16_t raw_signed = (int16_t)raw; + uint8_t gpio_mode = ctrl & LTC2959_CTRL_GPIO_MODE_MASK; + + switch (gpio_mode) { + case LTC2959_GPIO_MODE_BIPOLAR: + /** + * Bipolar ±97.5 mV + * Voltage = (97.5 mV × raw_signed) / 32768 = raw × 2970 / 1000 + */ + *value_uv = (raw_signed * LTC2959_GPIO_BIPOLAR_UV_SF) >> 15; + break; + + case LTC2959_GPIO_MODE_UNIPOLAR: + /** + * Unipolar 0–1.56 V + * Voltage = (1.56 V × raw_unsigned) / 65536 = raw × 1560000 / 65536 + * = raw × 23.8 µV + */ + *value_uv = (raw_signed * LTC2959_GPIO_UNIPOLAR_UV_SF) >> 16; + break; + + default: + LOG_ERR("Unsupported GPIO analog mode: 0x%x", gpio_mode); + return -EINVAL; + } + return 0; +} + +static int ltc2959_get_gpio_threshold_uv(const struct device *dev, bool high, int32_t *value_uv) +{ + uint8_t reg = high ? LTC2959_REG_GPIO_THRESH_HIGH_MSB + : LTC2959_REG_GPIO_THRESH_LOW_MSB; + + const struct ltc2959_config *cfg = dev->config; + + uint8_t ctrl; + int ret = i2c_reg_read_byte_dt(&cfg->i2c, LTC2959_REG_ADC_CONTROL, &ctrl); + if (ret < 0) { + LOG_ERR("NO CTRL: %i", ret); + return ret; + } + + uint16_t raw; + ret = ltc2959_read16(dev, reg, &raw); + if (ret < 0) { + return ret; + } + + int16_t raw_signed = (int16_t)raw; + uint8_t gpio_mode = ctrl & LTC2959_CTRL_GPIO_MODE_MASK; + + switch (gpio_mode) { + case LTC2959_GPIO_MODE_BIPOLAR: + *value_uv = (raw_signed * LTC2959_GPIO_BIPOLAR_UV_SF) >> 15; + break; + case LTC2959_GPIO_MODE_UNIPOLAR: + *value_uv = (raw_signed * LTC2959_GPIO_UNIPOLAR_UV_SF) >> 16; + break; + default: + LOG_ERR("Unsupported GPIO mode: 0x%x", gpio_mode); + return -ENOTSUP; + } + return 0; +} + +static int ltc2959_set_gpio_threshold_uv(const struct device *dev, bool high, int32_t value_uv) +{ + uint8_t reg = high ? LTC2959_REG_GPIO_THRESH_HIGH_MSB + : LTC2959_REG_GPIO_THRESH_LOW_MSB; + + const struct ltc2959_config *cfg = dev->config; + + uint8_t ctrl; + int ret = i2c_reg_read_byte_dt(&cfg->i2c, LTC2959_REG_ADC_CONTROL, &ctrl); + if (ret < 0) { + return ret; + } + + uint8_t gpio_mode = ctrl & LTC2959_CTRL_GPIO_MODE_MASK; + int16_t raw; + + switch (gpio_mode) { + case LTC2959_GPIO_MODE_BIPOLAR: + raw = ((int32_t)value_uv << 15) / LTC2959_GPIO_BIPOLAR_UV_SF; + break; + + case LTC2959_GPIO_MODE_UNIPOLAR: + raw = (value_uv << 16) / LTC2959_GPIO_UNIPOLAR_UV_SF; + break; + + case LTC2959_GPIO_MODE_ALERT: + case LTC2959_GPIO_MODE_CHGCOMP: + default: + LOG_ERR("Unsupported GPIO mode: 0x%02x", gpio_mode); + return -ENOTSUP; + } + + uint8_t buf[2] = { raw >> 8, raw & 0xFF }; + return i2c_burst_write_dt(&cfg->i2c, reg, buf, sizeof(buf)); +} + +static int ltc2959_get_voltage_threshold_mv(const struct device *dev, bool high, uint16_t* value) +{ + uint8_t reg = high ? LTC2959_REG_VOLT_THRESH_HIGH_MSB : LTC2959_REG_VOLT_THRESH_LOW_MSB; + uint16_t raw; + int ret = ltc2959_read16(dev, reg, &raw); + if (ret < 0) { + LOG_ERR("ERROR: %i", ret); + return ret; + } + *value = (uint32_t)(raw * LTC2959_VOLT_MV_SF) >> 16; + return 0; +} + +static int ltc2959_set_voltage_threshold_mv(const struct device *dev, bool high, uint16_t value_mv) +{ + uint8_t reg = high ? LTC2959_REG_VOLT_THRESH_HIGH_MSB + : LTC2959_REG_VOLT_THRESH_LOW_MSB; + + // NOTE: use either version based on needs/accuracy. + // const uint16_t raw = ((uint32_t)value_mv << 16) / 62600; + const uint16_t raw = ((uint32_t)value_mv * LTC2959_RAW_TO_MV_SF) >> 16; + + uint8_t buf[2] = {raw >> 8, raw & 0xFF}; + const struct ltc2959_config *cfg = dev->config; + return i2c_burst_write_dt(&cfg->i2c, reg, buf, sizeof(buf)); +} + +static int ltc2959_get_current_threshold_ua(const struct device *dev, bool high, int16_t *value_ua) +{ + uint8_t reg = high ? LTC2959_REG_CURR_THRESH_HIGH_MSB + : LTC2959_REG_CURR_THRESH_LOW_MSB; + + uint16_t raw; + int ret = ltc2959_read16(dev, reg, &raw); + if (ret < 0) { + return ret; + } + + int16_t signed_raw = (int16_t)raw; + *value_ua = signed_raw * LTC2959_CURRENT_LSB_UV; + return 0; +} + +static int ltc2959_set_current_threshold_ua(const struct device *dev, bool high, int16_t value_ua) +{ + uint8_t reg = high ? LTC2959_REG_CURR_THRESH_HIGH_MSB + : LTC2959_REG_CURR_THRESH_LOW_MSB; + + /* Reverse of: current = raw * LSB → raw = current / LSB */ + int16_t raw = value_ua / LTC2959_CURRENT_LSB_UV; + + uint8_t buf[2] = { raw >> 8, raw & 0xFF }; + const struct ltc2959_config *cfg = dev->config; + return i2c_burst_write_dt(&cfg->i2c, reg, buf, sizeof(buf)); +} + +static int ltc2959_get_temp_threshold_dK(const struct device *dev, bool high, uint16_t *value_dK) +{ + uint8_t reg = high ? LTC2959_REG_TEMP_THRESH_HIGH_MSB + : LTC2959_REG_TEMP_THRESH_LOW_MSB; + + uint16_t raw; + int ret = ltc2959_read16(dev, reg, &raw); + if (ret < 0) { + return ret; + } + + *value_dK = ((uint32_t)raw * LTC2959_TEMP_K_SF) >> 16; + return 0; +} + +static int ltc2959_set_temp_threshold_dK(const struct device *dev, bool high, uint16_t value_dK) +{ + uint8_t reg = high ? LTC2959_REG_TEMP_THRESH_HIGH_MSB + : LTC2959_REG_TEMP_THRESH_LOW_MSB; + + uint16_t raw = ((uint32_t)value_dK << 16) / LTC2959_TEMP_K_SF; + + uint8_t buf[2] = { raw >> 8, raw & 0xFF }; + const struct ltc2959_config *cfg = dev->config; + return i2c_burst_write_dt(&cfg->i2c, reg, buf, sizeof(buf)); +} + + +static int ltc2959_get_prop(const struct device *dev, + fuel_gauge_prop_t prop, + union fuel_gauge_prop_val *val) +{ + const struct ltc2959_config *cfg = dev->config; + int ret; + uint8_t raw; + + switch (prop) { + case FUEL_GAUGE_STATUS: + ret = i2c_reg_read_byte_dt(&cfg->i2c, LTC2959_REG_STATUS, &raw); + if (ret < 0) { + return ret; + } + val->fg_status = raw; + return 0; + + case FUEL_GAUGE_VOLTAGE: + uint16_t raw_voltage; + ret = ltc2959_read16(dev, LTC2959_REG_VOLTAGE_MSB, &raw_voltage); + if (ret < 0) { + return ret; + } + /** + * NOTE: LSB = 62.6V / 65536 = ~955 µV + * Zephyr's API expects this value in microvolts + * https://docs.zephyrproject.org/latest/doxygen/html/group__fuel__gauge__interface.html + */ + val->voltage = raw_voltage * LTC2959_RAW_TO_UVOLT_SF; + return 0; + + case FUEL_GAUGE_CURRENT: + uint16_t raw_current; + ret = ltc2959_read16(dev, LTC2959_REG_CURRENT_MSB, &raw_current); + if (ret < 0) { + return ret; + } + /* Signed 16-bit value from ADC */ + int16_t current_raw = (int16_t)raw_current; + val->current = current_raw * LTC2959_CURRENT_LSB_UV; + return 0; + + case FUEL_GAUGE_TEMPERATURE: + uint16_t raw_temp; + ret = ltc2959_read16(dev, LTC2959_REG_TEMP_MSB, &raw_temp); + if (ret < 0) { + return ret; + } + /** + * NOTE: + * Temp is in deciKelvin as per API requirements. + * from the datasheet: + * T(°C) = 825 * (raw / 65536) - 273.15 + * T(dK) = 8250 * (raw / 65536) + * 65536 = 2 ^ 16, so we can avoid division altogether. + */ + val->temperature = ((uint32_t)raw_temp * LTC2959_TEMP_K_SF) >> 16; + return 0; + + case FUEL_GAUGE_CHARGE_CURRENT: + uint32_t raw; + ret = ltc2959_read32(dev, LTC2959_REG_ACC_CHARGE_3, &raw); + if (ret < 0) { + return ret; + } + val->chg_current = ((uint64_t)raw * LTC2959_CHARGE_UAH_SF) >> 16; + return 0; + + case FUEL_GAUGE_ADC_MODE: + ret = ltc2959_get_adc_mode(dev, (uint8_t*)val->flags); + break; + + case FUEL_GAUGE_VOLTAGE_HIGH_THRESHOLD: + ret = ltc2959_get_voltage_threshold_mv(dev, true, (uint16_t*)&val->design_volt); + break; + + case FUEL_GAUGE_VOLTAGE_LOW_THRESHOLD: + ret = ltc2959_get_voltage_threshold_mv(dev, false, (uint16_t*)&val->design_volt); + break; + + case FUEL_GAUGE_CURRENT_HIGH_THRESHOLD: + int16_t hi_curr; + ret = ltc2959_get_current_threshold_ua(dev, true, &hi_curr); + if (ret == 0) { + val->current = hi_curr; + } + break; + + case FUEL_GAUGE_CURRENT_LOW_THRESHOLD: + int16_t lo_curr; + ret = ltc2959_get_current_threshold_ua(dev, false, &lo_curr); + if (ret == 0) { + val->current = lo_curr; + } + break; + + case FUEL_GAUGE_TEMPERATURE_HIGH_THRESHOLD: + ret = ltc2959_get_temp_threshold_dK(dev, true, &val->temperature); + break; + + case FUEL_GAUGE_TEMPERATURE_LOW_THRESHOLD: + ret = ltc2959_get_temp_threshold_dK(dev, false, &val->temperature); + break; + + case FUEL_GAUGE_GPIO_VOLTAGE_UV: + ret = ltc2959_get_gpio_voltage_uv(dev, &val->voltage); + break; + + case FUEL_GAUGE_GPIO_HIGH_THRESHOLD: + int32_t hi_gpio; + ret = ltc2959_get_gpio_threshold_uv(dev, true, &hi_gpio); + if (ret == 0) { + val->voltage = hi_gpio; + } + break; + + case FUEL_GAUGE_GPIO_LOW_THRESHOLD: + int32_t lo_gpio; + ret = ltc2959_get_gpio_threshold_uv(dev, false, &lo_gpio); + if (ret == 0) { + val->voltage = lo_gpio; + } + break; + + case FUEL_GAUGE_CC_CONFIG: + uint8_t cc_ctrl; + ret = ltc2959_get_cc_config(dev, &cc_ctrl); + if (ret == 0) { + val->fg_status = cc_ctrl; + } + break; + + default: + return -ENOTSUP; + } + return 0; +} + +//TODO: bind to DEVICE_API when supported +#if 0 +static int ltc2959_get_props(const struct device *dev, + fuel_gauge_prop_t *props, + union fuel_gauge_prop_val *vals, + size_t len) +{ + + int ret; + for (size_t i = 0; i < len; ++i) { + ret = ltc2959_get_prop(dev, props[i], &vals[i]); + if (ret < 0) { + return ret; + } + } + return 0; + +} +#endif + +static int ltc2959_set_prop(const struct device *dev, + fuel_gauge_prop_t prop, + union fuel_gauge_prop_val val) +{ + int ret = 0; + + switch (prop) { + case FUEL_GAUGE_ADC_MODE: + ret = ltc2959_set_adc_mode(dev, val.fg_status); + break; + + case FUEL_GAUGE_VOLTAGE_LOW_THRESHOLD: + ret = ltc2959_set_voltage_threshold_mv(dev, false, val.voltage); + break; + + case FUEL_GAUGE_VOLTAGE_HIGH_THRESHOLD: + ret = ltc2959_set_voltage_threshold_mv(dev, true, val.voltage); + break; + + case FUEL_GAUGE_CURRENT_LOW_THRESHOLD: + ret = ltc2959_set_current_threshold_ua(dev, false, val.current); + break; + + case FUEL_GAUGE_CURRENT_HIGH_THRESHOLD: + ret = ltc2959_set_current_threshold_ua(dev, true, val.current); + break; + + case FUEL_GAUGE_TEMPERATURE_LOW_THRESHOLD: + ret = ltc2959_set_temp_threshold_dK(dev, false, val.temperature); + break; + + case FUEL_GAUGE_TEMPERATURE_HIGH_THRESHOLD: + ret = ltc2959_set_temp_threshold_dK(dev, true, val.temperature); + break; + + case FUEL_GAUGE_GPIO_HIGH_THRESHOLD: + ret = ltc2959_set_gpio_threshold_uv(dev, true, val.voltage); + break; + case FUEL_GAUGE_GPIO_LOW_THRESHOLD: + ret = ltc2959_set_gpio_threshold_uv(dev, false, val.voltage); + break; + + case FUEL_GAUGE_CC_CONFIG: + LOG_INF("config stats: 0x%02X", val.fg_status); + ret = ltc2959_set_cc_config(dev, val.fg_status); + break; + + case FUEL_GAUGE_CC_CLEAR: + uint8_t adc_mode; + ret = ltc2959_get_adc_mode(dev, &adc_mode); + if (ret < 0) { + break; + } + + uint8_t gpio_mode = adc_mode & LTC2959_CTRL_GPIO_MODE_MASK; + if (gpio_mode == LTC2959_GPIO_MODE_CHGCOMP) { + // NOTE: In CHGCOMP mode, ACR is cleared by pulling GPIO low externally + LOG_WRN("ACR not cleared in firmware: GPIO is in charge-complete input mode"); + break; + } + // NOTE: clearing to 0xFFFFFFFF to match GPIO behavior + ret = ltc2959_write_acr(dev, LTC2959_ACR_CLR); + break; + + case FUEL_GAUGE_CC_HOLD: + ret = ltc2959_hold_accumulator(dev, val.flags); // use flags = true/false + break; + + default: + ret = -ENOTSUP; + break; + } + return ret; +} + + +static int ltc2959_init(const struct device *dev) +{ + const struct ltc2959_config *cfg = dev->config; + + if (!device_is_ready(cfg->i2c.bus)) { + LOG_ERR("I2C bus not ready"); + return -ENODEV; + } + + return 0; +} + +static DEVICE_API(fuel_gauge, ltc2959_driver_api) = { + .get_property = <c2959_get_prop, + .set_property = <c2959_set_prop +}; + +#define LTC2959_DEFINE(inst) \ + static const struct ltc2959_config ltc2959_config_##inst = { \ + .i2c = I2C_DT_SPEC_INST_GET(inst) \ + }; \ + DEVICE_DT_INST_DEFINE(inst, \ + ltc2959_init, \ + NULL, \ + NULL, <c2959_config_##inst, \ + POST_KERNEL, CONFIG_FUEL_GAUGE_INIT_PRIORITY, \ + <c2959_driver_api); + +DT_INST_FOREACH_STATUS_OKAY(LTC2959_DEFINE) + diff --git a/drivers/fuel_gauge/ltc2959/ltc2959.h b/drivers/fuel_gauge/ltc2959/ltc2959.h new file mode 100644 index 000000000000..ce25785c7600 --- /dev/null +++ b/drivers/fuel_gauge/ltc2959/ltc2959.h @@ -0,0 +1,187 @@ +/* + * Copyright (c) 2025, Nathan Winslow + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef ZEPHYR_DRIVERS_FUELGAUGE_LTC2959_GAUGE_H_ +#define ZEPHYR_DRIVERS_FUELGAUGE_LTC2959_GAUGE_H_ + +#include +#include + +/* LTC2959 Register Map — from datasheet (Rev A) */ + +/* Status and Control */ +#define LTC2959_REG_STATUS (0x00) +#define LTC2959_REG_ADC_CONTROL (0x01) +#define LTC2959_REG_CC_CONTROL (0x02) + +/* Accumulated Charge (uint32_t) */ +#define LTC2959_REG_ACC_CHARGE_3 (0x03) +#define LTC2959_REG_ACC_CHARGE_2 (0x04) +#define LTC2959_REG_ACC_CHARGE_1 (0x05) +#define LTC2959_REG_ACC_CHARGE_0 (0x06) + +/* Charge Thresholds (Low and High, uint32_t each) */ +#define LTC2959_REG_CHG_THRESH_LOW_3 (0x07) +#define LTC2959_REG_CHG_THRESH_LOW_2 (0x08) +#define LTC2959_REG_CHG_THRESH_LOW_1 (0x09) +#define LTC2959_REG_CHG_THRESH_LOW_0 (0x0A) + +#define LTC2959_REG_CHG_THRESH_HIGH_3 (0x0B) +#define LTC2959_REG_CHG_THRESH_HIGH_2 (0x0C) +#define LTC2959_REG_CHG_THRESH_HIGH_1 (0x0D) +#define LTC2959_REG_CHG_THRESH_HIGH_0 (0x0E) + +/* Voltage (uint16_t) */ +#define LTC2959_REG_VOLTAGE_MSB (0x0F) +#define LTC2959_REG_VOLTAGE_LSB (0x10) +#define LTC2959_REG_VOLT_THRESH_HIGH_MSB (0x11) +#define LTC2959_REG_VOLT_THRESH_HIGH_LSB (0x12) +#define LTC2959_REG_VOLT_THRESH_LOW_MSB (0x13) +#define LTC2959_REG_VOLT_THRESH_LOW_LSB (0x14) + +#define LTC2959_REG_MAX_VOLTAGE_MSB (0x15) +#define LTC2959_REG_MAX_VOLTAGE_LSB (0x16) +#define LTC2959_REG_MIN_VOLTAGE_MSB (0x17) +#define LTC2959_REG_MIN_VOLTAGE_LSB (0x18) + +/* Current (int16_t) */ +#define LTC2959_REG_CURRENT_MSB (0x19) +#define LTC2959_REG_CURRENT_LSB (0x1A) + +#define LTC2959_REG_CURR_THRESH_HIGH_MSB (0x1B) +#define LTC2959_REG_CURR_THRESH_HIGH_LSB (0x1C) + +#define LTC2959_REG_CURR_THRESH_LOW_MSB (0x1D) +#define LTC2959_REG_CURR_THRESH_LOW_LSB (0x1E) + +#define LTC2959_REG_MAX_CURRENT_MSB (0x1F) +#define LTC2959_REG_MAX_CURRENT_LSB (0x20) +#define LTC2959_REG_MIN_CURRENT_MSB (0x21) +#define LTC2959_REG_MIN_CURRENT_LSB (0x22) + +/* Temperature (uint16_t) */ +#define LTC2959_REG_TEMP_MSB (0x23) +#define LTC2959_REG_TEMP_LSB (0x24) + +#define LTC2959_REG_TEMP_THRESH_HIGH_MSB (0x25) +#define LTC2959_REG_TEMP_THRESH_HIGH_LSB (0x26) + +#define LTC2959_REG_TEMP_THRESH_LOW_MSB (0x27) +#define LTC2959_REG_TEMP_THRESH_LOW_LSB (0x28) + +/* GPIO */ +#define LTC2959_REG_GPIO_VOLTAGE_MSB (0x29) +#define LTC2959_REG_GPIO_VOLTAGE_LSB (0x2A) + +#define LTC2959_REG_GPIO_THRESH_HIGH_MSB (0x2B) +#define LTC2959_REG_GPIO_THRESH_HIGH_LSB (0x2C) + +#define LTC2959_REG_GPIO_THRESH_LOW_MSB (0x2D) +#define LTC2959_REG_GPIO_THRESH_LOW_LSB (0x2E) + +/* STATUS Register Bit Definitions (0x00) */ +#define LTC2959_STATUS_GPIO_ALERT BIT(7) +#define LTC2959_STATUS_CURRENT_ALERT BIT(6) +#define LTC2959_STATUS_CHARGE_OVER_UNDER BIT(5) +#define LTC2959_STATUS_TEMP_ALERT BIT(4) +#define LTC2959_STATUS_CHARGE_HIGH BIT(3) +#define LTC2959_STATUS_CHARGE_LOW BIT(2) +#define LTC2959_STATUS_VOLTAGE_ALERT BIT(1) +#define LTC2959_STATUS_UVLO BIT(0) + +/* CONTROL Register Bit Masks */ +#define LTC2959_CTRL_ADC_MODE_MASK GENMASK(7, 5) +#define LTC2959_CTRL_GPIO_MODE_MASK GENMASK(4, 3) +#define LTC2959_CTRL_VIN_SEL_BIT BIT(2) +#define LTC2959_CTRL_RESERVED_MASK GENMASK(1, 0) + +/* CONTROL Register Values for ADC Mode */ +#define LTC2959_CTRL_VIN_VDD (0x0 << 2) +#define LTC2959_CTRL_GPIO_ALERT (0x0 << 3) + +/* ADC mode values (bits 7:5 of CONTROL register 0x01) */ +#define LTC2959_ADC_MODE_SLEEP (0x0 << 5) +#define LTC2959_ADC_MODE_SMART_SLEEP (0x1 << 5) +#define LTC2959_ADC_MODE_CONT_V (0x2 << 5) +#define LTC2959_ADC_MODE_CONT_I (0x3 << 5) +#define LTC2959_ADC_MODE_CONT_VI (0x4 << 5) +#define LTC2959_ADC_MODE_SINGLE_SHOT (0x5 << 5) +#define LTC2959_ADC_MODE_CONT_VIT (0x6 << 5) // recommended for full telemetry + +/* GPIO mode bits (bits 4:3) */ +#define LTC2959_GPIO_MODE_ALERT (0x00) +#define LTC2959_GPIO_MODE_CHGCOMP (0x08) +#define LTC2959_GPIO_MODE_BIPOLAR (0x10) +#define LTC2959_GPIO_MODE_UNIPOLAR (0x18) + +/* Voltage source selection (bit 2) */ +#define LTC2959_VIN_VDD (0x0 << 2) +#define LTC2959_VIN_SENSEN (0x1 << 2) + +/* CC Control bits (bits 7:5 of CC register 0x02)*/ +#define LTC2959_CC_ACCUM_CLR_BIT BIT(7) +#define LTC2959_CC_THRESH_EN_BIT BIT(6) +#define LTC2959_CC_HOLD_BIT BIT(5) +#define LTC2959_CC_ENABLE_THRESH LTC2959_CC_THRESH_EN_BIT +#define LTC2959_CC_HOLD LTC2959_CC_HOLD_BIT +#define LTC2959_CC_CLEAR LTC2959_CC_ACCUM_CLR_BIT +#define LTC2959_CC_DEADBAND_0UV (0b00 << 6) +#define LTC2959_CC_DEADBAND_20UV (0b01 << 6) +#define LTC2959_CC_DEADBAND_40UV (0b10 << 6) +#define LTC2959_CC_DEADBAND_80UV (0b11 << 6) +#define LTC2959_CC_DO_NOT_COUNT (BIT(3)) +#define LTC2959_CC_RESERVED_MASK (BIT(4)) // Bit 4 = 1, Bit 5 = 0 — reserved pattern: 0b01 +#define LTC2959_CC_RESERVED_VALUE (0b01 << 4) +#define LTC2959_CC_WRITABLE_MASK (LTC2959_CC_ACCUM_CLR_BIT | \ + LTC2959_CC_THRESH_EN_BIT | \ + LTC2959_CC_DO_NOT_COUNT) + +/* Used when ACR is controlled via firmware */ +#define LTC2959_ACR_CLR (0xFFFFFFFF) + +#ifdef CONFIG_FUEL_GAUGE_LTC2959_RSENSE_MOHMS +/* Use Kconfig-defined value if available */ +#define LTC2959_RSENSE_mOHMS CONFIG_FUEL_GAUGE_LTC2959_RSENSE_MOHMS +#else +#define LTC2959_RSENSE_mOHMS (80) +#endif + +/* LSB in µV across the sense resistor */ +#define LTC2959_CURRENT_LSB_UV (97500000 / (LTC2959_RSENSE_mOHMS * 32768)) + +// NOTE: For 3.8V → raw = (3800 * 1049) >> 4 +#define LTC2959_VOLT_TO_RAW_SF (1049) +#define LTC2959_RAW_TO_UVOLT_SF (955) // µV per LSB + +// NOTE: QLSB = 533 nAh = ~0.533 µAh → raw × (34952 / 65536) +#define LTC2959_CHARGE_UAH_SF (34952) +#define LTC2959_TEMP_K_SF (8250) +#define LTC2959_VOLT_MV_SF (62600) +#define LTC2959_RAW_TO_MV_SF (68607) +#define LTC2959_GPIO_BIPOLAR_UV_SF (97500) +#define LTC2959_GPIO_UNIPOLAR_UV_SF (1560000) + +enum ltc2959_custom_props { + FUEL_GAUGE_ADC_MODE = FUEL_GAUGE_CUSTOM_BEGIN, //0x8000 + FUEL_GAUGE_VOLTAGE_HIGH_THRESHOLD, + FUEL_GAUGE_VOLTAGE_LOW_THRESHOLD, + FUEL_GAUGE_CURRENT_HIGH_THRESHOLD, + FUEL_GAUGE_CURRENT_LOW_THRESHOLD, + FUEL_GAUGE_TEMPERATURE_HIGH_THRESHOLD, + FUEL_GAUGE_TEMPERATURE_LOW_THRESHOLD, + FUEL_GAUGE_GPIO_VOLTAGE_UV, + FUEL_GAUGE_GPIO_HIGH_THRESHOLD, + FUEL_GAUGE_GPIO_LOW_THRESHOLD, + FUEL_GAUGE_CC_CONFIG, + FUEL_GAUGE_CC_CLEAR, + FUEL_GAUGE_CC_HOLD +}; + +struct ltc2959_config { + struct i2c_dt_spec i2c; +}; + +#endif diff --git a/dts/bindings/fuel-gauge/adi,ltc2959.yaml b/dts/bindings/fuel-gauge/adi,ltc2959.yaml new file mode 100644 index 000000000000..56d5ab7bd272 --- /dev/null +++ b/dts/bindings/fuel-gauge/adi,ltc2959.yaml @@ -0,0 +1,26 @@ +# Copyright (c) 2025 Nathan Winslow +# SPDX-License-Identifier: Apache-2.0 + +description: | + Analog Devices LTC2959 ultra-low-power battery fuel gauge. + +compatible: "adi,ltc2959" + +include: [i2c-device.yaml, fuel-gauge.yaml] +include: base.yaml + +properties: + reg: + required: true + + label: + required: true + + alert-gpios: + required: false + type: phandle-array + description: | + Optional GPIO used for ALERT output. The LTC2959 pulls this pin low + when thresholds are crossed or charge events occur. Typically wired + to a host interrupt-capable GPIO pin. +