Devicetree
 help / color / mirror / Atom feed
* [RFC PATCH 0/3] power: supply: read-only CellWise CW2017 monitor for Redmi Go
@ 2026-10-29 17:53 Kupreenko Danila
  2026-10-29 17:53 ` [RFC PATCH 1/3] dt-bindings: power: supply: add CellWise CW2017 fuel gauge Kupreenko Danila
                   ` (2 more replies)
  0 siblings, 3 replies; 7+ messages in thread
From: Kupreenko Danila @ 2026-10-29 17:53 UTC (permalink / raw)
  To: Sebastian Reichel
  Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Bjorn Andersson,
	Konrad Dybcio, Abel Vesa, linux-pm, devicetree, linux-arm-msm,
	linux-kernel

Add a read-only power_supply driver for the CellWise CW2017 fuel gauge,
its Devicetree binding, and the Xiaomi Redmi Go (tiare) I2C node.

Unlike CW2015, CW2017 has a different register map and battery profile
protocol. The driver requires a gauge already initialized with a valid
battery profile; it reads voltage, integer SOC and temperature but never
writes CONFIG, changes the battery profile or resets the chip. Charging
status is unknown; presence is not inferred.

The CW2017 source is an independent implementation assisted by Codex
(GPT-6). Firefly was consulted later as a behavioral reference, not as
a source of copied code. AI assistance is disclosed in commit trailers.

Hardware testing on Xiaomi Redmi Go (tiare), postmarketOS, Linux
7.1.3-msm89x7 with a module built from the same RFC driver source (R5):
  - module auto-loaded and bound to I2C device 2-0063;
  - running srcversion matched the locally built R5 module;
  - capacity, voltage and temperature appeared in power_supply sysfs;
  - three consecutive measurements remained accessible;
  - udevadm monitor observed kernel power_supply change events, including
    changes in voltage and temperature, with roughly 30-second cadence.

Mainline 7.3 ARM64 build/MODPOST, targeted DT/schema validation, sparse
and host mock regressions with sanitizers passed. The mainline kernel
itself has NOT been booted on Redmi Go. Hardware PM/unbind, power cost,
calibration, temperature-source provenance and induced I2C error recovery
remain untested. The source operates as a monitor, not a safety controller.

Open review questions include policy for already-initialized gauges,
3-attempt probe retries, nominal 30-second polling and up to 300-second
backoff after failures, and event frequency with small voltage changes.

This is the first PUBLIC RFC; preceding revisions remained local.

Kupreenko Danila (3):
  dt-bindings: power: supply: add CellWise CW2017 fuel gauge
  power: supply: add read-only CW2017 battery monitor
  arm64: dts: qcom: tiare: describe CW2017 fuel gauge

 .../power/supply/cellwise,cw2017.yaml         |  52 +++
 MAINTAINERS                                   |   7 +
 .../boot/dts/qcom/msm8917-xiaomi-tiare.dts    |   9 +
 drivers/power/supply/Kconfig                  |  14 +
 drivers/power/supply/Makefile                 |   1 +
 drivers/power/supply/cw2017_battery.c         | 370 ++++++++++++++++++
 6 files changed, 453 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/power/supply/cellwise,cw2017.yaml
 create mode 100644 drivers/power/supply/cw2017_battery.c

base-commit: c339f4010dea8b0d754f84ace70cd1b089878ef8
-- 
2.56.0

^ permalink raw reply	[flat|nested] 7+ messages in thread

* [RFC PATCH 2/3] power: supply: add read-only CW2017 battery monitor
  2026-10-29 17:53 [RFC PATCH 0/3] power: supply: read-only CellWise CW2017 monitor for Redmi Go Kupreenko Danila
  2026-10-29 17:53 ` [RFC PATCH 1/3] dt-bindings: power: supply: add CellWise CW2017 fuel gauge Kupreenko Danila
  2026-10-29 17:53 ` [RFC PATCH 3/3] arm64: dts: qcom: tiare: describe " Kupreenko Danila
@ 2026-10-29 17:53 ` Kupreenko Danila
  2026-10-29 18:06   ` sashiko-bot
  2 siblings, 1 reply; 7+ messages in thread
From: Kupreenko Danila @ 2026-10-29 17:53 UTC (permalink / raw)
  To: Sebastian Reichel
  Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Bjorn Andersson,
	Konrad Dybcio, Abel Vesa, linux-pm, devicetree, linux-arm-msm,
	linux-kernel

Expose voltage, state of charge and temperature from a firmware-
initialized CW2017. Its register map and profile protocol differ from
CW2015, so use a separate monitor without initialization, reset or
profile upload operations. Reject all register-content writes through
regmap.

Require a successful VERSION read before registering power_supply. Retry
common bus errors up to three times with 20 ms between attempts, then
fail probe with the last errno. After communication succeeds, register
temporarily unavailable measurements and poll for recovery. Preserve
EPROBE_DEFER.

Keep the original sample errno internally and in rate-limited debug
output, but return ENODATA for unavailable measurements. Other transport
errno values abort power_supply_uevent, preventing an availability-
change notification from being formatted. Notify on validity transitions
and any changed exported measurement; fractional SOC changes that do not
change capacity stay quiet.

Poll every 30 seconds with coalesced timers. Back off failed polls
through 60, 120, 240 and at most 300 seconds; restore the normal period
on success. Resume triggers an immediate read and resets backoff. This
trades freshness for fewer scheduled polls, without claiming measured
energy savings.

