From: John Erasmus Mari Geronimo <johnerasmusmari.geronimo@analog.com>
To: <sre@kernel.org>
Cc: <robh@kernel.org>, <krzk+dt@kernel.org>, <conor+dt@kernel.org>,
<linux-pm@vger.kernel.org>, <devicetree@vger.kernel.org>,
<linux-kernel@vger.kernel.org>
Subject: [PATCH v3 2/2] power: supply: add LT8491 battery charger driver
Date: Thu, 29 Oct 2026 15:30:12 +0800 [thread overview]
Message-ID: <20261029073012.1498-3-johnerasmusmari.geronimo@analog.com> (raw)
In-Reply-To: <20261029073012.1498-1-johnerasmusmari.geronimo@analog.com>
Add support for the Analog Devices LT8491 buck-boost battery charge
controller connected over I2C. The driver registers a power supply
exposing the charging status, battery voltage, current, power and
temperature, and the battery temperature limits. The PCB resistor
values needed by the chip's telemetry are read from the devicetree and
written to the chip at probe.
Signed-off-by: John Erasmus Mari Geronimo <johnerasmusmari.geronimo@analog.com>
---
MAINTAINERS | 1 +
drivers/power/supply/Kconfig | 9 +
drivers/power/supply/Makefile | 1 +
drivers/power/supply/lt8491_charger.c | 428 ++++++++++++++++++++++++++
4 files changed, 439 insertions(+)
create mode 100644 drivers/power/supply/lt8491_charger.c
diff --git a/MAINTAINERS b/MAINTAINERS
index 2b3cc7649..badfce0b8 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -15471,6 +15471,7 @@ M: John Erasmus Mari Geronimo <johnerasmusmari.geronimo@analog.com>
L: linux-pm@vger.kernel.org
S: Maintained
F: Documentation/devicetree/bindings/power/supply/adi,lt8491.yaml
+F: drivers/power/supply/lt8491_charger.c
LTC1660 DAC DRIVER
M: Marcus Folkesson <marcus.folkesson@gmail.com>
diff --git a/drivers/power/supply/Kconfig b/drivers/power/supply/Kconfig
index ee7dc5f26..2a5a0c823 100644
--- a/drivers/power/supply/Kconfig
+++ b/drivers/power/supply/Kconfig
@@ -598,6 +598,15 @@ config CHARGER_LT3651
Say Y to include support for the Analog Devices (Linear Technology)
LT3651 battery charger which reports its status via GPIO lines.
+config CHARGER_LT8491
+ tristate "Analog Devices LT8491 charger"
+ depends on I2C
+ help
+ Say Y to include support for the Analog Devices (Linear Technology)
+ LT8491 battery charge controller connected to I2C. The LT8491 is a
+ high voltage buck-boost switching regulator battery charger
+ controller.
+
config CHARGER_LTC4162L
tristate "LTC4162-L charger"
depends on I2C
diff --git a/drivers/power/supply/Makefile b/drivers/power/supply/Makefile
index f3c0d1006..7b85b35b5 100644
--- a/drivers/power/supply/Makefile
+++ b/drivers/power/supply/Makefile
@@ -81,6 +81,7 @@ obj-$(CONFIG_CHARGER_LP8788) += lp8788-charger.o
obj-$(CONFIG_CHARGER_GPIO) += gpio-charger.o
obj-$(CONFIG_CHARGER_MANAGER) += charger-manager.o
obj-$(CONFIG_CHARGER_LT3651) += lt3651-charger.o
+obj-$(CONFIG_CHARGER_LT8491) += lt8491_charger.o
obj-$(CONFIG_CHARGER_LTC4162L) += ltc4162-l-charger.o
obj-$(CONFIG_CHARGER_MAX14577) += max14577_charger.o
obj-$(CONFIG_CHARGER_DETECTOR_MAX14656) += max14656_charger_detector.o
diff --git a/drivers/power/supply/lt8491_charger.c b/drivers/power/supply/lt8491_charger.c
new file mode 100644
index 000000000..eab5ea720
--- /dev/null
+++ b/drivers/power/supply/lt8491_charger.c
@@ -0,0 +1,428 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Analog Devices LT8491 Battery Charger
+ *
+ * Copyright 2024 Analog Devices Inc.
+ */
+
+#include <linux/bitfield.h>
+#include <linux/device.h>
+#include <linux/errno.h>
+#include <linux/i2c.h>
+#include <linux/iopoll.h>
+#include <linux/module.h>
+#include <linux/power_supply.h>
+
+#define LT8491_TELE_TBAT_REG 0x0
+#define LT8491_TELE_POUT_REG 0x2
+#define LT8491_TELE_PIN_REG 0x4
+#define LT8491_TELE_EFF_REG 0x6
+#define LT8491_TELE_IOUT_REG 0x8
+#define LT8491_TELE_IIN_REG 0xA
+#define LT8491_TELE_VBAT_REG 0xC
+#define LT8491_TELE_VIN_REG 0xE
+#define LT8491_TELE_VINR_REG 0x10
+#define LT8491_STAT_CHARGER_REG 0x12
+#define LT8491_CTRL_UPDATE_TELEM_REG 0x26
+
+#define LT8491_CFG_RSENSE1_REG 0x28
+#define LT8491_CFG_RIMON_OUT_REG 0x2A
+#define LT8491_CFG_RSENSE2_REG 0x2C
+#define LT8491_CFG_RDACO_REG 0x2E
+#define LT8491_CFG_RFBOUT1_REG 0x30
+#define LT8491_CFG_RFBOUT2_REG 0x32
+#define LT8491_CFG_RDACI_REG 0x34
+#define LT8491_CFG_RFBIN2_REG 0x36
+#define LT8491_CFG_RFBIN1_REG 0x38
+#define LT8491_CFG_TBAT_MIN_REG 0x40
+#define LT8491_CFG_TBAT_MAX_REG 0x41
+#define LT8491_MFR_DATA1_LSB_REG 0x5C
+
+#define LT8491_TELEM_ACTIVE_MASK BIT(6)
+#define LT8491_CHARGING_MASK BIT(2)
+
+#define LT8491_UPDATE_TELEM_CMD 0xAA
+
+/* A telemetry update typically takes 20ms to complete, per the datasheet. */
+#define LT8491_TELEM_POLL_PERIOD_US 2000
+#define LT8491_TELEM_UPDATE_TIMEOUT_US 50000
+
+/* LT8491_CFG_TBAT_{MIN,MAX}_REG hold a signed whole-degree-Celsius value. */
+#define LT8491_TBAT_MIN_C -128
+#define LT8491_TBAT_MAX_C 127
+
+/* LT8491_TELE_TBAT_REG: reset value, and thermistor-disconnected indication. */
+#define LT8491_TBAT_NOT_MEASURED 0x7FFF
+#define LT8491_TBAT_DISCONNECTED 0x7777
+
+#define LT8491_MFR_DATA_LEN 0x3
+
+struct lt8491_info {
+ struct i2c_client *client;
+ struct power_supply *psp;
+ char serial_number[16];
+ /* protect against device accesses */
+ struct mutex lock;
+};
+
+static int lt8491_read_serial_number(struct lt8491_info *info)
+{
+ int i;
+ int serial_number[LT8491_MFR_DATA_LEN];
+
+ for (i = 0; i < LT8491_MFR_DATA_LEN; i++) {
+ serial_number[i] = i2c_smbus_read_word_data(info->client,
+ LT8491_MFR_DATA1_LSB_REG + i * 2);
+ if (serial_number[i] < 0)
+ return serial_number[i];
+ }
+
+ scnprintf(info->serial_number, sizeof(info->serial_number), "%04x%04x%04x",
+ serial_number[0], serial_number[1], serial_number[2]);
+
+ return 0;
+}
+
+static int lt8491_update_telemetry(struct lt8491_info *info)
+{
+ int ret, val;
+
+ ret = i2c_smbus_read_byte_data(info->client, LT8491_STAT_CHARGER_REG);
+ if (ret < 0)
+ return ret;
+
+ /* Telemetry acquisition is already running; nothing to trigger. */
+ if (FIELD_GET(LT8491_TELEM_ACTIVE_MASK, ret))
+ return 0;
+
+ ret = i2c_smbus_write_byte_data(info->client, LT8491_CTRL_UPDATE_TELEM_REG,
+ LT8491_UPDATE_TELEM_CMD);
+ if (ret)
+ return ret;
+
+ ret = read_poll_timeout(i2c_smbus_read_byte_data, val,
+ val < 0 || !FIELD_GET(LT8491_TELEM_ACTIVE_MASK, val),
+ LT8491_TELEM_POLL_PERIOD_US,
+ LT8491_TELEM_UPDATE_TIMEOUT_US, false,
+ info->client, LT8491_STAT_CHARGER_REG);
+ if (ret)
+ return ret;
+
+ return val < 0 ? val : 0;
+}
+
+static int lt8491_get_property(struct power_supply *psy,
+ enum power_supply_property psp,
+ union power_supply_propval *val)
+{
+ struct lt8491_info *info = power_supply_get_drvdata(psy);
+ int ret;
+
+ switch (psp) {
+ case POWER_SUPPLY_PROP_STATUS:
+ ret = i2c_smbus_read_byte_data(info->client,
+ LT8491_STAT_CHARGER_REG);
+ if (ret < 0)
+ return ret;
+
+ val->intval = FIELD_GET(LT8491_CHARGING_MASK, ret) ?
+ POWER_SUPPLY_STATUS_CHARGING :
+ POWER_SUPPLY_STATUS_NOT_CHARGING;
+
+ return 0;
+ case POWER_SUPPLY_PROP_VOLTAGE_NOW:
+ scoped_guard(mutex, &info->lock) {
+ ret = lt8491_update_telemetry(info);
+ if (ret)
+ return ret;
+
+ ret = i2c_smbus_read_word_data(info->client,
+ LT8491_TELE_VBAT_REG);
+ if (ret < 0)
+ return ret;
+ }
+
+ val->intval = ret * 10000;
+
+ return 0;
+ case POWER_SUPPLY_PROP_CURRENT_NOW:
+ scoped_guard(mutex, &info->lock) {
+ ret = lt8491_update_telemetry(info);
+ if (ret)
+ return ret;
+
+ ret = i2c_smbus_read_word_data(info->client,
+ LT8491_TELE_IOUT_REG);
+ if (ret < 0)
+ return ret;
+ }
+
+ val->intval = ret * 1000;
+
+ return 0;
+ case POWER_SUPPLY_PROP_POWER_NOW:
+ scoped_guard(mutex, &info->lock) {
+ ret = lt8491_update_telemetry(info);
+ if (ret)
+ return ret;
+
+ ret = i2c_smbus_read_word_data(info->client,
+ LT8491_TELE_POUT_REG);
+ if (ret < 0)
+ return ret;
+ }
+
+ val->intval = ret * 10000;
+
+ return 0;
+ case POWER_SUPPLY_PROP_TEMP:
+ scoped_guard(mutex, &info->lock) {
+ ret = lt8491_update_telemetry(info);
+ if (ret)
+ return ret;
+
+ ret = i2c_smbus_read_word_data(info->client,
+ LT8491_TELE_TBAT_REG);
+ if (ret < 0)
+ return ret;
+ }
+
+ if (ret == LT8491_TBAT_NOT_MEASURED ||
+ ret == LT8491_TBAT_DISCONNECTED)
+ return -ENODATA;
+
+ val->intval = sign_extend32(ret, 15);
+
+ return 0;
+ case POWER_SUPPLY_PROP_TEMP_ALERT_MIN:
+ ret = i2c_smbus_read_byte_data(info->client,
+ LT8491_CFG_TBAT_MIN_REG);
+ if (ret < 0)
+ return ret;
+
+ val->intval = sign_extend32(ret, 7) * 10;
+
+ return 0;
+ case POWER_SUPPLY_PROP_TEMP_ALERT_MAX:
+ ret = i2c_smbus_read_byte_data(info->client,
+ LT8491_CFG_TBAT_MAX_REG);
+ if (ret < 0)
+ return ret;
+
+ val->intval = sign_extend32(ret, 7) * 10;
+
+ return 0;
+ case POWER_SUPPLY_PROP_MODEL_NAME:
+ val->strval = "lt8491";
+
+ return 0;
+ case POWER_SUPPLY_PROP_MANUFACTURER:
+ val->strval = "Analog Devices";
+
+ return 0;
+ case POWER_SUPPLY_PROP_SERIAL_NUMBER:
+ val->strval = info->serial_number;
+
+ return 0;
+ default:
+ return -EINVAL;
+ }
+}
+
+static int lt8491_set_property(struct power_supply *psy,
+ enum power_supply_property psp,
+ const union power_supply_propval *val)
+{
+ struct lt8491_info *info = power_supply_get_drvdata(psy);
+
+ switch (psp) {
+ case POWER_SUPPLY_PROP_TEMP_ALERT_MIN:
+ case POWER_SUPPLY_PROP_TEMP_ALERT_MAX:
+ if (val->intval < LT8491_TBAT_MIN_C * 10 ||
+ val->intval > LT8491_TBAT_MAX_C * 10)
+ return -EINVAL;
+
+ return i2c_smbus_write_byte_data(info->client,
+ psp == POWER_SUPPLY_PROP_TEMP_ALERT_MIN ?
+ LT8491_CFG_TBAT_MIN_REG :
+ LT8491_CFG_TBAT_MAX_REG,
+ val->intval / 10);
+ default:
+ return -EINVAL;
+ }
+}
+
+static int lt8491_property_is_writeable(struct power_supply *psy,
+ enum power_supply_property psp)
+{
+ switch (psp) {
+ case POWER_SUPPLY_PROP_TEMP_ALERT_MIN:
+ case POWER_SUPPLY_PROP_TEMP_ALERT_MAX:
+ return 1;
+ default:
+ return 0;
+ }
+}
+
+static const enum power_supply_property lt8491_properties[] = {
+ POWER_SUPPLY_PROP_STATUS,
+ POWER_SUPPLY_PROP_VOLTAGE_NOW,
+ POWER_SUPPLY_PROP_CURRENT_NOW,
+ POWER_SUPPLY_PROP_POWER_NOW,
+ POWER_SUPPLY_PROP_TEMP,
+ POWER_SUPPLY_PROP_TEMP_ALERT_MIN,
+ POWER_SUPPLY_PROP_TEMP_ALERT_MAX,
+ POWER_SUPPLY_PROP_MODEL_NAME,
+ POWER_SUPPLY_PROP_MANUFACTURER,
+ POWER_SUPPLY_PROP_SERIAL_NUMBER,
+};
+
+static const struct power_supply_desc lt8491_desc = {
+ .name = "lt8491",
+ .type = POWER_SUPPLY_TYPE_BATTERY,
+ .properties = lt8491_properties,
+ .num_properties = ARRAY_SIZE(lt8491_properties),
+ .get_property = lt8491_get_property,
+ .set_property = lt8491_set_property,
+ .property_is_writeable = lt8491_property_is_writeable,
+};
+
+static int lt8491_configure_resistor(struct lt8491_info *info,
+ const char *propname, int divider,
+ unsigned int reg)
+{
+ struct device *dev = &info->client->dev;
+ int ret;
+ u32 val;
+
+ ret = device_property_read_u32(dev, propname, &val);
+ if (ret < 0)
+ return dev_err_probe(dev, ret, "Missing %s property.\n",
+ propname);
+
+ val /= divider;
+ if (val > U16_MAX)
+ return dev_err_probe(dev, -EINVAL,
+ "Value for %s is out of range.\n",
+ propname);
+
+ /* The device has no word-write transaction; write low then high byte. */
+ ret = i2c_smbus_write_byte_data(info->client, reg, val & 0xff);
+ if (ret)
+ return ret;
+
+ return i2c_smbus_write_byte_data(info->client, reg + 1, val >> 8);
+}
+
+static int lt8491_configure_telemetry(struct lt8491_info *info)
+{
+ int ret;
+
+ ret = lt8491_configure_resistor(info, "adi,rsense1-micro-ohms", 10,
+ LT8491_CFG_RSENSE1_REG);
+ if (ret)
+ return ret;
+
+ ret = lt8491_configure_resistor(info, "adi,rimon-out-ohms", 10,
+ LT8491_CFG_RIMON_OUT_REG);
+ if (ret)
+ return ret;
+
+ ret = lt8491_configure_resistor(info, "adi,rsense2-micro-ohms", 10,
+ LT8491_CFG_RSENSE2_REG);
+ if (ret)
+ return ret;
+
+ ret = lt8491_configure_resistor(info, "adi,rdaco-ohms", 10,
+ LT8491_CFG_RDACO_REG);
+ if (ret)
+ return ret;
+
+ ret = lt8491_configure_resistor(info, "adi,rfbout1-ohms", 100,
+ LT8491_CFG_RFBOUT1_REG);
+ if (ret)
+ return ret;
+
+ ret = lt8491_configure_resistor(info, "adi,rfbout2-ohms", 10,
+ LT8491_CFG_RFBOUT2_REG);
+ if (ret)
+ return ret;
+
+ ret = lt8491_configure_resistor(info, "adi,rdaci-ohms", 10,
+ LT8491_CFG_RDACI_REG);
+ if (ret)
+ return ret;
+
+ ret = lt8491_configure_resistor(info, "adi,rfbin2-ohms", 10,
+ LT8491_CFG_RFBIN2_REG);
+ if (ret)
+ return ret;
+
+ return lt8491_configure_resistor(info, "adi,rfbin1-ohms", 100,
+ LT8491_CFG_RFBIN1_REG);
+}
+
+static int lt8491_probe(struct i2c_client *client)
+{
+ struct device *dev = &client->dev;
+ struct lt8491_info *info;
+ struct power_supply_config psy_cfg = {};
+ int ret;
+
+ if (!i2c_check_functionality(client->adapter, I2C_FUNC_SMBUS_BYTE_DATA |
+ I2C_FUNC_SMBUS_READ_WORD_DATA))
+ return -EOPNOTSUPP;
+
+ info = devm_kzalloc(dev, sizeof(*info), GFP_KERNEL);
+ if (!info)
+ return -ENOMEM;
+
+ info->client = client;
+ psy_cfg.drv_data = info;
+
+ ret = devm_mutex_init(dev, &info->lock);
+ if (ret)
+ return ret;
+
+ ret = lt8491_read_serial_number(info);
+ if (ret)
+ return dev_err_probe(dev, ret,
+ "Can't read serial. Hardware error.\n");
+
+ ret = lt8491_configure_telemetry(info);
+ if (ret)
+ return ret;
+
+ info->psp = devm_power_supply_register(dev, <8491_desc, &psy_cfg);
+ if (IS_ERR(info->psp))
+ return dev_err_probe(dev, PTR_ERR(info->psp),
+ "Failed to register power supply.\n");
+
+ return 0;
+}
+
+static const struct i2c_device_id lt8491_id[] = {
+ { .name = "lt8491" },
+ { }
+};
+MODULE_DEVICE_TABLE(i2c, lt8491_id);
+
+static const struct of_device_id lt8491_of_match[] = {
+ { .compatible = "adi,lt8491" },
+ { }
+};
+MODULE_DEVICE_TABLE(of, lt8491_of_match);
+
+static struct i2c_driver lt8491_driver = {
+ .driver = {
+ .name = "lt8491",
+ .of_match_table = lt8491_of_match,
+ },
+ .probe = lt8491_probe,
+ .id_table = lt8491_id,
+};
+module_i2c_driver(lt8491_driver);
+
+MODULE_AUTHOR("John Erasmus Mari Geronimo <johnerasmusmari.geronimo@analog.com>");
+MODULE_DESCRIPTION("LT8491 battery charger");
+MODULE_LICENSE("GPL");
--
2.34.1
prev parent reply other threads:[~2026-10-29 7:30 UTC|newest]
Thread overview: 3+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-29 7:30 [PATCH v3 0/2] Add LT8491 driver John Erasmus Mari Geronimo
2026-10-29 7:30 ` [PATCH v3 1/2] dt-bindings: power: supply: add adi,lt8491.yaml John Erasmus Mari Geronimo
2026-10-29 7:30 ` John Erasmus Mari Geronimo [this message]
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261029073012.1498-3-johnerasmusmari.geronimo@analog.com \
--to=johnerasmusmari.geronimo@analog.com \
--cc=conor+dt@kernel.org \
--cc=devicetree@vger.kernel.org \
--cc=krzk+dt@kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-pm@vger.kernel.org \
--cc=robh@kernel.org \
--cc=sre@kernel.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
all inboxes | Powered by JetHome®