From: John Erasmus Mari Geronimo <johnerasmusmari.geronimo@analog.com>
To: <linux-pm@vger.kernel.org>, <devicetree@vger.kernel.org>,
<linux-kernel@vger.kernel.org>
Cc: <krzk+dt@kernel.org>, <conor+dt@kernel.org>, <robh@kernel.org>,
<sre@kernel.org>
Subject: [PATCH v2 2/2] power: supply: add LT8491 battery charger driver
Date: Thu, 3 Sep 2026 18:46:37 +0800 [thread overview]
Message-ID: <9c260e1c2056c69be1cb7a05abcd10ba657c1548.1788432152.git.johnerasmusmari.geronimo@analog.com> (raw)
In-Reply-To: <cover.1788432152.git.johnerasmusmari.geronimo@analog.com>
Adds initial support for the LT8491 High Voltage Buck-Boost Battery
Charge Controller with I2C
The driver provides functionality to monitor key parameters including:
Voltage, Current, Power, Temperature, Status
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 | 412 ++++++++++++++++++++++++++
4 files changed, 423 insertions(+)
create mode 100644 drivers/power/supply/lt8491_charger.c
diff --git a/MAINTAINERS b/MAINTAINERS
index d79187312f789..317ca2e8ca9bd 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -15175,6 +15175,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 f0ede1cecb6a8..146ea3743eb2d 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 31fe4a145929a..d3d2721333204 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 0000000000000..225d5dd4dd721
--- /dev/null
+++ b/drivers/power/supply/lt8491_charger.c
@@ -0,0 +1,412 @@
+// 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/mod_devicetable.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_STAT_CHRG_FAULTS_REG 0x19
+#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_BAT_DISCON_FLT_MASK BIT(3)
+
+#define LT8491_UPDATE_TELEM_CMD 0xAA
+
+#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, ret;
+ 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];
+ }
+
+ ret = sprintf(info->serial_number, "%04x%04x%04x", serial_number[0],
+ serial_number[1], serial_number[2]);
+ if (ret < 0)
+ return ret;
+
+ return 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);
+ s16 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 = i2c_smbus_read_byte_data(info->client,
+ LT8491_STAT_CHARGER_REG);
+ if (ret < 0)
+ return ret;
+
+ if (!FIELD_GET(LT8491_TELEM_ACTIVE_MASK, ret)) {
+ ret = i2c_smbus_write_byte_data(info->client,
+ LT8491_CTRL_UPDATE_TELEM_REG,
+ LT8491_UPDATE_TELEM_CMD);
+ 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 = i2c_smbus_read_byte_data(info->client,
+ LT8491_STAT_CHARGER_REG);
+ if (ret < 0)
+ return ret;
+
+ if (!FIELD_GET(LT8491_TELEM_ACTIVE_MASK, ret)) {
+ ret = i2c_smbus_write_byte_data(info->client,
+ LT8491_CTRL_UPDATE_TELEM_REG,
+ LT8491_UPDATE_TELEM_CMD);
+ if (ret)
+ return ret;
+ }
+
+ ret = i2c_smbus_read_word_data(info->client,
+ LT8491_TELE_IOUT_REG);
+ if (ret < 0)
+ return ret;
+ }
+
+ val->intval = ret;
+
+ return 0;
+ case POWER_SUPPLY_PROP_POWER_NOW:
+ scoped_guard(mutex, &info->lock) {
+ ret = i2c_smbus_read_byte_data(info->client,
+ LT8491_STAT_CHARGER_REG);
+ if (ret < 0)
+ return ret;
+
+ if (!FIELD_GET(LT8491_TELEM_ACTIVE_MASK, ret)) {
+ ret = i2c_smbus_write_byte_data(info->client,
+ LT8491_CTRL_UPDATE_TELEM_REG,
+ LT8491_UPDATE_TELEM_CMD);
+ 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 = i2c_smbus_read_byte_data(info->client,
+ LT8491_STAT_CHARGER_REG);
+ if (ret < 0)
+ return ret;
+
+ if (!FIELD_GET(LT8491_TELEM_ACTIVE_MASK, ret)) {
+ ret = i2c_smbus_write_byte_data(info->client,
+ LT8491_CTRL_UPDATE_TELEM_REG,
+ LT8491_UPDATE_TELEM_CMD);
+ if (ret)
+ return ret;
+ }
+
+ ret = i2c_smbus_read_word_data(info->client,
+ LT8491_TELE_TBAT_REG);
+ if (ret < 0)
+ return ret;
+ }
+
+ 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;
+ }
+
+ return ret;
+}
+
+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:
+ return i2c_smbus_write_byte_data(info->client,
+ LT8491_CFG_TBAT_MIN_REG,
+ val->intval / 10);
+ case POWER_SUPPLY_PROP_TEMP_ALERT_MAX:
+ return i2c_smbus_write_byte_data(info->client,
+ 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 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);
+
+ return i2c_smbus_write_word_data(info->client, reg, val / divider);
+}
+
+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 = 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[] = {
+ { "lt8491", 0 },
+ { }
+};
+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
next prev parent reply other threads:[~2026-09-03 10:47 UTC|newest]
Thread overview: 11+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-01-10 8:02 [PATCH 0/2] Add LT8491 driver John Erasmus Mari Geronimo
2025-01-10 8:02 ` [PATCH 1/2] dt-bindings: power: supply: add adi,lt8491.yaml John Erasmus Mari Geronimo
2025-01-10 9:26 ` Krzysztof Kozlowski
2025-01-10 8:02 ` [PATCH 2/2] power: supply: add LT8491 battery charger driver John Erasmus Mari Geronimo
2025-01-14 16:45 ` kernel test robot
2026-09-03 10:46 ` [PATCH v2 0/2] Add LT8491 driver John Erasmus Mari Geronimo
2026-09-03 10:46 ` [PATCH v2 1/2] dt-bindings: power: supply: add adi,lt8491.yaml John Erasmus Mari Geronimo
2026-09-03 10:52 ` sashiko-bot
2026-09-03 17:21 ` Conor Dooley
2026-09-03 10:46 ` John Erasmus Mari Geronimo [this message]
2026-09-03 10:56 ` [PATCH v2 2/2] power: supply: add LT8491 battery charger driver sashiko-bot
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=9c260e1c2056c69be1cb7a05abcd10ba657c1548.1788432152.git.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