Protect cached samples with a mutex and cancel managed polling before
the power supply and regmap are released. Omit unsupported battery
presence and leave charging status unknown. Profile markers do not prove
calibration or temperature-source correctness; this monitor depends on
firmware setup.

Developed with Codex (GPT-6), independently of the later Firefly
reference. Host tests exercise the actual driver and mainline uevent
helpers with mocked kernel infrastructure.

The same driver source was built for Linux 7.1.3-msm89x7 and tested
on Xiaomi Redmi Go (R5). Module autoload, I2C binding, battery
measurements and kernel power_supply change events were verified.
The mainline kernel itself was not booted on this device.
Hardware suspend/resume, teardown, calibration and energy use
remain untested.

Assisted-by: LLM sparse
Signed-off-by: Kupreenko Danila <keckerz@yandex.ru>
---
 MAINTAINERS                           |   1 +
 drivers/power/supply/Kconfig          |  14 +
 drivers/power/supply/Makefile         |   1 +
 drivers/power/supply/cw2017_battery.c | 370 ++++++++++++++++++++++++++
 4 files changed, 386 insertions(+)
 create mode 100644 drivers/power/supply/cw2017_battery.c

diff --git a/MAINTAINERS b/MAINTAINERS
index 27124fd02..29a2e318a 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -6159,6 +6159,7 @@ M:	Kupreenko Danila <Keckerz@yandex.ru>
 L:	linux-pm@vger.kernel.org
 S:	Maintained
 F:	Documentation/devicetree/bindings/power/supply/cellwise,cw2017.yaml
+F:	drivers/power/supply/cw2017_battery.c
 
 CEPH COMMON CODE (LIBCEPH)
 M:	Ilya Dryomov <idryomov@gmail.com>
diff --git a/drivers/power/supply/Kconfig b/drivers/power/supply/Kconfig
index ee7dc5f26..a0503db31 100644
--- a/drivers/power/supply/Kconfig
+++ b/drivers/power/supply/Kconfig
@@ -141,6 +141,20 @@ config BATTERY_CW2015
 	  This driver can also be built as a module. If so, the module will be
 	  called cw2015_battery.
 
+config BATTERY_CW2017
+	tristate "CellWise CW2017 battery fuel gauge"
+	depends on I2C
+	select REGMAP_I2C
+	help
+	  Read voltage, temperature and state of charge from a CellWise CW2017
+	  fuel gauge which has already been initialized with the correct battery
+	  profile. This driver does not wake or reset the gauge, upload profiles,
+	  or supply host-reported temperature. Uninitialized gauges do not report
+	  measurements. Battery profiles must use a maintained temperature input.
+
+	  To compile this driver as a module, choose M here: the module will be
+	  called cw2017_battery.
+
 config BATTERY_DS2760
 	tristate "DS2760 battery driver (HP iPAQ & others)"
 	depends on W1
diff --git a/drivers/power/supply/Makefile b/drivers/power/supply/Makefile
index f3c0d1006..04f496d29 100644
--- a/drivers/power/supply/Makefile
+++ b/drivers/power/supply/Makefile
@@ -27,6 +27,7 @@ obj-$(CONFIG_CHARGER_AXP20X)	+= axp20x_ac_power.o
 obj-$(CONFIG_BATTERY_CHAGALL)	+= chagall-battery.o
 obj-$(CONFIG_BATTERY_CPCAP)	+= cpcap-battery.o
 obj-$(CONFIG_BATTERY_CW2015)	+= cw2015_battery.o
+obj-$(CONFIG_BATTERY_CW2017)	+= cw2017_battery.o
 obj-$(CONFIG_BATTERY_DS2760)	+= ds2760_battery.o
 obj-$(CONFIG_BATTERY_DS2780)	+= ds2780_battery.o
 obj-$(CONFIG_BATTERY_DS2781)	+= ds2781_battery.o
