drivers: fuelgauge: Added test for ADI LTC2959
Adds test and native sim support for ADI LTC2959 fuel gauge.
Signed-off-by: Nathan Winslow <natelostintimeandspace@gmail.com>
diff --git a/tests/drivers/fuel_gauge/ltc2959/CMakeLists.txt b/tests/drivers/fuel_gauge/ltc2959/CMakeLists.txt
new file mode 100644
index 0000000..afc8757
--- /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 0000000..022a71d
--- /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 0000000..3a3511e
--- /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 0000000..824ffaf
--- /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 0000000..49f9aed
--- /dev/null
+++ b/tests/drivers/fuel_gauge/ltc2959/src/test_ltc2959.c
@@ -0,0 +1,287 @@
+/*
+ * Copyright (c) 2025 Nathan Winslow <natelostintimeandspace@gmail.com>
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+#include <zephyr/ztest.h>
+#include <zephyr/device.h>
+#include <zephyr/drivers/fuel_gauge.h>
+#include <zephyr/logging/log.h>
+#include <zephyr/kernel.h>
+#include <zephyr/sys/byteorder.h>
+
+#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 0000000..1f359bb
--- /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