diff --git a/drivers/fuel_gauge/CMakeLists.txt b/drivers/fuel_gauge/CMakeLists.txt index afd417040cf89..fa1a0f12bcbfe 100644 --- a/drivers/fuel_gauge/CMakeLists.txt +++ b/drivers/fuel_gauge/CMakeLists.txt @@ -12,6 +12,7 @@ add_subdirectory_ifdef(CONFIG_FUEL_GAUGE_AXP2101 axp2101) add_subdirectory_ifdef(CONFIG_LC709203F lc709203f) add_subdirectory_ifdef(CONFIG_SY24561 sy24561) add_subdirectory_ifdef(CONFIG_BQ40Z50 bq40z50) +add_subdirectory_ifdef(CONFIG_FUEL_GAUGE_LTC2959 ltc2959) zephyr_library_sources_ifdef(CONFIG_USERSPACE fuel_gauge_syscall_handlers.c) diff --git a/drivers/fuel_gauge/Kconfig b/drivers/fuel_gauge/Kconfig index c7168036e3eac..cb4ca429c863f 100644 --- a/drivers/fuel_gauge/Kconfig +++ b/drivers/fuel_gauge/Kconfig @@ -27,5 +27,6 @@ source "drivers/fuel_gauge/composite/Kconfig" source "drivers/fuel_gauge/axp2101/Kconfig" source "drivers/fuel_gauge/lc709203f/Kconfig" source "drivers/fuel_gauge/sy24561/Kconfig" +source "drivers/fuel_gauge/ltc2959/Kconfig" endif # FUEL_GAUGE diff --git a/drivers/fuel_gauge/ltc2959/CMakeLists.txt b/drivers/fuel_gauge/ltc2959/CMakeLists.txt new file mode 100644 index 0000000000000..3c072d684ab17 --- /dev/null +++ b/drivers/fuel_gauge/ltc2959/CMakeLists.txt @@ -0,0 +1,4 @@ +zephyr_library_sources(ltc2959.c) + +zephyr_include_directories_ifdef(CONFIG_EMUL_LTC2959 .) +zephyr_library_sources_ifdef(CONFIG_EMUL_LTC2959 ./emul_ltc2959.c) diff --git a/drivers/fuel_gauge/ltc2959/Kconfig b/drivers/fuel_gauge/ltc2959/Kconfig new file mode 100644 index 0000000000000..070b809c85cca --- /dev/null +++ b/drivers/fuel_gauge/ltc2959/Kconfig @@ -0,0 +1,20 @@ +# Copyright (c) 2025 Nathan Winslow +# +# SPDX-License-Identifier: Apache-2.0 + +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 EMUL_LTC2959 + bool "Emulate an LTC2959 fuel gauge" + default y + depends on EMUL + depends on FUEL_GAUGE_LTC2959 + help + It provides readings which follow a simple sequence, thus allowing + test code to check that things are working as expected. diff --git a/drivers/fuel_gauge/ltc2959/emul_ltc2959.c b/drivers/fuel_gauge/ltc2959/emul_ltc2959.c new file mode 100644 index 0000000000000..fe40019cc8142 --- /dev/null +++ b/drivers/fuel_gauge/ltc2959/emul_ltc2959.c @@ -0,0 +1,255 @@ +/** + * Copyright (c) 2025 Nathan Winslow + * SPDX-License-Identifier: Apache-2.0 + * + * Emulator for ltc2959 fuel gauge + */ + +#include +#include +#include +#include +#include +#include +#include + +#define DT_DRV_COMPAT adi_ltc2959 + +LOG_MODULE_REGISTER(EMUL_LTC2959); + +#include "ltc2959.h" + +struct ltc2959_emul_data { + uint8_t regs[LTC2959_REG_GPIO_THRESH_LOW_LSB + 1]; /* enough for all regs */ +}; + +struct ltc2959_emul_cfg { + /* I2C Address of emulator */ + uint16_t addr; +}; + +static int ltc2959_emul_reset(const struct emul *target) +{ + struct ltc2959_emul_data *data = (struct ltc2959_emul_data *)target->data; + + memset(data->regs, 0, sizeof(data->regs)); + + /* Values according to pgs 10-11 of the LTC2959 datasheet */ + data->regs[LTC2959_REG_STATUS] = 0x01; + data->regs[LTC2959_REG_ADC_CONTROL] = 0x18; + data->regs[LTC2959_REG_CC_CONTROL] = 0x50; + data->regs[LTC2959_REG_ACC_CHARGE_3] = 0x80; + data->regs[LTC2959_REG_CHG_THRESH_HIGH_3] = 0xFF; + data->regs[LTC2959_REG_CHG_THRESH_HIGH_2] = 0xFF; + data->regs[LTC2959_REG_CHG_THRESH_HIGH_1] = 0xFF; + data->regs[LTC2959_REG_CHG_THRESH_HIGH_0] = 0xFF; + data->regs[LTC2959_REG_VOLT_THRESH_HIGH_MSB] = 0xFF; + data->regs[LTC2959_REG_VOLT_THRESH_HIGH_LSB] = 0xFF; + data->regs[LTC2959_REG_CURR_THRESH_HIGH_MSB] = 0x7F; + data->regs[LTC2959_REG_CURR_THRESH_HIGH_LSB] = 0xFF; + data->regs[LTC2959_REG_CURR_THRESH_LOW_MSB] = 0x80; + data->regs[LTC2959_REG_MAX_CURRENT_MSB] = 0x80; + data->regs[LTC2959_REG_MIN_CURRENT_MSB] = 0x7F; + data->regs[LTC2959_REG_MIN_CURRENT_LSB] = 0xFF; + data->regs[LTC2959_REG_TEMP_THRESH_HIGH_MSB] = 0xFF; + data->regs[LTC2959_REG_TEMP_THRESH_HIGH_LSB] = 0xFF; + data->regs[LTC2959_REG_GPIO_THRESH_HIGH_MSB] = 0x7F; + data->regs[LTC2959_REG_GPIO_THRESH_HIGH_LSB] = 0xFF; + data->regs[LTC2959_REG_GPIO_THRESH_LOW_MSB] = 0x80; + + return 0; +} + +static int emul_ltc2959_reg_write(const struct emul *target, int reg, int val) +{ + struct ltc2959_emul_data *data = target->data; + + switch (reg) { + case LTC2959_REG_ADC_CONTROL: + case LTC2959_REG_CC_CONTROL: + case LTC2959_REG_ACC_CHARGE_3: + case LTC2959_REG_ACC_CHARGE_2: + case LTC2959_REG_ACC_CHARGE_1: + case LTC2959_REG_ACC_CHARGE_0: + case LTC2959_REG_CHG_THRESH_LOW_3: + case LTC2959_REG_CHG_THRESH_LOW_2: + case LTC2959_REG_CHG_THRESH_LOW_1: + case LTC2959_REG_CHG_THRESH_LOW_0: + case LTC2959_REG_CHG_THRESH_HIGH_3: + case LTC2959_REG_CHG_THRESH_HIGH_2: + case LTC2959_REG_CHG_THRESH_HIGH_1: + case LTC2959_REG_CHG_THRESH_HIGH_0: + case LTC2959_REG_VOLT_THRESH_HIGH_MSB: + case LTC2959_REG_VOLT_THRESH_HIGH_LSB: + case LTC2959_REG_VOLT_THRESH_LOW_MSB: + case LTC2959_REG_VOLT_THRESH_LOW_LSB: + case LTC2959_REG_MAX_VOLTAGE_MSB: + case LTC2959_REG_MAX_VOLTAGE_LSB: + case LTC2959_REG_MIN_VOLTAGE_MSB: + case LTC2959_REG_MIN_VOLTAGE_LSB: + case LTC2959_REG_CURR_THRESH_HIGH_MSB: + case LTC2959_REG_CURR_THRESH_HIGH_LSB: + case LTC2959_REG_CURR_THRESH_LOW_MSB: + case LTC2959_REG_CURR_THRESH_LOW_LSB: + case LTC2959_REG_MAX_CURRENT_MSB: + case LTC2959_REG_MAX_CURRENT_LSB: + case LTC2959_REG_MIN_CURRENT_MSB: + case LTC2959_REG_MIN_CURRENT_LSB: + case LTC2959_REG_TEMP_THRESH_HIGH_MSB: + case LTC2959_REG_TEMP_THRESH_HIGH_LSB: + case LTC2959_REG_TEMP_THRESH_LOW_MSB: + case LTC2959_REG_TEMP_THRESH_LOW_LSB: + case LTC2959_REG_GPIO_THRESH_HIGH_MSB: + case LTC2959_REG_GPIO_THRESH_HIGH_LSB: + case LTC2959_REG_GPIO_THRESH_LOW_MSB: + case LTC2959_REG_GPIO_THRESH_LOW_LSB: + data->regs[reg] = val; + break; + + case LTC2959_REG_STATUS: + case LTC2959_REG_VOLTAGE_MSB: + case LTC2959_REG_VOLTAGE_LSB: + case LTC2959_REG_CURRENT_MSB: + case LTC2959_REG_CURRENT_LSB: + case LTC2959_REG_TEMP_MSB: + case LTC2959_REG_TEMP_LSB: + case LTC2959_REG_GPIO_VOLTAGE_MSB: + case LTC2959_REG_GPIO_VOLTAGE_LSB: + default: + LOG_ERR("Unknown or Read Only Register: 0x%x", reg); + return -EIO; + } + return 0; +} + +static int emul_ltc2959_reg_read(const struct emul *target, int reg, int *val) +{ + if (reg < LTC2959_REG_STATUS || reg > LTC2959_REG_GPIO_THRESH_LOW_LSB) { + LOG_ERR("Unknown Register: 0x%x", reg); + return -EIO; + } + + struct ltc2959_emul_data *data = target->data; + *val = data->regs[reg]; + + return 0; +} + +static int ltc2959_emul_transfer_i2c(const struct emul *target, struct i2c_msg *msgs, int num_msgs, + int addr) +{ + __ASSERT_NO_MSG(msgs && num_msgs); + i2c_dump_msgs_rw(target->dev, msgs, num_msgs, addr, false); + + switch (num_msgs) { + case 1: { + /* Single write: [reg, data0, data1, ...] */ + struct i2c_msg *m = &msgs[0]; + + if (m->flags & I2C_MSG_READ) { + LOG_ERR("Unexpected single-message read"); + return -EIO; + } + if (m->len < 2) { + LOG_ERR("Single-message write must be reg+data (len=%d)", m->len); + return -EIO; + } + uint8_t reg = m->buf[0]; + + for (size_t i = 1; i < m->len; i++, reg++) { + int ret = emul_ltc2959_reg_write(target, reg, m->buf[i]); + + if (ret < 0) { + return ret; + } + } + return 0; + } + + case 2: { + /* Two-message: [reg], then [read N] OR [write N] */ + struct i2c_msg *m0 = &msgs[0]; + struct i2c_msg *m1 = &msgs[1]; + + if ((m0->flags & I2C_MSG_READ) || m0->len != 1) { + LOG_ERR("Invalid first msg (flags=0x%x len=%d)", m0->flags, m0->len); + return -EIO; + } + + uint8_t reg = m0->buf[0]; + + if (m1->flags & I2C_MSG_READ) { + /* Burst READ: stream N bytes starting at reg */ + for (size_t i = 0; i < m1->len; i++) { + int val; + int ret = emul_ltc2959_reg_read(target, reg + i, &val); + + if (ret < 0) { + return ret; + } + + m1->buf[i] = (uint8_t)val; + } + return 0; + } + /* Burst WRITE: stream N bytes into reg..reg+N-1 */ + if (!m1->len) { + LOG_ERR("Empty write"); + return -EIO; + } + for (size_t i = 0; i < m1->len; i++) { + int ret = emul_ltc2959_reg_write(target, reg + i, m1->buf[i]); + + if (ret < 0) { + return ret; + } + } + return 0; + } + + default: + LOG_ERR("Unsupported number of I2C messages: %d", num_msgs); + return -EIO; + } +} + +/* The I2C emulator API required by Zephyr. */ +static const struct i2c_emul_api ltc2959_emul_api_i2c = { + .transfer = ltc2959_emul_transfer_i2c, +}; + +#ifdef CONFIG_ZTEST +#include + +/* Add test reset handlers in when using emulators with tests */ +#define LTC2959_EMUL_RESET_RULE_BEFORE(inst) ltc2959_emul_reset(EMUL_DT_GET(DT_DRV_INST(inst))); + +static void ltc2959_gauge_reset_rule_after(const struct ztest_unit_test *test, void *data) +{ + ARG_UNUSED(test); + ARG_UNUSED(data); + + DT_INST_FOREACH_STATUS_OKAY(LTC2959_EMUL_RESET_RULE_BEFORE) +} +ZTEST_RULE(ltc2959_gauge_reset, NULL, ltc2959_gauge_reset_rule_after); +#endif /* CONFIG_ZTEST */ + +static int ltc2959_emul_init(const struct emul *target, const struct device *parent) +{ + ARG_UNUSED(parent); + ltc2959_emul_reset(target); + return 0; +} + +/* + * Main instantiation macro. + */ +#define DEFINE_LTC2959_EMUL(n) \ + static struct ltc2959_emul_data ltc2959_emul_data_##n; \ + static const struct ltc2959_emul_cfg ltc2959_emul_cfg_##n = { \ + .addr = DT_INST_REG_ADDR(n), \ + }; \ + EMUL_DT_INST_DEFINE(n, ltc2959_emul_init, <c2959_emul_data_##n, <c2959_emul_cfg_##n, \ + <c2959_emul_api_i2c, NULL) + +DT_INST_FOREACH_STATUS_OKAY(DEFINE_LTC2959_EMUL); diff --git a/drivers/fuel_gauge/ltc2959/ltc2959.c b/drivers/fuel_gauge/ltc2959/ltc2959.c new file mode 100644 index 0000000000000..6632c930e0225 --- /dev/null +++ b/drivers/fuel_gauge/ltc2959/ltc2959.c @@ -0,0 +1,683 @@ +/* + * Copyright (c) 2025, Nathan Winslow + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include +#include +#include "ltc2959.h" + +#define DT_DRV_COMPAT adi_ltc2959 + +LOG_MODULE_REGISTER(LTC2959, CONFIG_FUEL_GAUGE_LOG_LEVEL); + +#define LTC2959_TEMP_K_SF (8250) +#define LTC2959_VOLT_UV_SF (955) /* µV per full-scale (16-bit) */ +#define LTC2959_GPIO_BIPOLAR_UV_SF (97500) +#define LTC2959_GPIO_UNIPOLAR_UV_SF (1560000) +#define LTC2959_VOLT_THRESH_UV_SCALAR (62600000) + +/* 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) + +#define LTC2959_CC_WRITABLE_MASK (BIT(7) | BIT(6) | BIT(3)) /* 0xC8 */ +#define LTC2959_CC_RESERVED_FIXED BIT(4) + +/* Used when ACR is controlled via firmware */ +#define LTC2959_ACR_CLR (0xFFFFFFFF) +/* ACR base (50 mΩ) LSB: 533 nAh = 0.533 µAh */ +#define LTC2959_ACR_UAH_NUM (533u) /* numerator (µAh) */ +#define LTC2959_ACR_UAH_DEN (1000u) /* denominator (—) */ +#define LTC2959_ACR_RSENSE_REF_MOHM (50u) + +/* Voltage source selection (bit 2 of Control Register) */ +#define LTC2959_VIN_VDD (0x0 << 2) +#define LTC2959_VIN_SENSEN (0x1 << 2) + +/* STATUS Register Bit Definitions (0x00) */ +enum ltc2959_status_flags { + LTC2959_STATUS_GPIO_ALERT = BIT(7), /* Default: 0 */ + LTC2959_STATUS_CURRENT_ALERT = BIT(6), /* Default: 0 */ + LTC2959_STATUS_CHARGE_OVER_UNDER = BIT(5), /* Default: 0 */ + LTC2959_STATUS_TEMP_ALERT = BIT(4), /* Default: 0 */ + LTC2959_STATUS_CHARGE_HIGH = BIT(3), /* Default: 0 */ + LTC2959_STATUS_CHARGE_LOW = BIT(2), /* Default: 0 */ + LTC2959_STATUS_VOLTAGE_ALERT = BIT(1), /* Default: 0 */ + LTC2959_STATUS_UVLO = BIT(0) /* Default: 1 */ +}; + +/* ADC mode values (bits 7:5 of CONTROL register 0x01) */ +enum ltc2959_adc_modes { + LTC2959_ADC_MODE_SLEEP = 0x00, + LTC2959_ADC_MODE_SMART_SLEEP = 0x20, + LTC2959_ADC_MODE_CONT_V = 0x40, + LTC2959_ADC_MODE_CONT_I = 0x60, + LTC2959_ADC_MODE_CONT_VI = 0x80, + LTC2959_ADC_MODE_SINGLE_SHOT = 0xA0, + LTC2959_ADC_MODE_CONT_VIT = 0xC0 /* recommended for full telemetry */ +}; + +/* GPIO mode bits (bits 4:3 of CONTROL register 0x01) */ +enum ltc2959_gpio_modes { + LTC2959_GPIO_MODE_ALERT = 0x00, + LTC2959_GPIO_MODE_CHGCOMP = 0x08, + LTC2959_GPIO_MODE_BIPOLAR = 0x10, + LTC2959_GPIO_MODE_UNIPOLAR = 0x18, +}; + +/* CC Control bits (CC register 0x02)*/ +enum ltc2959_cc_options { + LTC2959_CC_DEADBAND_0UV = (0b00 << 6), + LTC2959_CC_DEADBAND_20UV = (0b01 << 6), + LTC2959_CC_DEADBAND_40UV = (0b10 << 6), + LTC2959_CC_DEADBAND_80UV = (0b11 << 6), + LTC2959_CC_DO_NOT_COUNT = BIT(3), +}; + +struct ltc2959_config { + struct i2c_dt_spec i2c; + int32_t current_lsb_ua; + uint32_t rsense_milliohms; +}; + +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; + uint8_t ctrl; + int ret; + + if ((mode & ~(LTC2959_CTRL_ADC_MODE_MASK | LTC2959_CTRL_GPIO_MODE_MASK | + LTC2959_CTRL_VIN_SEL_BIT)) != 0U) { + return -EINVAL; + } + + ret = i2c_reg_read_byte_dt(&cfg->i2c, LTC2959_REG_ADC_CONTROL, &ctrl); + + if (ret < 0) { + return ret; + } + + ctrl &= ~(LTC2959_CTRL_ADC_MODE_MASK | LTC2959_CTRL_GPIO_MODE_MASK | + LTC2959_CTRL_VIN_SEL_BIT); + ctrl |= mode; + + ret = i2c_reg_write_byte_dt(&cfg->i2c, LTC2959_REG_ADC_CONTROL, ctrl); + + if (ret < 0) { + LOG_ERR("Failed to set ADC mode: 0x%02x (ctrl=0x%02x)", mode, ctrl); + 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 value) +{ + const struct ltc2959_config *cfg = dev->config; + uint8_t mask = (value & LTC2959_CC_WRITABLE_MASK) | LTC2959_CC_RESERVED_FIXED; + + LOG_DBG("setting cc to: 0x%02X", mask); + return i2c_reg_write_byte_dt(&cfg->i2c, LTC2959_REG_CC_CONTROL, mask); +} + +static inline uint32_t u64_div_round_closest_u32_sat(uint64_t n, uint32_t d) +{ + /* round-to-nearest: (n + d/2) / d, with saturation to u32 */ + uint64_t q = (n + (uint64_t)(d / 2u)) / d; + + return (q > UINT32_MAX) ? UINT32_MAX : (uint32_t)q; +} + +static inline uint32_t ltc2959_counts_to_uah(uint32_t counts, const struct ltc2959_config *cfg) +{ + /* µAh = counts * 0.533µAh * (50 mΩ / r_sense) */ + uint64_t prod = (uint64_t)counts * (uint64_t)LTC2959_ACR_UAH_NUM * + (uint64_t)LTC2959_ACR_RSENSE_REF_MOHM; + uint32_t den = LTC2959_ACR_UAH_DEN * cfg->rsense_milliohms; + + return u64_div_round_closest_u32_sat(prod, den); +} + +static inline uint32_t ltc2959_uah_to_counts(uint32_t uah, const struct ltc2959_config *cfg) +{ + /* counts = µAh * (r_sense / 50 mΩ) * 1000 / 533 */ + uint64_t prod = + (uint64_t)uah * (uint64_t)LTC2959_ACR_UAH_DEN * (uint64_t)cfg->rsense_milliohms; + uint32_t den = LTC2959_ACR_UAH_NUM * LTC2959_ACR_RSENSE_REF_MOHM; + + return u64_div_round_closest_u32_sat(prod, den); +} + +static int ltc2959_read_acr(const struct device *dev, uint32_t *value) +{ + return ltc2959_read32(dev, LTC2959_REG_ACC_CHARGE_3, value); +} + +static int ltc2959_write_acr(const struct device *dev, uint32_t value) +{ + const struct ltc2959_config *cfg = dev->config; + uint8_t buf[4]; + + sys_put_be32(value, buf); + return i2c_burst_write_dt(&cfg->i2c, LTC2959_REG_ACC_CHARGE_3, buf, sizeof(buf)); +} + +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); + + if (ret < 0) { + return ret; + } + + 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: + *value_uv = ((int64_t)raw_signed * LTC2959_GPIO_BIPOLAR_UV_SF) >> 15; + break; + + case LTC2959_GPIO_MODE_UNIPOLAR: + *value_uv = + (int32_t)(((uint64_t)raw * (uint64_t)LTC2959_GPIO_UNIPOLAR_UV_SF) >> 15); + 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_th; + + ret = ltc2959_read16(dev, reg, &raw_th); + + if (ret < 0) { + return ret; + } + + int16_t raw_signed = (int16_t)raw_th; + uint8_t gpio_mode = ctrl & LTC2959_CTRL_GPIO_MODE_MASK; + + switch (gpio_mode) { + case LTC2959_GPIO_MODE_BIPOLAR: + *value_uv = ((int64_t)raw_signed * LTC2959_GPIO_BIPOLAR_UV_SF) >> 15; + break; + + case LTC2959_GPIO_MODE_UNIPOLAR: + *value_uv = + (int32_t)(((uint64_t)raw_th * (uint64_t)LTC2959_GPIO_UNIPOLAR_UV_SF) >> 15); + 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; + + switch (gpio_mode) { + case LTC2959_GPIO_MODE_BIPOLAR: { + int64_t raw_bp64 = ((int64_t)value_uv * 32768) / LTC2959_GPIO_BIPOLAR_UV_SF; + + if ((raw_bp64 < INT16_MIN) || (raw_bp64 > INT16_MAX)) { + return -ERANGE; + } + + uint16_t raw_bp = (uint16_t)((int16_t)raw_bp64); + uint8_t buf[2]; + + sys_put_be16(raw_bp, buf); + return i2c_burst_write_dt(&cfg->i2c, reg, buf, sizeof(buf)); + } + case LTC2959_GPIO_MODE_UNIPOLAR: { + + if (value_uv < 0) { + return -ERANGE; + } + + int64_t raw_up64 = ((int64_t)value_uv * 32768) / LTC2959_GPIO_UNIPOLAR_UV_SF; + + if ((raw_up64 < 0) || (raw_up64 > UINT16_MAX)) { + return -ERANGE; + } + + uint16_t raw_up = (uint16_t)raw_up64; + uint8_t buf[2]; + + sys_put_be16(raw_up, buf); + return i2c_burst_write_dt(&cfg->i2c, reg, buf, sizeof(buf)); + } + default: + break; + } + + LOG_ERR("Unsupported GPIO mode: 0x%02x", gpio_mode); + return -ENOTSUP; +} + +static int ltc2959_get_voltage_threshold_uv(const struct device *dev, bool high, uint32_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("Failed to get voltage threshold: %i", ret); + return ret; + } + + *value = ((uint64_t)raw * LTC2959_VOLT_THRESH_UV_SCALAR) >> 15; + + return 0; +} + +static int ltc2959_set_voltage_threshold_uv(const struct device *dev, bool high, uint32_t value) +{ + uint8_t reg = high ? LTC2959_REG_VOLT_THRESH_HIGH_MSB : LTC2959_REG_VOLT_THRESH_LOW_MSB; + uint64_t raw64 = ((uint64_t)value << 15) / LTC2959_VOLT_THRESH_UV_SCALAR; + + if (raw64 > UINT16_MAX) { + return -ERANGE; + } + + uint16_t raw = (uint16_t)raw64; + + uint8_t buf[2]; + + sys_put_be16(raw, buf); + 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, int32_t *value_ua) +{ + uint8_t reg = high ? LTC2959_REG_CURR_THRESH_HIGH_MSB : LTC2959_REG_CURR_THRESH_LOW_MSB; + + uint16_t raw_cth; + int ret = ltc2959_read16(dev, reg, &raw_cth); + + if (ret < 0) { + return ret; + } + + const struct ltc2959_config *cfg = dev->config; + int16_t signed_raw = (int16_t)raw_cth; + *value_ua = signed_raw * cfg->current_lsb_ua; + + return 0; +} + +static int ltc2959_set_current_threshold_ua(const struct device *dev, bool high, int32_t value_ua) +{ + uint8_t reg = high ? LTC2959_REG_CURR_THRESH_HIGH_MSB : LTC2959_REG_CURR_THRESH_LOW_MSB; + const struct ltc2959_config *cfg = dev->config; + + if (!cfg->current_lsb_ua) { + return -ERANGE; + } + + int32_t raw32 = value_ua / cfg->current_lsb_ua; + + /* To account for cases where current thresholds are +-2A */ + int16_t raw16 = CLAMP(raw32, INT16_MIN, INT16_MAX); + uint8_t buf[2]; + + sys_put_be16(raw16, buf); + 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_tth; + int ret = ltc2959_read16(dev, reg, &raw_tth); + + if (ret < 0) { + return ret; + } + + *value_dK = ((uint32_t)raw_tth * 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; + uint64_t raw64 = ((uint64_t)value_dK << 16) / LTC2959_TEMP_K_SF; + + if (raw64 > UINT16_MAX) { + return -ERANGE; + } + + uint16_t raw = (uint16_t)raw64; + uint8_t buf[2]; + + sys_put_be16(raw, buf); + 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; + + switch (prop) { + case FUEL_GAUGE_STATUS: + uint8_t raw_st; + + ret = i2c_reg_read_byte_dt(&cfg->i2c, LTC2959_REG_STATUS, &raw_st); + + if (ret < 0) { + return ret; + } + + val->fg_status = raw_st; + + break; + + 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_VOLT_UV_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 * cfg->current_lsb_ua; + + break; + + 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; + break; + + case FUEL_GAUGE_REMAINING_CAPACITY: + uint32_t acr; + + ret = ltc2959_read_acr(dev, &acr); + + if (ret < 0) { + return ret; + } + + val->remaining_capacity = ltc2959_counts_to_uah(acr, cfg); + break; + + case FUEL_GAUGE_ADC_MODE: + ret = ltc2959_get_adc_mode(dev, &val->adc_mode); + break; + + case FUEL_GAUGE_HIGH_VOLTAGE_ALARM: + ret = ltc2959_get_voltage_threshold_uv(dev, true, &val->high_voltage_alarm); + break; + + case FUEL_GAUGE_LOW_VOLTAGE_ALARM: + ret = ltc2959_get_voltage_threshold_uv(dev, false, &val->low_voltage_alarm); + break; + + case FUEL_GAUGE_HIGH_CURRENT_ALARM: + ret = ltc2959_get_current_threshold_ua(dev, true, &val->high_current_alarm); + break; + + case FUEL_GAUGE_LOW_CURRENT_ALARM: + ret = ltc2959_get_current_threshold_ua(dev, false, &val->low_current_alarm); + break; + + case FUEL_GAUGE_HIGH_TEMPERATURE_ALARM: + ret = ltc2959_get_temp_threshold_dK(dev, true, &val->high_temperature_alarm); + break; + + case FUEL_GAUGE_LOW_TEMPERATURE_ALARM: + ret = ltc2959_get_temp_threshold_dK(dev, false, &val->low_temperature_alarm); + break; + + case FUEL_GAUGE_GPIO_VOLTAGE: + ret = ltc2959_get_gpio_voltage_uv(dev, &val->gpio_voltage); + break; + + case FUEL_GAUGE_HIGH_GPIO_ALARM: + ret = ltc2959_get_gpio_threshold_uv(dev, true, &val->high_gpio_alarm); + break; + + case FUEL_GAUGE_LOW_GPIO_ALARM: + ret = ltc2959_get_gpio_threshold_uv(dev, false, &val->low_gpio_alarm); + break; + + case FUEL_GAUGE_CC_CONFIG: + ret = ltc2959_get_cc_config(dev, &val->cc_config); + break; + + default: + return -ENOTSUP; + } + return ret; +} + +static int ltc2959_set_prop(const struct device *dev, fuel_gauge_prop_t prop, + union fuel_gauge_prop_val val) +{ + int ret = 0; + const struct ltc2959_config *cfg = dev->config; + + switch (prop) { + case FUEL_GAUGE_ADC_MODE: + ret = ltc2959_set_adc_mode(dev, val.adc_mode); + break; + + case FUEL_GAUGE_LOW_VOLTAGE_ALARM: + ret = ltc2959_set_voltage_threshold_uv(dev, false, val.low_voltage_alarm); + break; + + case FUEL_GAUGE_HIGH_VOLTAGE_ALARM: + ret = ltc2959_set_voltage_threshold_uv(dev, true, val.high_voltage_alarm); + break; + + case FUEL_GAUGE_LOW_CURRENT_ALARM: + ret = ltc2959_set_current_threshold_ua(dev, false, val.low_current_alarm); + break; + + case FUEL_GAUGE_HIGH_CURRENT_ALARM: + ret = ltc2959_set_current_threshold_ua(dev, true, val.high_current_alarm); + break; + + case FUEL_GAUGE_LOW_TEMPERATURE_ALARM: + ret = ltc2959_set_temp_threshold_dK(dev, false, val.low_temperature_alarm); + break; + + case FUEL_GAUGE_HIGH_TEMPERATURE_ALARM: + ret = ltc2959_set_temp_threshold_dK(dev, true, val.high_temperature_alarm); + break; + + case FUEL_GAUGE_LOW_GPIO_ALARM: + ret = ltc2959_set_gpio_threshold_uv(dev, false, val.low_gpio_alarm); + break; + + case FUEL_GAUGE_HIGH_GPIO_ALARM: + ret = ltc2959_set_gpio_threshold_uv(dev, true, val.high_gpio_alarm); + break; + + case FUEL_GAUGE_CC_CONFIG: + LOG_DBG("config stats: 0x%02X", val.cc_config); + ret = ltc2959_set_cc_config(dev, val.cc_config); + break; + + case FUEL_GAUGE_REMAINING_CAPACITY: + uint32_t counts = ltc2959_uah_to_counts(val.remaining_capacity, cfg); + + if (counts == LTC2959_ACR_CLR) { + counts = LTC2959_ACR_CLR - 1; + } + ret = ltc2959_write_acr(dev, counts); + 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) \ + BUILD_ASSERT(DT_NODE_HAS_PROP(DT_DRV_INST(inst), rsense_milliohms)); \ + BUILD_ASSERT(DT_INST_PROP(inst, rsense_milliohms) > 0); \ + static const struct ltc2959_config ltc2959_config_##inst = { \ + .i2c = I2C_DT_SPEC_INST_GET(inst), \ + .current_lsb_ua = (97500000 / (DT_INST_PROP(inst, rsense_milliohms) * 32768)), \ + .rsense_milliohms = DT_INST_PROP(inst, rsense_milliohms), \ + }; \ + 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 0000000000000..09b78159d2e90 --- /dev/null +++ b/drivers/fuel_gauge/ltc2959/ltc2959.h @@ -0,0 +1,76 @@ +/* + * 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 + +enum ltc2959_regs { + /* Status and Control */ + LTC2959_REG_STATUS = 0x00, + LTC2959_REG_ADC_CONTROL = 0x01, + LTC2959_REG_CC_CONTROL = 0x02, + + /* Accumulated Charge (uint32_t) */ + LTC2959_REG_ACC_CHARGE_3 = 0x03, + LTC2959_REG_ACC_CHARGE_2 = 0x04, + LTC2959_REG_ACC_CHARGE_1 = 0x05, + LTC2959_REG_ACC_CHARGE_0 = 0x06, + + /* Charge Thresholds (Low and High, uint32_t each) */ + LTC2959_REG_CHG_THRESH_LOW_3 = 0x07, + LTC2959_REG_CHG_THRESH_LOW_2 = 0x08, + LTC2959_REG_CHG_THRESH_LOW_1 = 0x09, + LTC2959_REG_CHG_THRESH_LOW_0 = 0x0A, + LTC2959_REG_CHG_THRESH_HIGH_3 = 0x0B, + LTC2959_REG_CHG_THRESH_HIGH_2 = 0x0C, + LTC2959_REG_CHG_THRESH_HIGH_1 = 0x0D, + LTC2959_REG_CHG_THRESH_HIGH_0 = 0x0E, + + /* Voltage (uint16_t) */ + LTC2959_REG_VOLTAGE_MSB = 0x0F, + LTC2959_REG_VOLTAGE_LSB = 0x10, + LTC2959_REG_VOLT_THRESH_HIGH_MSB = 0x11, + LTC2959_REG_VOLT_THRESH_HIGH_LSB = 0x12, + LTC2959_REG_VOLT_THRESH_LOW_MSB = 0x13, + LTC2959_REG_VOLT_THRESH_LOW_LSB = 0x14, + LTC2959_REG_MAX_VOLTAGE_MSB = 0x15, + LTC2959_REG_MAX_VOLTAGE_LSB = 0x16, + LTC2959_REG_MIN_VOLTAGE_MSB = 0x17, + LTC2959_REG_MIN_VOLTAGE_LSB = 0x18, + + /* Current (int16_t) */ + LTC2959_REG_CURRENT_MSB = 0x19, + LTC2959_REG_CURRENT_LSB = 0x1A, + LTC2959_REG_CURR_THRESH_HIGH_MSB = 0x1B, + LTC2959_REG_CURR_THRESH_HIGH_LSB = 0x1C, + LTC2959_REG_CURR_THRESH_LOW_MSB = 0x1D, + LTC2959_REG_CURR_THRESH_LOW_LSB = 0x1E, + LTC2959_REG_MAX_CURRENT_MSB = 0x1F, + LTC2959_REG_MAX_CURRENT_LSB = 0x20, + LTC2959_REG_MIN_CURRENT_MSB = 0x21, + LTC2959_REG_MIN_CURRENT_LSB = 0x22, + + /* Temperature (uint16_t) */ + LTC2959_REG_TEMP_MSB = 0x23, + LTC2959_REG_TEMP_LSB = 0x24, + LTC2959_REG_TEMP_THRESH_HIGH_MSB = 0x25, + LTC2959_REG_TEMP_THRESH_HIGH_LSB = 0x26, + LTC2959_REG_TEMP_THRESH_LOW_MSB = 0x27, + LTC2959_REG_TEMP_THRESH_LOW_LSB = 0x28, + + /* GPIO */ + LTC2959_REG_GPIO_VOLTAGE_MSB = 0x29, + LTC2959_REG_GPIO_VOLTAGE_LSB = 0x2A, + LTC2959_REG_GPIO_THRESH_HIGH_MSB = 0x2B, + LTC2959_REG_GPIO_THRESH_HIGH_LSB = 0x2C, + LTC2959_REG_GPIO_THRESH_LOW_MSB = 0x2D, + LTC2959_REG_GPIO_THRESH_LOW_LSB = 0x2E, +}; + +#endif /* END ZEPHYR_DRIVERS_FUELGAUGE_LTC2959_GAUGE_H_ */ diff --git a/dts/bindings/fuel-gauge/adi,ltc2959.yaml b/dts/bindings/fuel-gauge/adi,ltc2959.yaml new file mode 100644 index 0000000000000..61f3e3cc80848 --- /dev/null +++ b/dts/bindings/fuel-gauge/adi,ltc2959.yaml @@ -0,0 +1,20 @@ +# 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: [base.yaml, i2c-device.yaml, fuel-gauge.yaml] + +properties: + reg: + required: true + + rsense-milliohms: + type: int + required: true + description: | + Sensor resistor value in milliohms. Connected to SENSEP or SENSEN on the IC. + NOTE: The datasheet uses a default value of 50 milliOhm diff --git a/include/zephyr/drivers/fuel_gauge.h b/include/zephyr/drivers/fuel_gauge.h index 9dee971c4df06..fb95d2a116b10 100644 --- a/include/zephyr/drivers/fuel_gauge.h +++ b/include/zephyr/drivers/fuel_gauge.h @@ -111,6 +111,26 @@ enum fuel_gauge_prop_type { FUEL_GAUGE_STATE_OF_CHARGE_ALARM, /** Low Cell Voltage Alarm (uV)*/ FUEL_GAUGE_LOW_VOLTAGE_ALARM, + /** High Cell Voltage Alarm (uV)*/ + FUEL_GAUGE_HIGH_VOLTAGE_ALARM, + /** Low Cell Current Alarm (uA)*/ + FUEL_GAUGE_LOW_CURRENT_ALARM, + /** High Cell Current Alarm (uA)*/ + FUEL_GAUGE_HIGH_CURRENT_ALARM, + /** Low Cell Temperature Alarm (dK)*/ + FUEL_GAUGE_LOW_TEMPERATURE_ALARM, + /** High Cell Temperature Alarm (dK)*/ + FUEL_GAUGE_HIGH_TEMPERATURE_ALARM, + /** Low GPIO Voltage Alarm (uV)*/ + FUEL_GAUGE_LOW_GPIO_ALARM, + /** High GPIO Voltage Alarm (uV)*/ + FUEL_GAUGE_HIGH_GPIO_ALARM, + /** GPIO Voltage (uV)*/ + FUEL_GAUGE_GPIO_VOLTAGE, + /** ADC Mode (flags) */ + FUEL_GAUGE_ADC_MODE, + /** Coulomb Counter Config (flags)*/ + FUEL_GAUGE_CC_CONFIG, /** Reserved to demark end of common fuel gauge properties */ FUEL_GAUGE_COMMON_COUNT, @@ -195,6 +215,26 @@ union fuel_gauge_prop_val { uint8_t state_of_charge_alarm; /** FUEL_GAUGE_LOW_VOLTAGE_ALARM */ uint32_t low_voltage_alarm; + /** FUEL_GAUGE_HIGH_VOLTAGE_ALARM */ + uint32_t high_voltage_alarm; + /** FUEL_GAUGE_LOW_CURRENT_ALARM */ + int32_t low_current_alarm; + /** FUEL_GAUGE_HIGH_CURRENT_ALARM */ + int32_t high_current_alarm; + /** FUEL_GAUGE_LOW_TEMPERATURE_ALARM */ + uint16_t low_temperature_alarm; + /** FUEL_GAUGE_HIGH_TEMPERATURE_ALARM */ + uint16_t high_temperature_alarm; + /** FUEL_GAUGE_GPIO_VOLTAGE*/ + int32_t gpio_voltage; + /** FUEL_GAUGE_LOW_GPIO_ALARM */ + int32_t low_gpio_alarm; + /** FUEL_GAUGE_HIGH_GPIO_ALARM */ + int32_t high_gpio_alarm; + /** FUEL_GAUGE_ADC_MODE */ + uint8_t adc_mode; + /** FUEL_GAUGE_CC_CONFIG */ + uint8_t cc_config; }; /** diff --git a/tests/drivers/fuel_gauge/ltc2959/CMakeLists.txt b/tests/drivers/fuel_gauge/ltc2959/CMakeLists.txt new file mode 100644 index 0000000000000..afc875718de71 --- /dev/null +++ b/tests/drivers/fuel_gauge/ltc2959/CMakeLists.txt @@ -0,0 +1,8 @@ +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20.0) + +find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) +project(device) + +target_sources(app PRIVATE src/test_ltc2959.c) diff --git a/tests/drivers/fuel_gauge/ltc2959/boards/native_sim.conf b/tests/drivers/fuel_gauge/ltc2959/boards/native_sim.conf new file mode 100644 index 0000000000000..022a71dd0f0a6 --- /dev/null +++ b/tests/drivers/fuel_gauge/ltc2959/boards/native_sim.conf @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: Apache-2.0 + +CONFIG_EMUL=y diff --git a/tests/drivers/fuel_gauge/ltc2959/boards/native_sim.overlay b/tests/drivers/fuel_gauge/ltc2959/boards/native_sim.overlay new file mode 100644 index 0000000000000..3a3511ee5bc74 --- /dev/null +++ b/tests/drivers/fuel_gauge/ltc2959/boards/native_sim.overlay @@ -0,0 +1,13 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * SPDX-FileCopyrightText: Copyright The Zephyr Project Contributors + */ + +&i2c0 { + ltc2959: ltc2959@63 { + compatible = "adi,ltc2959"; + reg = <0x63>; + rsense-milliohms = <50>; + status = "okay"; + }; +}; diff --git a/tests/drivers/fuel_gauge/ltc2959/prj.conf b/tests/drivers/fuel_gauge/ltc2959/prj.conf new file mode 100644 index 0000000000000..824ffaf7142a3 --- /dev/null +++ b/tests/drivers/fuel_gauge/ltc2959/prj.conf @@ -0,0 +1,7 @@ +CONFIG_ZTEST=y +CONFIG_I2C=y +CONFIG_TEST_USERSPACE=y +CONFIG_LOG=y + +CONFIG_FUEL_GAUGE=y +CONFIG_FUEL_GAUGE_LTC2959=y diff --git a/tests/drivers/fuel_gauge/ltc2959/src/test_ltc2959.c b/tests/drivers/fuel_gauge/ltc2959/src/test_ltc2959.c new file mode 100644 index 0000000000000..49f9aed79169b --- /dev/null +++ b/tests/drivers/fuel_gauge/ltc2959/src/test_ltc2959.c @@ -0,0 +1,287 @@ +/* + * Copyright (c) 2025 Nathan Winslow + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include + +#define LTC_NODE DT_COMPAT_GET_ANY_STATUS_OKAY(adi_ltc2959) +BUILD_ASSERT(DT_NODE_EXISTS(LTC_NODE), "No adi,ltc2959 node in DT for tests"); + +#define RSENSE_MOHMS DT_PROP(LTC_NODE, rsense_milliohms) + +/* Integer LSB sizes (keep tests stable) */ +#define CURRENT_LSB_UA (97500000ULL / ((uint64_t)RSENSE_MOHMS * 32768ULL)) +#define VOLTAGE_MAX_UV (UINT16_MAX * 955U) /* ~955 = 62.6V full scale / 65536 */ + +struct ltc2959_fixture { + const struct device *dev; + const struct fuel_gauge_driver_api *api; +}; + +static void *ltc2959_setup(void) +{ + static ZTEST_DMEM struct ltc2959_fixture fixture; + + fixture.dev = DEVICE_DT_GET_ANY(adi_ltc2959); + k_object_access_all_grant(fixture.dev); + + zassume_true(device_is_ready(fixture.dev), "Fuel Gauge not found"); + + return &fixture; +} + +LOG_MODULE_REGISTER(test_ltc2959, LOG_LEVEL_INF); + +ZTEST_F(ltc2959, test_get_props__returns_ok) +{ + fuel_gauge_prop_t props[] = { + FUEL_GAUGE_STATUS, + FUEL_GAUGE_VOLTAGE, + FUEL_GAUGE_CURRENT, + FUEL_GAUGE_TEMPERATURE, + }; + + union fuel_gauge_prop_val vals[ARRAY_SIZE(props)]; + int ret = fuel_gauge_get_props(fixture->dev, props, vals, ARRAY_SIZE(props)); + +#if CONFIG_EMUL + zassert_equal(vals[0].fg_status, 0x01); + zassert_equal(vals[1].voltage, 0x00); + zassert_equal(vals[2].current, 0x00); + zassert_equal(vals[3].temperature, 0x00); +#else + zassert_between_inclusive(vals[0].fg_status, 0, 0xFF); + zassert_between_inclusive(vals[1].voltage, 0, VOLTAGE_MAX_UV); +#endif + zassert_equal(ret, 0, "Getting bad property has a good status."); +} + +ZTEST_F(ltc2959, test_set_get_single_prop) +{ + int ret; + union fuel_gauge_prop_val in = {.low_voltage_alarm = 1200000}; /* 1.2V */ + + ret = fuel_gauge_set_prop(fixture->dev, FUEL_GAUGE_LOW_VOLTAGE_ALARM, in); + zassert_equal(ret, 0, "set low voltage threshold failed"); + + union fuel_gauge_prop_val out; + + ret = fuel_gauge_get_prop(fixture->dev, FUEL_GAUGE_LOW_VOLTAGE_ALARM, &out); + zassert_equal(ret, 0, "get low voltage threshold failed"); + + /* Allow for register quantization: one LSB ≈ 1.91 mV */ + const int32_t lsb_uv = 62600000 / 32768; /* integer ≈ 1910 */ + int32_t diff = (int32_t)out.low_voltage_alarm - (int32_t)in.low_voltage_alarm; + + zassert_true(diff <= lsb_uv && diff >= -lsb_uv, + "Set/get mismatch: in=%d, out=%d, diff=%d > LSB=%d", (int)in.low_voltage_alarm, + (int)out.low_voltage_alarm, (int)(diff), (int)lsb_uv); + + LOG_INF("in=%d, out=%d, diff=%d > LSB=%d", (int)in.low_voltage_alarm, + (int)out.low_voltage_alarm, (int)(diff), (int)lsb_uv); +} + +ZTEST_F(ltc2959, test_current_threshold_roundtrip) +{ + int ret; + union fuel_gauge_prop_val in, out; + int32_t tol = CURRENT_LSB_UA ? (int32_t)CURRENT_LSB_UA : 100; + + in.high_current_alarm = 123456; /* µA */ + ret = fuel_gauge_set_prop(fixture->dev, FUEL_GAUGE_HIGH_CURRENT_ALARM, in); + zassert_equal(ret, 0, "set current high threshold failed (%d)", ret); + + ret = fuel_gauge_get_prop(fixture->dev, FUEL_GAUGE_HIGH_CURRENT_ALARM, &out); + zassert_equal(ret, 0, "get current high threshold failed (%d)", ret); + + int32_t diff = out.high_current_alarm - in.high_current_alarm; + + if (diff < 0) { + diff = -diff; + } + + zassert_true(diff <= tol, "current high threshold mismatch: in=%d out=%d diff=%d tol=%d", + (int)in.high_current_alarm, (int)out.high_current_alarm, (int)diff, (int)tol); + + in.low_current_alarm = -78901; /* µA */ + ret = fuel_gauge_set_prop(fixture->dev, FUEL_GAUGE_LOW_CURRENT_ALARM, in); + zassert_equal(ret, 0, "set current low threshold failed (%d)", ret); + + ret = fuel_gauge_get_prop(fixture->dev, FUEL_GAUGE_LOW_CURRENT_ALARM, &out); + zassert_equal(ret, 0, "get current low threshold failed (%d)", ret); + + diff = out.low_current_alarm - in.low_current_alarm; + + if (diff < 0) { + diff = -diff; + } + + zassert_true(diff <= tol, "current low threshold mismatch: in=%d out=%d diff=%d tol=%d", + (int)in.low_current_alarm, (int)out.low_current_alarm, (int)diff, (int)tol); +} + +ZTEST_F(ltc2959, test_temperature_threshold_roundtrip) +{ + int ret; + union fuel_gauge_prop_val in; + union fuel_gauge_prop_val out; + + in.low_temperature_alarm = 3000; + ret = fuel_gauge_set_prop(fixture->dev, FUEL_GAUGE_LOW_TEMPERATURE_ALARM, in); + zassert_equal(ret, 0, "set temp low threshold failed (%d)", ret); + + ret = fuel_gauge_get_prop(fixture->dev, FUEL_GAUGE_LOW_TEMPERATURE_ALARM, &out); + zassert_equal(ret, 0, "get temp low threshold failed (%d)", ret); + int32_t diff = (int32_t)out.low_temperature_alarm - (int32_t)in.low_temperature_alarm; + + if (diff < 0) { + diff = -diff; + } + + zassert_true(diff <= 1, "temp low threshold mismatch: in=%u out=%u diff=%d", + in.low_temperature_alarm, out.low_temperature_alarm, (int)diff); + + in.high_temperature_alarm = 3500; + ret = fuel_gauge_set_prop(fixture->dev, FUEL_GAUGE_HIGH_TEMPERATURE_ALARM, in); + zassert_equal(ret, 0, "set temp high threshold failed (%d)", ret); + + ret = fuel_gauge_get_prop(fixture->dev, FUEL_GAUGE_HIGH_TEMPERATURE_ALARM, &out); + zassert_equal(ret, 0, "get temp high threshold failed (%d)", ret); + diff = (int32_t)out.high_temperature_alarm - (int32_t)in.high_temperature_alarm; + + if (diff < 0) { + diff = -diff; + } + + zassert_true(diff <= 1, "temp high threshold mismatch: in=%u out=%u diff=%d", + in.high_temperature_alarm, out.high_temperature_alarm, (int)diff); +} + +ZTEST_F(ltc2959, test_adc_mode_roundtrip) +{ + int ret; + union fuel_gauge_prop_val in, out; + + in.adc_mode = 0xC0 | 0x10; /* CONT_VIT + GPIO BIPOLAR */ + ret = fuel_gauge_set_prop(fixture->dev, FUEL_GAUGE_ADC_MODE, in); + zassert_equal(ret, 0, "set ADC_MODE failed (%d)", ret); + + ret = fuel_gauge_get_prop(fixture->dev, FUEL_GAUGE_ADC_MODE, &out); + zassert_equal(ret, 0, "get ADC_MODE failed (%d)", ret); + zassert_equal(out.adc_mode, in.adc_mode, "ADC_MODE mismatch (got 0x%02x)", out.adc_mode); +} + +ZTEST_F(ltc2959, test_remaining_capacity_roundtrip) +{ + int ret; + union fuel_gauge_prop_val in, out; + + in.remaining_capacity = 1234567; /* µAh */ + ret = fuel_gauge_set_prop(fixture->dev, FUEL_GAUGE_REMAINING_CAPACITY, in); + zassert_equal(ret, 0, "set ACR failed (%d)", ret); + + ret = fuel_gauge_get_prop(fixture->dev, FUEL_GAUGE_REMAINING_CAPACITY, &out); + zassert_equal(ret, 0, "get ACR failed (%d)", ret); + + int32_t diff = (int32_t)out.remaining_capacity - (int32_t)in.remaining_capacity; + + if (diff < 0) { + diff = -diff; + } + + zassert_true(diff <= 1, "ACR mismatch: in=%d out=%d diff=%d tol=1", + (int)in.remaining_capacity, (int)out.remaining_capacity, (int)diff); +} + +ZTEST_F(ltc2959, test_remaining_capacity_reserved_guard) +{ + int ret; + union fuel_gauge_prop_val in, out; + + /* 0xFFFFFFFF counts ≈ 2,289,000,000 µAh (533 nAh/LSB) */ + in.remaining_capacity = 2289000000U; + ret = fuel_gauge_set_prop(fixture->dev, FUEL_GAUGE_REMAINING_CAPACITY, in); + zassert_equal(ret, 0, "set ACR near fullscale failed (%d)", ret); + + ret = fuel_gauge_get_prop(fixture->dev, FUEL_GAUGE_REMAINING_CAPACITY, &out); + zassert_equal(ret, 0, "get ACR near fullscale failed (%d)", ret); + + /* We expect the driver to write 0xFFFFFFFE instead, so out <= in and close */ + zassert_true(out.remaining_capacity <= in.remaining_capacity, + "ACR guard failed: got larger than requested"); + int32_t diff = (int32_t)in.remaining_capacity - (int32_t)out.remaining_capacity; + + if (diff < 0) { + diff = -diff; + } + + zassert_true(diff <= 1, "ACR guard too lossy: in=%d out=%d |diff|=%d", + (int)in.remaining_capacity, (int)out.remaining_capacity, (int)diff); +} + +ZTEST_F(ltc2959, test_cc_config_sanitized) +{ + int ret; + union fuel_gauge_prop_val in, out; + + in.cc_config = 0xFF; /* try to set everything */ + ret = fuel_gauge_set_prop(fixture->dev, FUEL_GAUGE_CC_CONFIG, in); + zassert_equal(ret, 0, "set cc_config failed (%d)", ret); + + ret = fuel_gauge_get_prop(fixture->dev, FUEL_GAUGE_CC_CONFIG, &out); + zassert_equal(ret, 0, "get cc_config failed (%d)", ret); + + /* Expect bits 7,6,3 kept; bit 4 forced; others cleared => 0xD8 */ + zassert_equal(out.cc_config, 0xD8, "cc_config not sanitized (got 0x%02X)", out.cc_config); +} + +ZTEST_USER_F(ltc2959, test_get_some_props_failed__returns_bad_status) +{ + fuel_gauge_prop_t props[] = { + /* First invalid property */ + FUEL_GAUGE_PROP_MAX, + /* Second invalid property */ + FUEL_GAUGE_PROP_MAX, + /* Valid property */ + FUEL_GAUGE_VOLTAGE, + }; + union fuel_gauge_prop_val vals[ARRAY_SIZE(props)]; + + int ret = fuel_gauge_get_props(fixture->dev, props, vals, ARRAY_SIZE(props)); + + zassert_equal(ret, -ENOTSUP, "Getting bad property has a good status."); +} + +ZTEST_F(ltc2959, test_set_some_props_failed__returns_err) +{ + fuel_gauge_prop_t prop_types[] = { + /* First invalid property */ + FUEL_GAUGE_PROP_MAX, + /* Second invalid property */ + FUEL_GAUGE_PROP_MAX, + /* Valid property */ + FUEL_GAUGE_LOW_VOLTAGE_ALARM, + }; + + union fuel_gauge_prop_val props[] = { + /* First invalid property */ + {0}, + /* Second invalid property */ + {0}, + /* Valid property */ + {.voltage = 0}, + }; + + int ret = fuel_gauge_set_props(fixture->dev, prop_types, props, ARRAY_SIZE(props)); + + zassert_equal(ret, -ENOTSUP); +} + +ZTEST_SUITE(ltc2959, NULL, ltc2959_setup, NULL, NULL, NULL); diff --git a/tests/drivers/fuel_gauge/ltc2959/testcase.yaml b/tests/drivers/fuel_gauge/ltc2959/testcase.yaml new file mode 100644 index 0000000000000..1f359bb1e893c --- /dev/null +++ b/tests/drivers/fuel_gauge/ltc2959/testcase.yaml @@ -0,0 +1,7 @@ +tests: + drivers.fuel_gauge.ltc2959: + tags: + - fuel_gauge + filter: dt_compat_enabled("adi,ltc2959") + platform_allow: + - native_sim