diff --git a/drivers/power/supply/cw2017_battery.c b/drivers/power/supply/cw2017_battery.c
new file mode 100644
index 000000000..86323849a
--- /dev/null
+++ b/drivers/power/supply/cw2017_battery.c
@@ -0,0 +1,370 @@
+// SPDX-License-Identifier: GPL-2.0-only
+/*
+ * Read-only monitoring of an initialized CellWise CW2017 fuel gauge.
+ * Register definitions and conversions follow the CW2017-DS V1.1 datasheet
+ * and Xiaomi's tiare-o-oss cw2017_fuel_gauge.c driver.
+ */
+
+#include <linux/bits.h>
+#include <linux/delay.h>
+#include <linux/devm-helpers.h>
+#include <linux/err.h>
+#include <linux/i2c.h>
+#include <linux/jiffies.h>
+#include <linux/module.h>
+#include <linux/minmax.h>
+#include <linux/mutex.h>
+#include <linux/of.h>
+#include <linux/pm.h>
+#include <linux/power_supply.h>
+#include <linux/regmap.h>
+#include <linux/slab.h>
+#include <linux/unaligned.h>
+#include <linux/workqueue.h>
+
+#define CW2017_REG_VERSION	0x00
+#define CW2017_REG_VCELL		0x02
+#define CW2017_REG_SOC		0x04
+#define CW2017_REG_TEMP		0x06
+#define CW2017_REG_CONFIG		0x08
+#define CW2017_REG_SOC_ALERT	0x0b
+
+#define CW2017_VERSION_NOT_READY	0xa0
+#define CW2017_CONFIG_NORMAL	0x00
+#define CW2017_PROFILE_UPDATED	BIT(7)
+#define CW2017_VCELL_MAX		GENMASK(13, 0)
+#define CW2017_SOC_MAX		(100 * 256)
+#define CW2017_POLL_INTERVAL_MS	30000U
+#define CW2017_MAX_POLL_INTERVAL_MS	300000U
+#define CW2017_PROBE_ATTEMPTS	3
+#define CW2017_PROBE_RETRY_MS	20
+
+struct cw2017_sample {
+	int voltage_uv;
+	int capacity;
+	int temp;
+	int error;
+};
+
+struct cw2017 {
+	struct device *dev;
+	struct regmap *regmap;
+	struct power_supply *psy;
+	struct power_supply_desc desc;
+	struct delayed_work work;
+	/* Protects the cached sample shared by polling and property readers. */
+	struct mutex lock;
+	struct cw2017_sample sample;
+	unsigned int poll_interval_ms;
+};
+
+/* Require an answer before registering a supply; do not infer battery presence. */
+static int cw2017_check_communication(struct cw2017 *cw)
+{
+	unsigned int version;
+	int attempt, ret;
+
+	for (attempt = 0; attempt < CW2017_PROBE_ATTEMPTS; attempt++) {
+		ret = regmap_read(cw->regmap, CW2017_REG_VERSION, &version);
+		switch (ret) {
+		case -EAGAIN:
+		case -EIO:
+		case -ENXIO:
+		case -EREMOTEIO:
+		case -ETIMEDOUT:
+			break;
+		default:
+			return ret;
+		}
+
+		if (attempt + 1 < CW2017_PROBE_ATTEMPTS)
+			msleep(CW2017_PROBE_RETRY_MS);
+	}
+
+	return ret;
+}
+
+/* Never wake/reset the gauge or change its profile as a side effect of a read. */
+static int cw2017_check_ready(struct cw2017 *cw)
+{
+	unsigned int value;
+	int ret;
+
+	ret = regmap_read(cw->regmap, CW2017_REG_CONFIG, &value);
+	if (ret)
+		return ret;
+	if (value != CW2017_CONFIG_NORMAL)
+		return -ENODATA;
+
+	ret = regmap_read(cw->regmap, CW2017_REG_VERSION, &value);
+	if (ret)
+		return ret;
+	if (value == CW2017_VERSION_NOT_READY)
+		return -ENODATA;
+
+	ret = regmap_read(cw->regmap, CW2017_REG_SOC_ALERT, &value);
+	if (ret)
+		return ret;
+
+	/* This flag is a host-written marker, not a profile checksum. */
+	return value & CW2017_PROFILE_UPDATED ? 0 : -ENODATA;
+}
+
+static int cw2017_read_sample(struct cw2017 *cw, struct cw2017_sample *sample)
+{
+	unsigned int temp;
+	u16 voltage, soc;
+	u8 raw[2];
+	int ret;
+
+	ret = cw2017_check_ready(cw);
+	if (ret)
+		return ret;
+
+	ret = regmap_bulk_read(cw->regmap, CW2017_REG_VCELL, raw, sizeof(raw));
+	if (ret)
+		return ret;
+	voltage = get_unaligned_be16(raw);
+
+	ret = regmap_bulk_read(cw->regmap, CW2017_REG_SOC, raw, sizeof(raw));
+	if (ret)
+		return ret;
+	soc = get_unaligned_be16(raw);
+
+	ret = regmap_read(cw->regmap, CW2017_REG_TEMP, &temp);
+	if (ret)
+		return ret;
+
+	if (voltage > CW2017_VCELL_MAX || soc > CW2017_SOC_MAX)
+		return -ENODATA;
+
+	/* Reject a sample if a reset became visible during the reads. */
+	ret = cw2017_check_ready(cw);
+	if (ret)
+		return ret;
+
+	/* VCELL: 312.5 uV/LSB; SOC: 1/256 percent; TEMP: 0.5 C/LSB - 40 C. */
+	sample->voltage_uv = voltage * 625 / 2;
+	sample->capacity = soc / 256;
+	sample->temp = (int)temp * 5 - 400;
+
+	return 0;
+}
+
+static int cw2017_get_property(struct power_supply *psy,
+			       enum power_supply_property property,
+			       union power_supply_propval *value)
+{
+	struct cw2017 *cw = power_supply_get_drvdata(psy);
+	int ret;
+
+	switch (property) {
+	case POWER_SUPPLY_PROP_MODEL_NAME:
+		value->strval = "CW2017";
+		return 0;
+	case POWER_SUPPLY_PROP_MANUFACTURER:
+		value->strval = "CellWise";
+		return 0;
+	case POWER_SUPPLY_PROP_STATUS:
+		value->intval = POWER_SUPPLY_STATUS_UNKNOWN;
+		return 0;
+	case POWER_SUPPLY_PROP_VOLTAGE_NOW:
+	case POWER_SUPPLY_PROP_CAPACITY:
+	case POWER_SUPPLY_PROP_TEMP:
+		break;
+	default:
+		return -EINVAL;
+	}
+
+	mutex_lock(&cw->lock);
+	/* Omit unavailable measurements from uevents instead of aborting them. */
+	ret = cw->sample.error ? -ENODATA : 0;
+	if (!ret) {
+		switch (property) {
+		case POWER_SUPPLY_PROP_VOLTAGE_NOW:
+			value->intval = cw->sample.voltage_uv;
+			break;
+		case POWER_SUPPLY_PROP_CAPACITY:
+			value->intval = cw->sample.capacity;
+			break;
+		case POWER_SUPPLY_PROP_TEMP:
+			value->intval = cw->sample.temp;
+			break;
+		default:
+			break;
+		}
+	}
+	mutex_unlock(&cw->lock);
+
+	return ret;
+}
+
+static void cw2017_poll(struct work_struct *work)
+{
+	struct cw2017 *cw = container_of(to_delayed_work(work), struct cw2017, work);
+	struct cw2017_sample sample = { };
+	bool changed;
+
+	sample.error = cw2017_read_sample(cw, &sample);
+	if (sample.error)
+		dev_dbg_ratelimited(cw->dev, "Measurements unavailable (%d)\n",
+				    sample.error);
+
+	mutex_lock(&cw->lock);
+	changed = !cw->sample.error != !sample.error ||
+		  (!sample.error &&
+		   (cw->sample.capacity != sample.capacity ||
+		    cw->sample.voltage_uv != sample.voltage_uv ||
+		    cw->sample.temp != sample.temp));
+	cw->sample = sample;
+	mutex_unlock(&cw->lock);
+
+	if (changed)
+		power_supply_changed(cw->psy);
+
+	if (sample.error)
+		cw->poll_interval_ms = min(cw->poll_interval_ms * 2,
+					   CW2017_MAX_POLL_INTERVAL_MS);
+	else
+		cw->poll_interval_ms = CW2017_POLL_INTERVAL_MS;
+
+	queue_delayed_work(system_freezable_wq, &cw->work,
+			   round_jiffies_relative(msecs_to_jiffies(cw->poll_interval_ms)));
+}
+
+static bool cw2017_readable_reg(struct device *dev, unsigned int reg)
+{
+	return reg == CW2017_REG_VERSION ||
+	       (reg >= CW2017_REG_VCELL && reg <= CW2017_REG_TEMP) ||
+	       reg == CW2017_REG_CONFIG || reg == CW2017_REG_SOC_ALERT;
+}
+
+static bool cw2017_writeable_reg(struct device *dev, unsigned int reg)
+{
+	return false;
+}
+
+static const struct regmap_config cw2017_regmap_config = {
+	.reg_bits = 8,
+	.val_bits = 8,
+	.max_register = CW2017_REG_SOC_ALERT,
+	.readable_reg = cw2017_readable_reg,
+	.writeable_reg = cw2017_writeable_reg,
+	.cache_type = REGCACHE_NONE,
+};
+
+static const enum power_supply_property cw2017_properties[] = {
+	POWER_SUPPLY_PROP_STATUS,
+	POWER_SUPPLY_PROP_VOLTAGE_NOW,
+	POWER_SUPPLY_PROP_CAPACITY,
+	POWER_SUPPLY_PROP_TEMP,
+	POWER_SUPPLY_PROP_MODEL_NAME,
+	POWER_SUPPLY_PROP_MANUFACTURER,
+};
+
+static int cw2017_probe(struct i2c_client *client)
+{
+	struct device *dev = &client->dev;
+	struct power_supply_config config = { };
+	struct cw2017 *cw;
+	int ret;
+
+	cw = devm_kzalloc(dev, sizeof(*cw), GFP_KERNEL);
+	if (!cw)
+		return -ENOMEM;
+
+	mutex_init(&cw->lock);
+	cw->dev = dev;
+	cw->poll_interval_ms = CW2017_POLL_INTERVAL_MS;
+	i2c_set_clientdata(client, cw);
+
+	cw->regmap = devm_regmap_init_i2c(client, &cw2017_regmap_config);
+	if (IS_ERR(cw->regmap))
+		return dev_err_probe(dev, PTR_ERR(cw->regmap), "Failed to create regmap\n");
+
+	ret = cw2017_check_communication(cw);
+	if (ret)
+		return dev_err_probe(dev, ret, "Gauge did not respond\n");
+
+	cw->sample.error = cw2017_read_sample(cw, &cw->sample);
+	if (cw->sample.error == -EPROBE_DEFER)
+		return dev_err_probe(dev, cw->sample.error, "Failed to read gauge\n");
+	if (cw->sample.error)
+		dev_warn(dev, "Measurements unavailable (%d); polling will retry\n",
+			 cw->sample.error);
+
+	cw->desc.name = devm_kasprintf(dev, GFP_KERNEL, "cw2017-%s", dev_name(dev));
+	if (!cw->desc.name)
+		return -ENOMEM;
+	cw->desc.type = POWER_SUPPLY_TYPE_BATTERY;
+	cw->desc.properties = cw2017_properties;
+	cw->desc.num_properties = ARRAY_SIZE(cw2017_properties);
+	cw->desc.get_property = cw2017_get_property;
+	config.drv_data = cw;
+	config.fwnode = dev_fwnode(dev);
+
+	cw->psy = devm_power_supply_register(dev, &cw->desc, &config);
+	if (IS_ERR(cw->psy))
+		return dev_err_probe(dev, PTR_ERR(cw->psy), "Failed to register battery\n");
+
+	/* Cancel polling before devres releases the power supply and regmap. */
+	ret = devm_delayed_work_autocancel(dev, &cw->work, cw2017_poll);
+	if (ret)
+		return ret;
+
+	queue_delayed_work(system_freezable_wq, &cw->work,
+			   round_jiffies_relative(msecs_to_jiffies(cw->poll_interval_ms)));
+
+	return 0;
+}
+
+static int cw2017_suspend(struct device *dev)
+{
+	struct cw2017 *cw = dev_get_drvdata(dev);
+
+	cancel_delayed_work_sync(&cw->work);
+	mutex_lock(&cw->lock);
+	cw->sample.error = -ENODATA;
+	mutex_unlock(&cw->lock);
+
+	return 0;
+}
+
+static int cw2017_resume(struct device *dev)
+{
+	struct cw2017 *cw = dev_get_drvdata(dev);
+
+	cw->poll_interval_ms = CW2017_POLL_INTERVAL_MS;
+	queue_delayed_work(system_freezable_wq, &cw->work, 0);
+
+	return 0;
+}
+
+static DEFINE_SIMPLE_DEV_PM_OPS(cw2017_pm_ops, cw2017_suspend, cw2017_resume);
+
+static const struct of_device_id cw2017_of_match[] = {
+	{ .compatible = "cellwise,cw2017" },
+	{ }
+};
+MODULE_DEVICE_TABLE(of, cw2017_of_match);
+
+static const struct i2c_device_id cw2017_ids[] = {
+	{ "cw2017" },
+	{ }
+};
+MODULE_DEVICE_TABLE(i2c, cw2017_ids);
+
+static struct i2c_driver cw2017_driver = {
+	.driver = {
+		.name = "cw2017-battery",
+		.of_match_table = cw2017_of_match,
+		.pm = pm_sleep_ptr(&cw2017_pm_ops),
+	},
+	.probe = cw2017_probe,
+	.id_table = cw2017_ids,
+};
+module_i2c_driver(cw2017_driver);
+
+MODULE_AUTHOR("Kupreenko Danila <Keckerz@yandex.ru>");
+MODULE_DESCRIPTION("CellWise CW2017 battery monitor");
+MODULE_LICENSE("GPL");
-- 
2.56.0


^ permalink raw reply related	[flat|nested] 7+ messages in thread

* [RFC PATCH 1/3] dt-bindings: power: supply: add CellWise CW2017 fuel gauge
  2026-10-29 17:53 [RFC PATCH 0/3] power: supply: read-only CellWise CW2017 monitor for Redmi Go Kupreenko Danila
@ 2026-10-29 17:53 ` Kupreenko Danila
  2026-10-29 17:53 ` [RFC PATCH 3/3] arm64: dts: qcom: tiare: describe " Kupreenko Danila
  2026-10-29 17:53 ` [RFC PATCH 2/3] power: supply: add read-only CW2017 battery monitor Kupreenko Danila
  2 siblings, 0 replies; 7+ messages in thread
From: Kupreenko Danila @ 2026-10-29 17:53 UTC (permalink / raw)
  To: Sebastian Reichel
  Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Bjorn Andersson,
	Konrad Dybcio, Abel Vesa, linux-pm, devicetree, linux-arm-msm,
	linux-kernel

Describe the CW2017 at its fixed I2C address. The battery-specific
profile must match both the battery and its temperature input. Do not
introduce a profile upload property for the read-only monitor.

Add the author's explicitly agreed local maintenance commitment.

Assisted-by: LLM
Signed-off-by: Kupreenko Danila <keckerz@yandex.ru>
---
 .../power/supply/cellwise,cw2017.yaml         | 52 +++++++++++++++++++
 MAINTAINERS                                   |  6 +++
 2 files changed, 58 insertions(+)
 create mode 100644 Documentation/devicetree/bindings/power/supply/cellwise,cw2017.yaml

diff --git a/Documentation/devicetree/bindings/power/supply/cellwise,cw2017.yaml b/Documentation/devicetree/bindings/power/supply/cellwise,cw2017.yaml
new file mode 100644
index 000000000..a8404f150
--- /dev/null
+++ b/Documentation/devicetree/bindings/power/supply/cellwise,cw2017.yaml
@@ -0,0 +1,52 @@
+# SPDX-License-Identifier: (GPL-2.0-only OR BSD-2-Clause)
+%YAML 1.2
+---
+$id: http://devicetree.org/schemas/power/supply/cellwise,cw2017.yaml#
+$schema: http://devicetree.org/meta-schemas/core.yaml#
+
+title: CellWise CW2017 battery fuel gauge
+
+maintainers:
+  - Kupreenko Danila <Keckerz@yandex.ru>
+
+description: |
+  The CW2017 estimates the state of charge of a battery using its voltage,
+  temperature and a battery-specific characterization profile. It has a fixed
+  I2C address of 0x63. The CW2017 is not register compatible with the CW2015.
+
+  The battery profile also configures the temperature input. It must match
+  the connected battery and temperature sensing circuit. A profile selecting
+  host-reported temperature requires an external provider to keep that input
+  up to date. The public datasheet does not describe the profile encoding.
+
+  Datasheet:
+  https://uploadcdn.oneyac.com/upload/document/1737612108523_8373.pdf
+
+allOf:
+  - $ref: power-supply.yaml#
+
+properties:
+  compatible:
+    const: cellwise,cw2017
+
+  reg:
+    items:
+      - const: 0x63
+
+required:
+  - compatible
+  - reg
+
+unevaluatedProperties: false
+
+examples:
+  - |
+    i2c {
+        #address-cells = <1>;
+        #size-cells = <0>;
+
+        fuel-gauge@63 {
+            compatible = "cellwise,cw2017";
+            reg = <0x63>;
+        };
+    };
diff --git a/MAINTAINERS b/MAINTAINERS
index 748ce4d1c..27124fd02 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -6154,6 +6154,12 @@ S:	Maintained
 F:	Documentation/devicetree/bindings/power/supply/cw2015_battery.yaml
 F:	drivers/power/supply/cw2015_battery.c
 
+CELLWISE CW2017 BATTERY DRIVER
+M:	Kupreenko Danila <Keckerz@yandex.ru>
+L:	linux-pm@vger.kernel.org
+S:	Maintained
+F:	Documentation/devicetree/bindings/power/supply/cellwise,cw2017.yaml
+
 CEPH COMMON CODE (LIBCEPH)
 M:	Ilya Dryomov <idryomov@gmail.com>
 M:	Alex Markuze <amarkuze@redhat.com>
-- 
2.56.0


^ permalink raw reply related	[flat|nested] 7+ messages in thread

* [RFC PATCH 3/3] arm64: dts: qcom: tiare: describe CW2017 fuel gauge
  2026-10-29 17:53 [RFC PATCH 0/3] power: supply: read-only CellWise CW2017 monitor for Redmi Go Kupreenko Danila
  2026-10-29 17:53 ` [RFC PATCH 1/3] dt-bindings: power: supply: add CellWise CW2017 fuel gauge Kupreenko Danila
@ 2026-10-29 17:53 ` Kupreenko Danila
  2026-10-29 18:11   ` sashiko-bot
  2026-10-29 17:53 ` [RFC PATCH 2/3] power: supply: add read-only CW2017 battery monitor Kupreenko Danila
  2 siblings, 1 reply; 7+ messages in thread
From: Kupreenko Danila @ 2026-10-29 17:53 UTC (permalink / raw)
  To: Sebastian Reichel
  Cc: Rob Herring, Krzysztof Kozlowski, Conor Dooley, Bjorn Andersson,
	Konrad Dybcio, Abel Vesa, linux-pm, devicetree, linux-arm-msm,
	linux-kernel

Enable the I2C controller at 0x7af5000 and describe the CW2017 at 0x63.
The controller mapping and gauge response were demonstrated on Redmi Go
with Linux 7.1.3. The independent monitor also registered power_supply
on that kernel. No charger node or battery profile is added.

The resulting mainline DTB is host-built and schema checked; it has not
been booted on the device.

Assisted-by: LLM
Signed-off-by: Kupreenko Danila <keckerz@yandex.ru>
---
 arch/arm64/boot/dts/qcom/msm8917-xiaomi-tiare.dts | 9 +++++++++
 1 file changed, 9 insertions(+)

diff --git a/arch/arm64/boot/dts/qcom/msm8917-xiaomi-tiare.dts b/arch/arm64/boot/dts/qcom/msm8917-xiaomi-tiare.dts
index fe8442300..6d72a4763 100644
--- a/arch/arm64/boot/dts/qcom/msm8917-xiaomi-tiare.dts
+++ b/arch/arm64/boot/dts/qcom/msm8917-xiaomi-tiare.dts
@@ -14,6 +14,15 @@ / {
 	qcom,board-id = <0x1000b 1>;
 };
 
+&blsp2_i2c1 {
+	status = "okay";
+
+	fuel-gauge@63 {
+		compatible = "cellwise,cw2017";
+		reg = <0x63>;
+	};
+};
+
 &pm8937_l22 {
 	regulator-min-microvolt = <2850000>;
 	regulator-max-microvolt = <2850000>;
-- 
2.56.0


^ permalink raw reply related	[flat|nested] 7+ messages in thread

* Re: [RFC PATCH 2/3] power: supply: add read-only CW2017 battery monitor
  2026-10-29 17:53 ` [RFC PATCH 2/3] power: supply: add read-only CW2017 battery monitor Kupreenko Danila
@ 2026-10-29 18:06   ` sashiko-bot
  2026-10-29 19:10     ` Kupreenko Danila
  0 siblings, 1 reply; 7+ messages in thread
From: sashiko-bot @ 2026-10-29 18:06 UTC (permalink / raw)
  To: Kupreenko Danila; +Cc: devicetree, robh, conor+dt

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] power: supply: cw2017: broken sample.error tracking in cw2017_suspend()
--

commit 2974e2dfc1b0336eb1b26f72d45d626971f0a216
Author: Kupreenko Danila <keckerz@yandex.ru>

power: supply: add read-only CW2017 battery monitor

This patch adds a read-only power_supply driver for firmware-initialized
CellWise CW2017 battery fuel gauges over I2C, polling voltage, state of
charge, and temperature with exponential backoff on read errors.

> diff --git a/drivers/power/supply/cw2017_battery.c b/drivers/power/supply/cw2017_battery.c
> new file mode 100644
> index 0000000000000..86323849a813a
> --- /dev/null
> +++ b/drivers/power/supply/cw2017_battery.c
> @@ -0,0 +1,370 @@

[ ... ]

> +static void cw2017_poll(struct work_struct *work)
> +{
> +	struct cw2017 *cw = container_of(to_delayed_work(work), struct cw2017, work);
> +	struct cw2017_sample sample = { };
> +	bool changed;
> +
> +	sample.error = cw2017_read_sample(cw, &sample);
> +	if (sample.error)
> +		dev_dbg_ratelimited(cw->dev, "Measurements unavailable (%d)\n",
> +				    sample.error);
> +
> +	mutex_lock(&cw->lock);
> +	changed = !cw->sample.error != !sample.error ||
> +		  (!sample.error &&
> +		   (cw->sample.capacity != sample.capacity ||
> +		    cw->sample.voltage_uv != sample.voltage_uv ||
> +		    cw->sample.temp != sample.temp));
> +	cw->sample = sample;
> +	mutex_unlock(&cw->lock);
> +
> +	if (changed)
> +		power_supply_changed(cw->psy);
> +
> +	if (sample.error)
> +		cw->poll_interval_ms = min(cw->poll_interval_ms * 2,
> +					   CW2017_MAX_POLL_INTERVAL_MS);
> +	else
> +		cw->poll_interval_ms = CW2017_POLL_INTERVAL_MS;
> +
> +	queue_delayed_work(system_freezable_wq, &cw->work,
> +			   round_jiffies_relative(msecs_to_jiffies(cw->poll_interval_ms)));
> +}

[ ... ]

> +static int cw2017_suspend(struct device *dev)
> +{
> +	struct cw2017 *cw = dev_get_drvdata(dev);
> +
> +	cancel_delayed_work_sync(&cw->work);
> +	mutex_lock(&cw->lock);
> +	cw->sample.error = -ENODATA;
> +	mutex_unlock(&cw->lock);
> +
> +	return 0;
> +}
> +
> +static int cw2017_resume(struct device *dev)
> +{
> +	struct cw2017 *cw = dev_get_drvdata(dev);
> +
> +	cw->poll_interval_ms = CW2017_POLL_INTERVAL_MS;
> +	queue_delayed_work(system_freezable_wq, &cw->work, 0);
> +
> +	return 0;
> +}

[Severity: Medium]
Does setting cw->sample.error to -ENODATA in cw2017_suspend() without
emitting a notification break the validity-transition tracking in
cw2017_poll() across suspend and resume?

In cw2017_poll(), validity transitions are detected by comparing the cached
error state against the new sample error state:

	changed = !cw->sample.error != !sample.error ||
		  (!sample.error &&
		   (cw->sample.capacity != sample.capacity ||
		    cw->sample.voltage_uv != sample.voltage_uv ||
		    cw->sample.temp != sample.temp));

Because cw2017_suspend() overwrites cw->sample.error with -ENODATA without
calling power_supply_changed(cw->psy), two notification issues arise when
cw2017_poll() runs after resume:

1. If measurements were valid before suspend (cw->sample.error == 0) and
   cw2017_read_sample() fails after resume (for example, if the gauge lost
   its profile or readiness across suspend or an I2C error occurs), both
   cw->sample.error and sample.error are non-zero. As a result, changed
   evaluates to false and power_supply_changed(cw->psy) is never called,
   leaving consumers with stale pre-suspend values.

2. If measurements were valid before suspend and remain valid and unchanged
   after resume (sample.error == 0), !cw->sample.error != !sample.error
   evaluates to true solely because cw2017_suspend() forced cw->sample.error
   to -ENODATA, triggering an unconditional power_supply_changed(cw->psy)
   uevent on every resume.

In addition, cw2017_resume() queues cw->work on system_freezable_wq, which
remains frozen until suspend_thaw_processes() runs after dpm_complete().
When dpm_complete() -> thermal_pm_complete() schedules
thermal_zone_device_resume() on the non-freezable thermal_wq (registered
for POWER_SUPPLY_PROP_TEMP via __power_supply_register() ->
psy_register_thermal()), cw2017_get_property() will see cw->sample.error ==
-ENODATA and fail thermal zone resume before the post-resume poll work has a
chance to run.

Would it be better to preserve cw->sample across suspend so cw2017_poll()
can compare the post-resume reading against the last reported state?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/179329639684.489944.6322623410181934358.cw2017-first-public-rfc@yandex.ru?part=2

^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: [RFC PATCH 3/3] arm64: dts: qcom: tiare: describe CW2017 fuel gauge
  2026-10-29 17:53 ` [RFC PATCH 3/3] arm64: dts: qcom: tiare: describe " Kupreenko Danila
@ 2026-10-29 18:11   ` sashiko-bot
  0 siblings, 0 replies; 7+ messages in thread
From: sashiko-bot @ 2026-10-29 18:11 UTC (permalink / raw)
  To: Kupreenko Danila; +Cc: devicetree, robh, conor+dt

Thank you for your contribution! Sashiko AI review found 1 potential issue(s) to consider:
- [Medium] power: supply: cw2017: sample invalidation in cw2017_suspend() breaks resume
--

commit e63b82695c67205a9de8a30516bebe6f4181c2ad
Author: Kupreenko Danila <keckerz@yandex.ru>

arm64: dts: qcom: tiare: describe CW2017 fuel gauge

This commit enables the BLSP2 I2C1 controller and adds the CellWise CW2017
fuel gauge node at address 0x63 for the Xiaomi Redmi Go (msm8917-xiaomi-tiare).

> diff --git a/arch/arm64/boot/dts/qcom/msm8917-xiaomi-tiare.dts b/arch/arm64/boot/dts/qcom/msm8917-xiaomi-tiare.dts
> index fe844230030f2..6d72a4763ac48 100644
> --- a/arch/arm64/boot/dts/qcom/msm8917-xiaomi-tiare.dts
> +++ b/arch/arm64/boot/dts/qcom/msm8917-xiaomi-tiare.dts
> @@ -14,6 +14,15 @@ / {
>  	qcom,board-id = <0x1000b 1>;
>  };
>  
> +&blsp2_i2c1 {
> +	status = "okay";
> +
> +	fuel-gauge@63 {
> +		compatible = "cellwise,cw2017";
> +		reg = <0x63>;
> +	};
> +};

[Severity: Medium]
When binding the cellwise,cw2017 node added here to the cw2017-battery driver
from earlier in this series, does invalidating cw->sample.error in
cw2017_suspend() cause -ENODATA errors and spurious uevents across system
suspend and resume?

In drivers/power/supply/cw2017_battery.c, cw2017_suspend() overwrites the
cached sample state on every suspend:

drivers/power/supply/cw2017_battery.c:cw2017_suspend() {
	struct cw2017 *cw = dev_get_drvdata(dev);

	cancel_delayed_work_sync(&cw->work);
	mutex_lock(&cw->lock);
	cw->sample.error = -ENODATA;
	mutex_unlock(&cw->lock);

	return 0;
}

On resume, cw2017_resume() queues cw->work onto system_freezable_wq:

drivers/power/supply/cw2017_battery.c:cw2017_resume() {
	struct cw2017 *cw = dev_get_drvdata(dev);

	cw->poll_interval_ms = CW2017_POLL_INTERVAL_MS;
	queue_delayed_work(system_freezable_wq, &cw->work, 0);

	return 0;
}

Because cw2017_properties includes POWER_SUPPLY_PROP_TEMP and
cw->desc.no_thermal is false, devm_power_supply_register() registers a
thermal zone via psy_register_thermal() whose callback is
power_supply_read_temp().

During resume in suspend_devices_and_enter() -> dpm_resume_end(),
dpm_complete() calls thermal_pm_complete(), which queues
thermal_zone_device_resume() on the non-freezable thermal_wq before
suspend_finish() -> suspend_thaw_processes() -> thaw_workqueues() thaws
system_freezable_wq. When __thermal_zone_device_update() ->
power_supply_read_temp() -> cw2017_get_property() runs while cw->work is
still frozen or in flight, it sees cw->sample.error == -ENODATA, causing
thermal_zone_recheck() to log a temperature check failure (-61) and enter
error-recheck polling. Similarly, any PM_POST_SUSPEND notifier or thawed
userspace process reading battery attributes before cw2017_poll() finishes
its I2C transfers receives -ENODATA.

In addition, when cw2017_poll() completes after resume:

drivers/power/supply/cw2017_battery.c:cw2017_poll() {
	mutex_lock(&cw->lock);
	changed = !cw->sample.error != !sample.error ||
		  (!sample.error &&
		   (cw->sample.capacity != sample.capacity ||
		    cw->sample.voltage_uv != sample.voltage_uv ||
		    cw->sample.temp != sample.temp));
	cw->sample = sample;
	mutex_unlock(&cw->lock);

	if (changed)
		power_supply_changed(cw->psy);
}

Because cw2017_suspend() set cw->sample.error = -ENODATA, the expression
!cw->sample.error != !sample.error is always true when the post-resume poll
succeeds, triggering an unconditional power_supply_changed() notification on
every resume even if capacity, voltage_uv, and temp have not changed.

Should cw2017_suspend() keep the last sample intact until the post-resume
poll updates it, or refresh the sample synchronously during resume?

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/179329639684.489944.6322623410181934358.cw2017-first-public-rfc@yandex.ru?part=3

^ permalink raw reply	[flat|nested] 7+ messages in thread

* Re: [RFC PATCH 2/3] power: supply: add read-only CW2017 battery monitor
  2026-10-29 18:06   ` sashiko-bot
@ 2026-10-29 19:10     ` Kupreenko Danila
  0 siblings, 0 replies; 7+ messages in thread
From: Kupreenko Danila @ 2026-10-29 19:10 UTC (permalink / raw)
  To: sashiko-reviews; +Cc: devicetree, robh, conor+dt

Hi,

Thanks for the review. I reproduced the three effects: a valid unchanged
post-resume sample causes a spurious notification, a first post-resume I2C
error loses the availability notification, and an early thermal TEMP read
returns the synthetic -ENODATA before the poll can refresh the cache.

The proposed fix removes the sample.error assignment and its lock/unlock
pair from cw2017_suspend(). It keeps cancel_delayed_work_sync(), preserves
the last completed sample, and leaves the immediate resume poll unchanged.
That poll can then compare the actual pre/post-suspend states. Real read
errors still invalidate measurements and notify consumers.

This also addresses the comment on PATCH 3/3; no board DTS change is needed.

The regression cases pass with ASan/UBSan and ThreadSanitizer, including
TEMP reads before and during a blocked first post-resume poll. Threaded
workqueue/devres models exercise synchronous cancellation and teardown;
they are not a replacement for kernel runtime stress. ARM64 builds with
PM/thermal enabled, the no-PM object, and sparse also pass.

The early temperature remains a cached reading and can be stale until the
new poll completes. This removes the artificial error; it does not promise
fresh measurements during thermal resume or hide genuine unavailability.
No phone PM test was run, and the tested R4/R5 artifacts are unchanged.

I'll include this fix in v2 of the series, along with any additional changes requested during review.

Thanks,
Danila

^ permalink raw reply	[flat|nested] 7+ messages in thread

end of thread, other threads:[~2026-10-29 19:10 UTC | newest]

Thread overview: 7+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-29 17:53 [RFC PATCH 0/3] power: supply: read-only CellWise CW2017 monitor for Redmi Go Kupreenko Danila
2026-10-29 17:53 ` [RFC PATCH 1/3] dt-bindings: power: supply: add CellWise CW2017 fuel gauge Kupreenko Danila
2026-10-29 17:53 ` [RFC PATCH 3/3] arm64: dts: qcom: tiare: describe " Kupreenko Danila
2026-10-29 18:11   ` sashiko-bot
2026-10-29 17:53 ` [RFC PATCH 2/3] power: supply: add read-only CW2017 battery monitor Kupreenko Danila
2026-10-29 18:06   ` sashiko-bot
2026-10-29 19:10     ` Kupreenko Danila

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox