* [PATCH 0/3] Add Cellwise CW2217
@ 2026-10-27 13:17 Chris Morgan
2026-10-27 13:17 ` [PATCH 1/3] dt-bindings: power: cw2015: add cw2215, cw2217, and cw2218 Chris Morgan
` (2 more replies)
0 siblings, 3 replies; 7+ messages in thread
From: Chris Morgan @ 2026-10-27 13:17 UTC (permalink / raw)
To: linux-pm
Cc: devicetree, linux-rockchip, heiko, conor+dt, krzk+dt, robh, sre,
t.schramm, Chris Morgan
From: Chris Morgan <macromorgan@hotmail.com>
Add the Cellwise CW2217 battery gauge driver. This driver is based on
the sources used for Rockchip's Board Support package [1] but has
been updated to use the regmap API and devm helpers.
[1] https://github.com/rockchip-linux/kernel/blob/develop-6.12/drivers/power/supply/cw221x_battery.c
Chris Morgan (3):
dt-bindings: power: cw2015: add cw2215, cw2217, and cw2218
power: supply: cw2217: Add support for Cellwise cw2217
arm64: dts: rockchip: Add Battery Gauge to Anbernic Vita-Pro
.../bindings/power/supply/cw2015_battery.yaml | 42 +-
.../rockchip/rk3576-anbernic-rg-vita-pro.dts | 21 +-
drivers/power/supply/Kconfig | 11 +
drivers/power/supply/Makefile | 1 +
drivers/power/supply/cw2217_battery.c | 818 ++++++++++++++++++
5 files changed, 885 insertions(+), 8 deletions(-)
create mode 100644 drivers/power/supply/cw2217_battery.c
--
2.53.0
^ permalink raw reply [flat|nested] 7+ messages in thread
* [PATCH 1/3] dt-bindings: power: cw2015: add cw2215, cw2217, and cw2218
2026-10-27 13:17 [PATCH 0/3] Add Cellwise CW2217 Chris Morgan
@ 2026-10-27 13:17 ` Chris Morgan
2026-10-27 15:57 ` Krzysztof Kozlowski
2026-10-27 13:17 ` [PATCH 2/3] power: supply: cw2217: Add support for Cellwise cw2217 Chris Morgan
2026-10-27 13:17 ` [PATCH 3/3] arm64: dts: rockchip: Add Battery Gauge to Anbernic Vita-Pro Chris Morgan
2 siblings, 1 reply; 7+ messages in thread
From: Chris Morgan @ 2026-10-27 13:17 UTC (permalink / raw)
To: linux-pm
Cc: devicetree, linux-rockchip, heiko, conor+dt, krzk+dt, robh, sre,
t.schramm, Chris Morgan
From: Chris Morgan <macromorgan@hotmail.com>
Add bindings for the Cellwise cw2215/cw2217/cw2218 i2C interface
battery gauge. It is similar in function to the existing cw2015 but
with a different register layout. Each of the cw2215, cw2217 and cw2218
only really differ in the minor specs and the hardware ID they display
when queried. shunt-resistor-micro-ohms is a property in use for
multiple devices so not appending a vendor prefix to it.
Signed-off-by: Chris Morgan <macromorgan@hotmail.com>
---
.../bindings/power/supply/cw2015_battery.yaml | 42 +++++++++++++++----
1 file changed, 35 insertions(+), 7 deletions(-)
diff --git a/Documentation/devicetree/bindings/power/supply/cw2015_battery.yaml b/Documentation/devicetree/bindings/power/supply/cw2015_battery.yaml
index f7bde324153d..12d2d9a9cf6f 100644
--- a/Documentation/devicetree/bindings/power/supply/cw2015_battery.yaml
+++ b/Documentation/devicetree/bindings/power/supply/cw2015_battery.yaml
@@ -14,12 +14,13 @@ description: |
phandle in monitored-battery. If specified the driver uses the
charge-full-design-microamp-hours property of the battery.
-allOf:
- - $ref: power-supply.yaml#
-
properties:
compatible:
- const: cellwise,cw2015
+ enum:
+ - cellwise,cw2015
+ - cellwise,cw2215
+ - cellwise,cw2217
+ - cellwise,cw2218
reg:
maxItems: 1
@@ -31,8 +32,6 @@ properties:
it is to mail two batteries to a test facility of CellWise and receive
back a test report with the binary blob.
$ref: /schemas/types.yaml#/definitions/uint8-array
- minItems: 64
- maxItems: 64
cellwise,monitor-interval-ms:
description:
@@ -41,7 +40,14 @@ properties:
power-supplies:
minItems: 1
- maxItems: 8 # Should be enough
+ maxItems: 8 # Should be enough
+
+ shunt-resistor-micro-ohms:
+ description: |
+ The value, in micro-ohms, of the shunt resistor used for sampling
+ the battery current.
+ minimum: 1000000
+ maximum: 50000000
monitored-battery: true
@@ -49,6 +55,28 @@ required:
- compatible
- reg
+allOf:
+ - $ref: power-supply.yaml#
+ - if:
+ properties:
+ compatible:
+ contains:
+ enum:
+ - cellwise,cw2015
+ then:
+ properties:
+ shunt-resistor-micro-ohms: false
+
+ cellwise,battery-profile:
+ minItems: 64
+ maxItems: 64
+
+ else:
+ properties:
+ cellwise,battery-profile:
+ minItems: 80
+ maxItems: 80
+
additionalProperties: false
examples:
--
2.53.0
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH 2/3] power: supply: cw2217: Add support for Cellwise cw2217
2026-10-27 13:17 [PATCH 0/3] Add Cellwise CW2217 Chris Morgan
2026-10-27 13:17 ` [PATCH 1/3] dt-bindings: power: cw2015: add cw2215, cw2217, and cw2218 Chris Morgan
@ 2026-10-27 13:17 ` Chris Morgan
2026-10-27 13:34 ` Shawn Lin
2026-10-27 13:17 ` [PATCH 3/3] arm64: dts: rockchip: Add Battery Gauge to Anbernic Vita-Pro Chris Morgan
2 siblings, 1 reply; 7+ messages in thread
From: Chris Morgan @ 2026-10-27 13:17 UTC (permalink / raw)
To: linux-pm
Cc: devicetree, linux-rockchip, heiko, conor+dt, krzk+dt, robh, sre,
t.schramm, Chris Morgan
From: Chris Morgan <macromorgan@hotmail.com>
Add support for the CW2217 i2c battery gauge. The gauge driver was
adapted from the Rockchip BSP driver [1].
[1] https://github.com/rockchip-linux/kernel/blob/develop-6.12/drivers/power/supply/cw221x_battery.c
Signed-off-by: Chris Morgan <macromorgan@hotmail.com>
---
drivers/power/supply/Kconfig | 11 +
drivers/power/supply/Makefile | 1 +
drivers/power/supply/cw2217_battery.c | 818 ++++++++++++++++++++++++++
3 files changed, 830 insertions(+)
create mode 100644 drivers/power/supply/cw2217_battery.c
diff --git a/drivers/power/supply/Kconfig b/drivers/power/supply/Kconfig
index b89ef40df7f8..6a5235ec16c1 100644
--- a/drivers/power/supply/Kconfig
+++ b/drivers/power/supply/Kconfig
@@ -141,6 +141,17 @@ config BATTERY_CW2015
This driver can also be built as a module. If so, the module will be
called cw2015_battery.
+config BATTERY_CW2217
+ tristate "CW2217 Battery driver"
+ depends on I2C
+ select REGMAP_I2C
+ help
+ Say Y here to enable support for the cellwise cw2217
+ battery fuel gauge (used in the Anbernic RG Vita Pro & others)
+
+ This driver can also be built as a module. If so, the module will be
+ called cw2217_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 f3c0d1006c1a..66e6303888c7 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_CW2217) += cw2217_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/cw2217_battery.c b/drivers/power/supply/cw2217_battery.c
new file mode 100644
index 000000000000..8b18db3d54db
--- /dev/null
+++ b/drivers/power/supply/cw2217_battery.c
@@ -0,0 +1,818 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Chrager driver for cw2217
+ *
+ * Copyright (c) 2026 Rockchip Electronics Co., Ltd.
+ *
+ * Author: Xu Shengfei <xsf@rock-chips.com>
+ */
+#include <linux/bits.h>
+#include <linux/delay.h>
+#include <linux/devm-helpers.h>
+#include <linux/i2c.h>
+#include <linux/minmax.h>
+#include <linux/module.h>
+#include <linux/power_supply.h>
+#include <linux/regmap.h>
+#include <linux/workqueue.h>
+
+#include <asm/byteorder.h>
+
+#define CW2217_VERSION 0x00
+#define CW2217_CHIP_ID 0xA0
+
+#define CW2217_VCELL_H 0x02
+#define CW2217_VCELL_L 0x03
+
+#define CW2217_SOC_H 0x04
+#define CW2217_SOC_L 0x05
+
+#define CW2217_TEMP 0x06
+
+#define CW2217_CONFIG 0x08
+#define CW2217_CONFIG_ACT_MSK GENMASK(7, 4)
+#define CW2217_CONFIG_SLEEP (0x03 << 6)
+#define CW2217_CONFIG_RESTART (0x03 << 4)
+#define CW2217_CONFIG_ACTIVE (0x00)
+
+#define CW2217_INT_CONF 0x0A
+#define CW2217_EN_SOC_INT BIT(6)
+#define CW2217_EN_TMX_INT BIT(5)
+#define CW2217_EN_TMN_INT BIT(4)
+
+#define CW2217_SOC_ALERT 0x0B
+#define CW2217_SOC_UPDATE_FLG BIT(7)
+#define CW2217_SOC_ALERT_MSK GENMASK(6, 0)
+#define CW2217_SOC_ALERT_OFF 0x0
+
+#define CW2217_TEMP_MAX 0x0C
+
+#define CW2217_TEMP_MIN 0x0D
+
+#define CW2217_CURRENT_H 0x0E
+#define CW2217_CURRENT_L 0x0F
+
+#define CW2217_BAT_PROFILE 0x10
+#define CW2217_PROFILE_SZ 80
+
+#define CW2217_T_HOST_H 0xA0
+#define CW2217_T_HOST_L 0xA1
+
+#define CW2217_USER_CONF 0xA2
+
+#define CW2217_CYCLECNT_H 0xA4
+#define CW2217_CYCLECNT_L 0xA5
+
+#define CW2217_SOH 0xA6
+
+/* This register not documented in datasheet. */
+#define CW2217_IC_STATE 0xA7
+#define CW2217_IC_READY_MASK 0x0C
+#define CW2217_IC_READY 3
+
+#define CW2217_STB_CUR_H 0xA8
+#define CW2217_STB_CUR_L 0xA9
+
+#define CW2217_FW_VERSION 0xAB
+#define CW2217_ID_MASK GENMASK(7, 6)
+#define CW2215_ID 2
+#define CW2217_ID 1
+#define CW2218_ID 0
+
+/* Default values from BSP driver */
+#define CW2217_USER_RSENSE 1500
+#define CW2217_DEFAULT_POLL_INTERVAL_MS 5000
+
+struct cw2217_battery {
+ struct device *dev;
+ struct regmap *regmap;
+ struct power_supply *cw_bat;
+ u8 *bat_profile;
+ struct workqueue_struct *battery_workqueue;
+ struct delayed_work battery_delay_work;
+ int user_rsense;
+ int chip_id;
+ int voltage;
+ int soc;
+ int temp;
+ long cw_current;
+ int cycle;
+ int soh;
+ int fw_version;
+ u32 poll_interval_ms;
+ bool update_profile;
+};
+
+/*
+ * The CONFIG register is used for the host MCU to configure the fuel gauge IC.
+ * The default value is 0xF0, SLEEP and RESTART bits are set. To power up the
+ * IC, the host MCU needs to write 0x30 to exit shutdown mode, and then write
+ * 0x00 to restart the gauge to enter active mode. To reset the IC, the host
+ * MCU needs to write 0xF0, 0x30 and 0x00 in sequence to this register to
+ * complete the restart procedure. The CW2217 will reload relevant parameters
+ * and settings and restart SOC calculation. Note that the SOC may be a
+ * different value after reset operation since it is a brand-new calculation
+ * based on the latest battery status. The BSP driver slept 20ms after restart
+ * and 10ms after setting active, though no delays are documented in the
+ * datasheet.
+ */
+static int cw2217_active(struct cw2217_battery *cw_bat, bool active)
+{
+ int ret;
+
+ ret = regmap_write(cw_bat->regmap, CW2217_CONFIG,
+ CW2217_CONFIG_RESTART);
+ if (ret)
+ return ret;
+
+ fsleep(20000);
+
+ if (active) {
+ ret = regmap_write(cw_bat->regmap, CW2217_CONFIG,
+ CW2217_CONFIG_ACTIVE);
+ if (ret)
+ return ret;
+ } else {
+ ret = regmap_write(cw_bat->regmap, CW2217_CONFIG,
+ CW2217_CONFIG_SLEEP | CW2217_CONFIG_RESTART);
+ if (ret)
+ return ret;
+ }
+
+ fsleep(10000);
+
+ return 0;
+}
+
+/*
+ * The 0x00 register is an UNSIGNED 8bit read-only register. Its value is
+ * fixed to 0xA0 in shutdown mode and active mode.
+ */
+static int cw2217_get_chip_id(struct cw2217_battery *cw_bat)
+{
+ u32 reg;
+ int ret;
+
+ ret = regmap_read(cw_bat->regmap, CW2217_VERSION, ®);
+ if (ret)
+ return ret;
+
+ cw_bat->chip_id = reg;
+
+ return 0;
+}
+
+/*
+ * The VCELL registers (0x02 0x03) are an UNSIGNED 14bit read-only register
+ * that updates the battery voltage continuously. Battery voltage is measured
+ * between the VCELL pin and VSS pin, which is the ground reference. A 14bit
+ * sigma-delta A/D converter is used and the voltage resolution is 312.5uV
+ * (0.3125mV is * 5/16).
+ */
+static int cw2217_get_voltage(struct cw2217_battery *cw_bat)
+{
+ __be16 reg;
+ int ret;
+
+ ret = regmap_bulk_read(cw_bat->regmap, CW2217_VCELL_H,
+ ®, sizeof(reg));
+ if (ret)
+ return ret;
+
+ cw_bat->voltage = ((be16_to_cpu(reg) * 5) / 16) * 1000;
+
+ return 0;
+}
+
+/*
+ * The SOC registers (0x04 0x05) are an UNSIGNED 16bit read-only register that
+ * indicates the SOC of the battery. The SOC shows in % format, which means
+ * how much percent of the battery's total available capacity is remaining in
+ * the battery now. The SOC can intrinsically adjust itself to cater to the
+ * change of battery status; including load, temperature, and aging.
+ * The high byte(0x04) contains the SOC in 1% unit which can be directly used
+ * if this resolution is good enough for the application. The low byte (0x05)
+ * provides more accurate fractional part of the SOC and its LSB is (1/256) %.
+ * For now, we are only interested in the high byte.
+ */
+static int cw2217_get_capacity(struct cw2217_battery *cw_bat)
+{
+ u32 reg;
+ int ret;
+
+ ret = regmap_read(cw_bat->regmap, CW2217_SOC_H, ®);
+ if (ret)
+ return ret;
+
+ cw_bat->soc = clamp(reg, 0, 100);
+
+ return 0;
+}
+
+/*
+ * The TEMP register is an UNSIGNED 8bit read only register.
+ * It reports the real-time battery temperature
+ * measured at TS pin. The scope is from -40 to 87.5 degrees Celsius,
+ * LSB is 0.5 degree Celsius. TEMP(C) = - 40 + Value(0x06 Reg) / 2
+ */
+static int cw2217_get_temp(struct cw2217_battery *cw_bat)
+{
+ u32 reg;
+ int ret;
+
+ ret = regmap_read(cw_bat->regmap, CW2217_TEMP, ®);
+ if (ret)
+ return ret;
+
+ cw_bat->temp = ((int)reg / 2) - 40;
+
+ return 0;
+}
+
+/*
+ * Current is a signed 16bit register (0x0E 0x0F) that reports current A/D
+ * converter result of the voltage across the current sense resistor,
+ * 10mohm typical. The register value should be divided by the sense
+ * resistance to convert to amperes. The value of the sense resistor
+ * determines the resolution and the full-scale range of the current readings.
+ * The LSB of 0x0F is (52.4/32768)uV for CW2215 and CW2217. The LSB of 0x0F is
+ * (125/32768)uV for CW2218.
+ */
+static int cw2217_get_current(struct cw2217_battery *cw_bat)
+{
+ s32 cw_current;
+ __be16 reg;
+ u8 fw_ver;
+ int ret;
+
+ if (cw_bat->fw_version == 0) {
+ cw_bat->cw_current = 0;
+ dev_err(cw_bat->dev, "Error! CW2217 firmware read error!\n");
+ return -EIO;
+ }
+
+ ret = regmap_bulk_read(cw_bat->regmap, CW2217_CURRENT_H,
+ ®, sizeof(reg));
+ if (ret)
+ return ret;
+
+ cw_current = (s16)be16_to_cpu(reg);
+
+ fw_ver = FIELD_GET(CW2217_ID_MASK, cw_bat->fw_version);
+ switch (fw_ver) {
+ case CW2215_ID:
+ case CW2217_ID:
+ cw_current = (cw_current * 1600) / (cw_bat->user_rsense / 1000);
+ cw_bat->cw_current = cw_current * 1000;
+ break;
+ case CW2218_ID:
+ cw_current = (cw_current * 3815) / (cw_bat->user_rsense / 1000);
+ cw_bat->cw_current = cw_current * 1000;
+ break;
+ default:
+ cw_bat->cw_current = 0;
+ dev_err(cw_bat->dev, "Error! CW2217 firmware read error!\n");
+ return -EIO;
+ }
+
+ return 0;
+}
+
+/*
+ * CYCLECNT is an UNSIGNED 16bit register (0xA4 0xA5) that counts cycle life
+ * of the battery. The LSB of 0xA5 stands for 1/16 cycle. This register will
+ * be cleared after entering shutdown mode.
+ */
+static int cw2217_get_cycle_count(struct cw2217_battery *cw_bat)
+{
+ __be16 reg;
+ int ret;
+
+ ret = regmap_bulk_read(cw_bat->regmap, CW2217_CYCLECNT_H,
+ ®, sizeof(reg));
+ if (ret)
+ return ret;
+
+ cw_bat->cycle = be16_to_cpu(reg) / 16;
+
+ return 0;
+}
+
+/*
+ * SOH (State of Health) is an UNSIGNED 8bit register(0xA6) that represents the level of
+ * battery aging by tracking battery internal impedance increment. When the device enters
+ * active mode, this register refresh to 0x64 by default. Its range is 0x00 to 0x64,
+ * indicating 0 to 100%. This register will be clear after enters shutdown mode.
+ */
+static int cw2217_get_soh(struct cw2217_battery *cw_bat)
+{
+ u32 reg;
+ int ret;
+
+ ret = regmap_read(cw_bat->regmap, CW2217_SOH, ®);
+ if (ret)
+ return ret;
+
+ cw_bat->soh = clamp(reg, 0, 100);
+
+ return 0;
+}
+
+/*
+ * FW_VERSION register reports the firmware (FW) running in the chip. It is
+ * fixed to 0x00 when the chip is in shutdown mode. When in active mode,
+ * Bit [7:6] = '01' stand for the CW2217, Bit [7:6] = '00' stand for the
+ * CW2218 and Bit [7:6] = '10' stand for CW2215. Bit[5:0] stand for the FW
+ * version running in the chip. Note that the FW version is subject to update.
+ */
+static int cw2217_get_fw_version(struct cw2217_battery *cw_bat)
+{
+ u32 reg;
+ int ret;
+
+ ret = regmap_read(cw_bat->regmap, CW2217_FW_VERSION, ®);
+ if (ret)
+ return ret;
+
+ cw_bat->fw_version = reg;
+
+ return 0;
+}
+
+static int cw2217_update_data(struct cw2217_battery *cw_bat)
+{
+ int ret;
+
+ ret = cw2217_get_voltage(cw_bat);
+ if (ret)
+ return ret;
+
+ ret = cw2217_get_capacity(cw_bat);
+ if (ret)
+ return ret;
+
+ ret = cw2217_get_temp(cw_bat);
+ if (ret)
+ return ret;
+
+ ret = cw2217_get_current(cw_bat);
+ if (ret)
+ return ret;
+
+ ret = cw2217_get_cycle_count(cw_bat);
+ if (ret)
+ return ret;
+
+ return cw2217_get_soh(cw_bat);
+}
+
+static int cw2217_parse_properties(struct cw2217_battery *cw_bat)
+{
+ struct device *dev = cw_bat->dev;
+ int length;
+ int ret;
+
+ ret = device_property_read_u32(dev, "cellwise,monitor-interval-ms",
+ &cw_bat->poll_interval_ms);
+ if (ret) {
+ dev_dbg(cw_bat->dev, "Using default poll interval\n");
+ cw_bat->poll_interval_ms = CW2217_DEFAULT_POLL_INTERVAL_MS;
+ }
+
+ /*
+ * Datasheet only lists options of between 1M and 50M for resistor
+ * values, so assuming that is all and clamping if out of range.
+ */
+ ret = device_property_read_u32(dev,
+ "shunt-resistor-micro-ohms",
+ &cw_bat->user_rsense);
+ if (ret) {
+ cw_bat->user_rsense = CW2217_USER_RSENSE;
+ dev_warn(cw_bat->dev,
+ "Using default resistor value, reported mA may be wrong\n");
+ }
+
+ /* Clamp, but also warn we're clamping due to out of range. */
+ if ((cw_bat->user_rsense < 1000000) || (cw_bat->user_rsense > 50000000)) {
+ cw_bat->user_rsense = clamp(cw_bat->user_rsense, 1000000, 50000000);
+ dev_warn(cw_bat->dev, "Shunt resistor value out of range, setting to %d",
+ cw_bat->user_rsense);
+ }
+
+ length = device_property_count_u8(dev, "cellwise,battery-profile");
+ if (length < 0) {
+ dev_warn(cw_bat->dev,
+ "No battery-profile found, using current flash contents\n");
+ } else if (length != CW2217_PROFILE_SZ) {
+ dev_err(cw_bat->dev, "battery-profile must be %d bytes\n",
+ CW2217_PROFILE_SZ);
+ return -EINVAL;
+ } else {
+ cw_bat->bat_profile = devm_kzalloc(dev, length, GFP_KERNEL);
+ if (!cw_bat->bat_profile)
+ return -ENOMEM;
+
+ ret = device_property_read_u8_array(dev,
+ "cellwise,battery-profile",
+ cw_bat->bat_profile,
+ length);
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+
+/* Load a new battery profile provided from the device tree. */
+static int cw2217_update_config(struct cw2217_battery *cw_bat)
+{
+ u32 reg;
+ int ret;
+
+ /* Set the battery monitor state to shutdown. */
+ ret = cw2217_active(cw_bat, false);
+ if (ret)
+ return ret;
+
+ /* update new battery info */
+ ret = regmap_raw_write(cw_bat->regmap, CW2217_BAT_PROFILE,
+ cw_bat->bat_profile, CW2217_PROFILE_SZ);
+ if (ret)
+ return ret;
+
+ /* Set update flag for new profile and disable SOC alert. */
+ ret = regmap_write(cw_bat->regmap, CW2217_SOC_ALERT,
+ CW2217_SOC_UPDATE_FLG | CW2217_SOC_ALERT_OFF);
+ if (ret)
+ return ret;
+
+ /* mask all interrupts */
+ ret = regmap_clear_bits(cw_bat->regmap, CW2217_INT_CONF,
+ CW2217_EN_SOC_INT | CW2217_EN_TMX_INT | CW2217_EN_TMN_INT);
+ if (ret)
+ return ret;
+
+ ret = cw2217_active(cw_bat, true);
+ if (ret)
+ return ret;
+
+ /*
+ * Check the undocumented register for bits 2 and 3 to be 1. Note that
+ * the BSP driver would sleep 100ms after every iteration and retry up
+ * to 50 times, this felt excessive so reducing to 10.
+ */
+ ret = regmap_read_poll_timeout(cw_bat->regmap, CW2217_IC_STATE, reg,
+ FIELD_GET(CW2217_IC_READY_MASK, reg) == CW2217_IC_READY,
+ 100 * USEC_PER_MSEC, 10 * USEC_PER_SEC);
+ if (ret) {
+ dev_err(cw_bat->dev,
+ "Gauge did not become ready after profile upload\n");
+ cw2217_active(cw_bat, false);
+ return ret;
+ }
+
+ return 0;
+}
+
+/* Determine whether the battery profile needs to be updated. */
+static int cw2217_check_profile(struct cw2217_battery *cw_bat)
+{
+ u8 bat_info[CW2217_PROFILE_SZ];
+ int ret;
+ u32 reg;
+
+ cw_bat->update_profile = false;
+
+ ret = regmap_read(cw_bat->regmap, CW2217_CONFIG, ®);
+ if (ret)
+ return ret;
+
+ /* Active is 0x00, anything else is inactive */
+ if (FIELD_GET(CW2217_CONFIG_ACT_MSK, reg)) {
+ cw_bat->update_profile = true;
+ return 0;
+ }
+
+ ret = regmap_read(cw_bat->regmap, CW2217_SOC_ALERT, ®);
+ if (ret)
+ return ret;
+
+ if (FIELD_GET(CW2217_SOC_UPDATE_FLG, reg) == 0) {
+ cw_bat->update_profile = true;
+ return 0;
+ }
+
+ ret = regmap_raw_read(cw_bat->regmap, CW2217_BAT_PROFILE, bat_info, CW2217_PROFILE_SZ);
+ if (ret) {
+ dev_err(cw_bat->dev, "Failed to read existing battery profile\n");
+ return ret;
+ }
+
+ if (memcmp(bat_info, cw_bat->bat_profile, CW2217_PROFILE_SZ)) {
+ dev_warn(cw_bat->dev, "Replacing stored battery profile\n");
+ cw_bat->update_profile = true;
+ }
+
+ return 0;
+}
+
+static int cw2217_init(struct cw2217_battery *cw_bat)
+{
+ int ret;
+
+ ret = cw2217_get_chip_id(cw_bat);
+ if (ret)
+ return dev_err_probe(cw_bat->dev, ret, "I2C read write error");
+
+ /*
+ * All of the known Cellwise 221x chips set the ID bits to 0xa0. If it
+ * doesn't match, we're trying to probe the wrong hardware.
+ */
+ if (cw_bat->chip_id != CW2217_CHIP_ID)
+ return dev_err_probe(cw_bat->dev, -ENODEV, "Not a cw221x\n");
+
+ ret = cw2217_check_profile(cw_bat);
+ if (ret) {
+ dev_err(cw_bat->dev, "i2c read write error");
+ return ret;
+ }
+
+ if (cw_bat->update_profile) {
+ ret = cw2217_update_config(cw_bat);
+ if (ret)
+ return ret;
+ }
+
+ return 0;
+}
+
+static void cw2217_bat_work(struct work_struct *work)
+{
+ struct delayed_work *delay_work;
+ struct cw2217_battery *cw_bat;
+ int ret;
+
+ delay_work = container_of(work,
+ struct delayed_work,
+ work);
+
+ cw_bat = container_of(delay_work,
+ struct cw2217_battery,
+ battery_delay_work);
+
+ ret = cw2217_update_data(cw_bat);
+ if (ret)
+ dev_err(cw_bat->dev, "i2c read error when update data");
+
+ queue_delayed_work(cw_bat->battery_workqueue,
+ &cw_bat->battery_delay_work,
+ msecs_to_jiffies(cw_bat->poll_interval_ms));
+}
+
+static int cw2217_battery_get_property(struct power_supply *psy,
+ enum power_supply_property psp,
+ union power_supply_propval *val)
+{
+ struct cw2217_battery *cw_bat = power_supply_get_drvdata(psy);
+ int ret = 0;
+
+ switch (psp) {
+ case POWER_SUPPLY_PROP_CYCLE_COUNT:
+ val->intval = cw_bat->cycle;
+ break;
+ case POWER_SUPPLY_PROP_CAPACITY:
+ val->intval = cw_bat->soc;
+ break;
+ case POWER_SUPPLY_PROP_STATUS:
+ /*
+ * Observed when battery was full that it would report
+ * approximately -12mv at between 97-99 SoC. Assume that if the
+ * battery current is above 0 it's charging, it's below
+ * 0 but with an SoC 95% or above it's not charging because
+ * it's full, otherwise it's simply not charging. If it's not
+ * supplied it is discharging.
+ */
+ if (power_supply_is_system_supplied()) {
+ if (cw_bat->cw_current > 0)
+ val->intval = POWER_SUPPLY_STATUS_CHARGING;
+ else if (cw_bat->soc >= 95)
+ val->intval = POWER_SUPPLY_STATUS_FULL;
+ else
+ val->intval = POWER_SUPPLY_STATUS_NOT_CHARGING;
+ } else
+ val->intval = POWER_SUPPLY_STATUS_DISCHARGING;
+ break;
+ case POWER_SUPPLY_PROP_PRESENT:
+ val->intval = (cw_bat->voltage <= 0) ? 0 : 1;
+ break;
+ case POWER_SUPPLY_PROP_VOLTAGE_NOW:
+ val->intval = cw_bat->voltage;
+ break;
+ case POWER_SUPPLY_PROP_CURRENT_NOW:
+ val->intval = cw_bat->cw_current;
+ break;
+ case POWER_SUPPLY_PROP_STATE_OF_HEALTH:
+ val->intval = cw_bat->soh;
+ break;
+ case POWER_SUPPLY_PROP_TEMP:
+ val->intval = cw_bat->temp;
+ break;
+ default:
+ ret = -EINVAL;
+ break;
+ }
+
+ return ret;
+}
+
+static enum power_supply_property cw2217_battery_properties[] = {
+ POWER_SUPPLY_PROP_CYCLE_COUNT,
+ POWER_SUPPLY_PROP_CAPACITY,
+ POWER_SUPPLY_PROP_PRESENT,
+ POWER_SUPPLY_PROP_VOLTAGE_NOW,
+ POWER_SUPPLY_PROP_CURRENT_NOW,
+ POWER_SUPPLY_PROP_STATUS,
+ POWER_SUPPLY_PROP_STATE_OF_HEALTH,
+ POWER_SUPPLY_PROP_TEMP,
+};
+
+static const struct power_supply_desc cw2217_bat_desc = {
+ .name = "cw2217-battery",
+ .type = POWER_SUPPLY_TYPE_BATTERY,
+ .properties = cw2217_battery_properties,
+ .num_properties = ARRAY_SIZE(cw2217_battery_properties),
+ .get_property = cw2217_battery_get_property,
+};
+
+static const struct regmap_range cw2217_ranges_rd_yes[] = {
+ regmap_reg_range(CW2217_VERSION, CW2217_VERSION),
+ regmap_reg_range(CW2217_VCELL_H, CW2217_TEMP),
+ regmap_reg_range(CW2217_CONFIG, CW2217_CONFIG),
+ regmap_reg_range(CW2217_INT_CONF, CW2217_CURRENT_L),
+ regmap_reg_range(CW2217_BAT_PROFILE,
+ CW2217_BAT_PROFILE + CW2217_PROFILE_SZ - 1),
+ regmap_reg_range(CW2217_T_HOST_H, CW2217_USER_CONF),
+ regmap_reg_range(CW2217_CYCLECNT_H, CW2217_STB_CUR_L),
+ regmap_reg_range(CW2217_FW_VERSION, CW2217_FW_VERSION),
+};
+
+static const struct regmap_access_table cw2217_rd_table = {
+ .yes_ranges = cw2217_ranges_rd_yes,
+ .n_yes_ranges = ARRAY_SIZE(cw2217_ranges_rd_yes),
+};
+
+static const struct regmap_range cw2217_ranges_wr_yes[] = {
+ regmap_reg_range(CW2217_TEMP, CW2217_TEMP),
+ regmap_reg_range(CW2217_CONFIG, CW2217_CONFIG),
+ regmap_reg_range(CW2217_INT_CONF, CW2217_CURRENT_L),
+ regmap_reg_range(CW2217_BAT_PROFILE,
+ CW2217_BAT_PROFILE + CW2217_PROFILE_SZ - 1),
+ regmap_reg_range(CW2217_T_HOST_H, CW2217_USER_CONF),
+};
+
+static const struct regmap_access_table cw2217_wr_table = {
+ .yes_ranges = cw2217_ranges_wr_yes,
+ .n_yes_ranges = ARRAY_SIZE(cw2217_ranges_wr_yes),
+};
+
+static const struct regmap_range cw2217_ranges_vol_yes[] = {
+ regmap_reg_range(CW2217_VCELL_H, CW2217_TEMP),
+ regmap_reg_range(CW2217_INT_CONF, CW2217_SOC_ALERT),
+ regmap_reg_range(CW2217_CURRENT_H, CW2217_CURRENT_L),
+ regmap_reg_range(CW2217_CYCLECNT_H, CW2217_STB_CUR_L),
+};
+
+static const struct regmap_access_table cw2217_vol_table = {
+ .yes_ranges = cw2217_ranges_vol_yes,
+ .n_yes_ranges = ARRAY_SIZE(cw2217_ranges_vol_yes),
+};
+
+static const struct regmap_config cw2217_regmap_config = {
+ .reg_bits = 8,
+ .val_bits = 8,
+ .rd_table = &cw2217_rd_table,
+ .wr_table = &cw2217_wr_table,
+ .volatile_table = &cw2217_vol_table,
+ .cache_type = REGCACHE_MAPLE,
+ .max_register = CW2217_FW_VERSION,
+};
+
+static int cw2217_probe(struct i2c_client *client)
+{
+ struct power_supply_config psy_cfg = { 0 };
+ struct cw2217_battery *cw_bat;
+ int ret;
+
+ cw_bat = devm_kzalloc(&client->dev, sizeof(*cw_bat), GFP_KERNEL);
+ if (!cw_bat)
+ return -ENOMEM;
+
+ i2c_set_clientdata(client, cw_bat);
+ cw_bat->dev = &client->dev;
+
+ cw_bat->regmap = devm_regmap_init_i2c(client, &cw2217_regmap_config);
+ if (IS_ERR(cw_bat->regmap))
+ return dev_err_probe(cw_bat->dev, PTR_ERR(cw_bat->regmap),
+ "Failed to allocate regmap: %ld\n",
+ PTR_ERR(cw_bat->regmap));
+
+ ret = cw2217_parse_properties(cw_bat);
+ if (ret)
+ return ret;
+
+ ret = cw2217_init(cw_bat);
+ if (ret)
+ return dev_err_probe(cw_bat->dev, ret, "cw2217 init fail\n");
+
+ ret = cw2217_get_fw_version(cw_bat);
+ if (ret)
+ return dev_err_probe(cw_bat->dev, ret,
+ "Unable to read fw version\n");
+
+ ret = cw2217_update_data(cw_bat);
+ if (ret)
+ return dev_err_probe(cw_bat->dev, ret,
+ "Unable to read initial data\n");
+
+ psy_cfg.drv_data = cw_bat;
+ cw_bat->cw_bat = devm_power_supply_register(&client->dev,
+ &cw2217_bat_desc, &psy_cfg);
+ if (IS_ERR(cw_bat->cw_bat))
+ return dev_err_probe(cw_bat->dev, PTR_ERR(cw_bat->cw_bat),
+ "Failed to register battery: %ld\n",
+ PTR_ERR(cw_bat->cw_bat));
+
+ cw_bat->battery_workqueue = devm_alloc_ordered_workqueue(&client->dev,
+ "cw2217_gauge", 0);
+ if (!cw_bat->battery_workqueue)
+ return -ENOMEM;
+
+ ret = devm_delayed_work_autocancel(&client->dev,
+ &cw_bat->battery_delay_work,
+ cw2217_bat_work);
+ if (ret)
+ return dev_err_probe(&client->dev, ret,
+ "Failed to register delayed work\n");
+
+ queue_delayed_work(cw_bat->battery_workqueue,
+ &cw_bat->battery_delay_work,
+ msecs_to_jiffies(50));
+
+ return 0;
+}
+
+static int __maybe_unused cw2217_bat_suspend(struct device *dev)
+{
+ struct i2c_client *client = to_i2c_client(dev);
+ struct cw2217_battery *cw_bat = i2c_get_clientdata(client);
+
+ cancel_delayed_work(&cw_bat->battery_delay_work);
+ return 0;
+}
+
+static int __maybe_unused cw2217_bat_resume(struct device *dev)
+{
+ struct i2c_client *client = to_i2c_client(dev);
+ struct cw2217_battery *cw_bat = i2c_get_clientdata(client);
+
+ queue_delayed_work(cw_bat->battery_workqueue,
+ &cw_bat->battery_delay_work,
+ msecs_to_jiffies(20));
+ return 0;
+}
+
+static SIMPLE_DEV_PM_OPS(cw2217_bat_pm_ops, cw2217_bat_suspend,
+ cw2217_bat_resume);
+
+static const struct i2c_device_id cw2217_id_table[] = {
+ { .name = "cw2215" },
+ { .name = "cw2217" },
+ { .name = "cw2218" },
+ { }
+};
+MODULE_DEVICE_TABLE(i2c, cw2217_id_table);
+
+static const struct of_device_id cw2217_match_table[] = {
+ { .compatible = "cellwise,cw2215", },
+ { .compatible = "cellwise,cw2217", },
+ { .compatible = "cellwise,cw2218", },
+ { },
+};
+MODULE_DEVICE_TABLE(of, cw2217_match_table);
+
+static struct i2c_driver cw2217_driver = {
+ .driver = {
+ .name = "cw2217",
+ .pm = &cw2217_bat_pm_ops,
+ .of_match_table = of_match_ptr(cw2217_match_table),
+ },
+ .probe = cw2217_probe,
+ .id_table = cw2217_id_table,
+};
+
+module_i2c_driver(cw2217_driver);
+
+MODULE_AUTHOR("Xu Shengfei <xsf@rock-chips.com>");
+MODULE_AUTHOR("Chris Morgan <macromorgan@hotmail.com>");
+MODULE_DESCRIPTION("cw2217 battery driver");
+MODULE_LICENSE("GPL");
--
2.53.0
^ permalink raw reply related [flat|nested] 7+ messages in thread
* [PATCH 3/3] arm64: dts: rockchip: Add Battery Gauge to Anbernic Vita-Pro
2026-10-27 13:17 [PATCH 0/3] Add Cellwise CW2217 Chris Morgan
2026-10-27 13:17 ` [PATCH 1/3] dt-bindings: power: cw2015: add cw2215, cw2217, and cw2218 Chris Morgan
2026-10-27 13:17 ` [PATCH 2/3] power: supply: cw2217: Add support for Cellwise cw2217 Chris Morgan
@ 2026-10-27 13:17 ` Chris Morgan
2 siblings, 0 replies; 7+ messages in thread
From: Chris Morgan @ 2026-10-27 13:17 UTC (permalink / raw)
To: linux-pm
Cc: devicetree, linux-rockchip, heiko, conor+dt, krzk+dt, robh, sre,
t.schramm, Chris Morgan
From: Chris Morgan <macromorgan@hotmail.com>
Add support for the Cellwise cw2217 battery gauge to the Anbernic
Vita-Pro.
Signed-off-by: Chris Morgan <macromorgan@hotmail.com>
---
.../rockchip/rk3576-anbernic-rg-vita-pro.dts | 21 ++++++++++++++++++-
1 file changed, 20 insertions(+), 1 deletion(-)
diff --git a/arch/arm64/boot/dts/rockchip/rk3576-anbernic-rg-vita-pro.dts b/arch/arm64/boot/dts/rockchip/rk3576-anbernic-rg-vita-pro.dts
index e66803b5399c..5c61467eaa03 100644
--- a/arch/arm64/boot/dts/rockchip/rk3576-anbernic-rg-vita-pro.dts
+++ b/arch/arm64/boot/dts/rockchip/rk3576-anbernic-rg-vita-pro.dts
@@ -954,7 +954,26 @@ hym8563: rtc@51 {
wakeup-source;
};
- /* cellwise,cw221X battery manager at 0x64 */
+ cw2217: charger@64 {
+ compatible = "cellwise,cw2217";
+ reg = <0x64>;
+ cellwise,battery-profile = /bits/ 8 <
+ 0x5a 0x00 0x00 0x00 0x00 0x00 0x00 0x00
+ 0xab 0xb5 0xae 0xc0 0xa9 0xa4 0xcf 0xaf
+ 0x96 0xff 0xff 0xd7 0x9a 0x83 0x70 0x69
+ 0x65 0x65 0x65 0xb8 0x81 0xd9 0xaf 0xd9
+ 0xd2 0xce 0xcd 0xcb 0xca 0xc7 0xc8 0xc5
+ 0xc2 0xba 0xa8 0x96 0x8a 0x85 0x7d 0x7a
+ 0x7f 0x88 0x92 0x93 0x94 0xd3 0xff 0x5f
+ 0x20 0x00 0xab 0x10 0x00 0x91 0x68 0x00
+ 0x00 0x00 0x64 0x3c 0xa0 0x89 0x00 0x00
+ 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0xb2
+ >;
+ monitored-battery = <&battery>;
+ power-supplies = <&sgm41542>;
+ shunt-resistor-micro-ohms = <2000000>;
+ status = "okay";
+ };
};
&mipidcphy {
--
2.53.0
^ permalink raw reply related [flat|nested] 7+ messages in thread
* Re: [PATCH 2/3] power: supply: cw2217: Add support for Cellwise cw2217
2026-10-27 13:17 ` [PATCH 2/3] power: supply: cw2217: Add support for Cellwise cw2217 Chris Morgan
@ 2026-10-27 13:34 ` Shawn Lin
2026-10-27 15:46 ` Chris Morgan
0 siblings, 1 reply; 7+ messages in thread
From: Shawn Lin @ 2026-10-27 13:34 UTC (permalink / raw)
To: Chris Morgan, linux-pm
Cc: shawn.lin, devicetree, linux-rockchip, heiko, conor+dt, krzk+dt,
robh, sre, t.schramm, Chris Morgan, Xu Shengfei
Cc: Shengfei
在 2026/10/27 星期二 21:17, Chris Morgan 写道:
> From: Chris Morgan <macromorgan@hotmail.com>
>
> Add support for the CW2217 i2c battery gauge. The gauge driver was
> adapted from the Rockchip BSP driver [1].
>
I don't look into it to evaluate how much it differ from Shengfei's
driver. If not too much, he deserves a credit here. I CCed him to
look more into your submission version.
> [1] https://github.com/rockchip-linux/kernel/blob/develop-6.12/drivers/power/supply/cw221x_battery.c
>
> Signed-off-by: Chris Morgan <macromorgan@hotmail.com>
> ---
> drivers/power/supply/Kconfig | 11 +
> drivers/power/supply/Makefile | 1 +
> drivers/power/supply/cw2217_battery.c | 818 ++++++++++++++++++++++++++
> 3 files changed, 830 insertions(+)
> create mode 100644 drivers/power/supply/cw2217_battery.c
>
> diff --git a/drivers/power/supply/Kconfig b/drivers/power/supply/Kconfig
> index b89ef40df7f8..6a5235ec16c1 100644
> --- a/drivers/power/supply/Kconfig
> +++ b/drivers/power/supply/Kconfig
> @@ -141,6 +141,17 @@ config BATTERY_CW2015
> This driver can also be built as a module. If so, the module will be
> called cw2015_battery.
>
> +config BATTERY_CW2217
> + tristate "CW2217 Battery driver"
> + depends on I2C
> + select REGMAP_I2C
> + help
> + Say Y here to enable support for the cellwise cw2217
> + battery fuel gauge (used in the Anbernic RG Vita Pro & others)
> +
> + This driver can also be built as a module. If so, the module will be
> + called cw2217_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 f3c0d1006c1a..66e6303888c7 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_CW2217) += cw2217_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/cw2217_battery.c b/drivers/power/supply/cw2217_battery.c
> new file mode 100644
> index 000000000000..8b18db3d54db
> --- /dev/null
> +++ b/drivers/power/supply/cw2217_battery.c
> @@ -0,0 +1,818 @@
> +// SPDX-License-Identifier: GPL-2.0
> +/*
> + * Chrager driver for cw2217
> + *
> + * Copyright (c) 2026 Rockchip Electronics Co., Ltd.
> + *
> + * Author: Xu Shengfei <xsf@rock-chips.com>
> + */
> +#include <linux/bits.h>
> +#include <linux/delay.h>
> +#include <linux/devm-helpers.h>
> +#include <linux/i2c.h>
> +#include <linux/minmax.h>
> +#include <linux/module.h>
> +#include <linux/power_supply.h>
> +#include <linux/regmap.h>
> +#include <linux/workqueue.h>
> +
> +#include <asm/byteorder.h>
> +
> +#define CW2217_VERSION 0x00
> +#define CW2217_CHIP_ID 0xA0
> +
> +#define CW2217_VCELL_H 0x02
> +#define CW2217_VCELL_L 0x03
> +
> +#define CW2217_SOC_H 0x04
> +#define CW2217_SOC_L 0x05
> +
> +#define CW2217_TEMP 0x06
> +
> +#define CW2217_CONFIG 0x08
> +#define CW2217_CONFIG_ACT_MSK GENMASK(7, 4)
> +#define CW2217_CONFIG_SLEEP (0x03 << 6)
> +#define CW2217_CONFIG_RESTART (0x03 << 4)
> +#define CW2217_CONFIG_ACTIVE (0x00)
> +
> +#define CW2217_INT_CONF 0x0A
> +#define CW2217_EN_SOC_INT BIT(6)
> +#define CW2217_EN_TMX_INT BIT(5)
> +#define CW2217_EN_TMN_INT BIT(4)
> +
> +#define CW2217_SOC_ALERT 0x0B
> +#define CW2217_SOC_UPDATE_FLG BIT(7)
> +#define CW2217_SOC_ALERT_MSK GENMASK(6, 0)
> +#define CW2217_SOC_ALERT_OFF 0x0
> +
> +#define CW2217_TEMP_MAX 0x0C
> +
> +#define CW2217_TEMP_MIN 0x0D
> +
> +#define CW2217_CURRENT_H 0x0E
> +#define CW2217_CURRENT_L 0x0F
> +
> +#define CW2217_BAT_PROFILE 0x10
> +#define CW2217_PROFILE_SZ 80
> +
> +#define CW2217_T_HOST_H 0xA0
> +#define CW2217_T_HOST_L 0xA1
> +
> +#define CW2217_USER_CONF 0xA2
> +
> +#define CW2217_CYCLECNT_H 0xA4
> +#define CW2217_CYCLECNT_L 0xA5
> +
> +#define CW2217_SOH 0xA6
> +
> +/* This register not documented in datasheet. */
> +#define CW2217_IC_STATE 0xA7
> +#define CW2217_IC_READY_MASK 0x0C
> +#define CW2217_IC_READY 3
> +
> +#define CW2217_STB_CUR_H 0xA8
> +#define CW2217_STB_CUR_L 0xA9
> +
> +#define CW2217_FW_VERSION 0xAB
> +#define CW2217_ID_MASK GENMASK(7, 6)
> +#define CW2215_ID 2
> +#define CW2217_ID 1
> +#define CW2218_ID 0
> +
> +/* Default values from BSP driver */
> +#define CW2217_USER_RSENSE 1500
> +#define CW2217_DEFAULT_POLL_INTERVAL_MS 5000
> +
> +struct cw2217_battery {
> + struct device *dev;
> + struct regmap *regmap;
> + struct power_supply *cw_bat;
> + u8 *bat_profile;
> + struct workqueue_struct *battery_workqueue;
> + struct delayed_work battery_delay_work;
> + int user_rsense;
> + int chip_id;
> + int voltage;
> + int soc;
> + int temp;
> + long cw_current;
> + int cycle;
> + int soh;
> + int fw_version;
> + u32 poll_interval_ms;
> + bool update_profile;
> +};
> +
> +/*
> + * The CONFIG register is used for the host MCU to configure the fuel gauge IC.
> + * The default value is 0xF0, SLEEP and RESTART bits are set. To power up the
> + * IC, the host MCU needs to write 0x30 to exit shutdown mode, and then write
> + * 0x00 to restart the gauge to enter active mode. To reset the IC, the host
> + * MCU needs to write 0xF0, 0x30 and 0x00 in sequence to this register to
> + * complete the restart procedure. The CW2217 will reload relevant parameters
> + * and settings and restart SOC calculation. Note that the SOC may be a
> + * different value after reset operation since it is a brand-new calculation
> + * based on the latest battery status. The BSP driver slept 20ms after restart
> + * and 10ms after setting active, though no delays are documented in the
> + * datasheet.
> + */
> +static int cw2217_active(struct cw2217_battery *cw_bat, bool active)
> +{
> + int ret;
> +
> + ret = regmap_write(cw_bat->regmap, CW2217_CONFIG,
> + CW2217_CONFIG_RESTART);
> + if (ret)
> + return ret;
> +
> + fsleep(20000);
> +
> + if (active) {
> + ret = regmap_write(cw_bat->regmap, CW2217_CONFIG,
> + CW2217_CONFIG_ACTIVE);
> + if (ret)
> + return ret;
> + } else {
> + ret = regmap_write(cw_bat->regmap, CW2217_CONFIG,
> + CW2217_CONFIG_SLEEP | CW2217_CONFIG_RESTART);
> + if (ret)
> + return ret;
> + }
> +
> + fsleep(10000);
> +
> + return 0;
> +}
> +
> +/*
> + * The 0x00 register is an UNSIGNED 8bit read-only register. Its value is
> + * fixed to 0xA0 in shutdown mode and active mode.
> + */
> +static int cw2217_get_chip_id(struct cw2217_battery *cw_bat)
> +{
> + u32 reg;
> + int ret;
> +
> + ret = regmap_read(cw_bat->regmap, CW2217_VERSION, ®);
> + if (ret)
> + return ret;
> +
> + cw_bat->chip_id = reg;
> +
> + return 0;
> +}
> +
> +/*
> + * The VCELL registers (0x02 0x03) are an UNSIGNED 14bit read-only register
> + * that updates the battery voltage continuously. Battery voltage is measured
> + * between the VCELL pin and VSS pin, which is the ground reference. A 14bit
> + * sigma-delta A/D converter is used and the voltage resolution is 312.5uV
> + * (0.3125mV is * 5/16).
> + */
> +static int cw2217_get_voltage(struct cw2217_battery *cw_bat)
> +{
> + __be16 reg;
> + int ret;
> +
> + ret = regmap_bulk_read(cw_bat->regmap, CW2217_VCELL_H,
> + ®, sizeof(reg));
> + if (ret)
> + return ret;
> +
> + cw_bat->voltage = ((be16_to_cpu(reg) * 5) / 16) * 1000;
> +
> + return 0;
> +}
> +
> +/*
> + * The SOC registers (0x04 0x05) are an UNSIGNED 16bit read-only register that
> + * indicates the SOC of the battery. The SOC shows in % format, which means
> + * how much percent of the battery's total available capacity is remaining in
> + * the battery now. The SOC can intrinsically adjust itself to cater to the
> + * change of battery status; including load, temperature, and aging.
> + * The high byte(0x04) contains the SOC in 1% unit which can be directly used
> + * if this resolution is good enough for the application. The low byte (0x05)
> + * provides more accurate fractional part of the SOC and its LSB is (1/256) %.
> + * For now, we are only interested in the high byte.
> + */
> +static int cw2217_get_capacity(struct cw2217_battery *cw_bat)
> +{
> + u32 reg;
> + int ret;
> +
> + ret = regmap_read(cw_bat->regmap, CW2217_SOC_H, ®);
> + if (ret)
> + return ret;
> +
> + cw_bat->soc = clamp(reg, 0, 100);
> +
> + return 0;
> +}
> +
> +/*
> + * The TEMP register is an UNSIGNED 8bit read only register.
> + * It reports the real-time battery temperature
> + * measured at TS pin. The scope is from -40 to 87.5 degrees Celsius,
> + * LSB is 0.5 degree Celsius. TEMP(C) = - 40 + Value(0x06 Reg) / 2
> + */
> +static int cw2217_get_temp(struct cw2217_battery *cw_bat)
> +{
> + u32 reg;
> + int ret;
> +
> + ret = regmap_read(cw_bat->regmap, CW2217_TEMP, ®);
> + if (ret)
> + return ret;
> +
> + cw_bat->temp = ((int)reg / 2) - 40;
> +
> + return 0;
> +}
> +
> +/*
> + * Current is a signed 16bit register (0x0E 0x0F) that reports current A/D
> + * converter result of the voltage across the current sense resistor,
> + * 10mohm typical. The register value should be divided by the sense
> + * resistance to convert to amperes. The value of the sense resistor
> + * determines the resolution and the full-scale range of the current readings.
> + * The LSB of 0x0F is (52.4/32768)uV for CW2215 and CW2217. The LSB of 0x0F is
> + * (125/32768)uV for CW2218.
> + */
> +static int cw2217_get_current(struct cw2217_battery *cw_bat)
> +{
> + s32 cw_current;
> + __be16 reg;
> + u8 fw_ver;
> + int ret;
> +
> + if (cw_bat->fw_version == 0) {
> + cw_bat->cw_current = 0;
> + dev_err(cw_bat->dev, "Error! CW2217 firmware read error!\n");
> + return -EIO;
> + }
> +
> + ret = regmap_bulk_read(cw_bat->regmap, CW2217_CURRENT_H,
> + ®, sizeof(reg));
> + if (ret)
> + return ret;
> +
> + cw_current = (s16)be16_to_cpu(reg);
> +
> + fw_ver = FIELD_GET(CW2217_ID_MASK, cw_bat->fw_version);
> + switch (fw_ver) {
> + case CW2215_ID:
> + case CW2217_ID:
> + cw_current = (cw_current * 1600) / (cw_bat->user_rsense / 1000);
> + cw_bat->cw_current = cw_current * 1000;
> + break;
> + case CW2218_ID:
> + cw_current = (cw_current * 3815) / (cw_bat->user_rsense / 1000);
> + cw_bat->cw_current = cw_current * 1000;
> + break;
> + default:
> + cw_bat->cw_current = 0;
> + dev_err(cw_bat->dev, "Error! CW2217 firmware read error!\n");
> + return -EIO;
> + }
> +
> + return 0;
> +}
> +
> +/*
> + * CYCLECNT is an UNSIGNED 16bit register (0xA4 0xA5) that counts cycle life
> + * of the battery. The LSB of 0xA5 stands for 1/16 cycle. This register will
> + * be cleared after entering shutdown mode.
> + */
> +static int cw2217_get_cycle_count(struct cw2217_battery *cw_bat)
> +{
> + __be16 reg;
> + int ret;
> +
> + ret = regmap_bulk_read(cw_bat->regmap, CW2217_CYCLECNT_H,
> + ®, sizeof(reg));
> + if (ret)
> + return ret;
> +
> + cw_bat->cycle = be16_to_cpu(reg) / 16;
> +
> + return 0;
> +}
> +
> +/*
> + * SOH (State of Health) is an UNSIGNED 8bit register(0xA6) that represents the level of
> + * battery aging by tracking battery internal impedance increment. When the device enters
> + * active mode, this register refresh to 0x64 by default. Its range is 0x00 to 0x64,
> + * indicating 0 to 100%. This register will be clear after enters shutdown mode.
> + */
> +static int cw2217_get_soh(struct cw2217_battery *cw_bat)
> +{
> + u32 reg;
> + int ret;
> +
> + ret = regmap_read(cw_bat->regmap, CW2217_SOH, ®);
> + if (ret)
> + return ret;
> +
> + cw_bat->soh = clamp(reg, 0, 100);
> +
> + return 0;
> +}
> +
> +/*
> + * FW_VERSION register reports the firmware (FW) running in the chip. It is
> + * fixed to 0x00 when the chip is in shutdown mode. When in active mode,
> + * Bit [7:6] = '01' stand for the CW2217, Bit [7:6] = '00' stand for the
> + * CW2218 and Bit [7:6] = '10' stand for CW2215. Bit[5:0] stand for the FW
> + * version running in the chip. Note that the FW version is subject to update.
> + */
> +static int cw2217_get_fw_version(struct cw2217_battery *cw_bat)
> +{
> + u32 reg;
> + int ret;
> +
> + ret = regmap_read(cw_bat->regmap, CW2217_FW_VERSION, ®);
> + if (ret)
> + return ret;
> +
> + cw_bat->fw_version = reg;
> +
> + return 0;
> +}
> +
> +static int cw2217_update_data(struct cw2217_battery *cw_bat)
> +{
> + int ret;
> +
> + ret = cw2217_get_voltage(cw_bat);
> + if (ret)
> + return ret;
> +
> + ret = cw2217_get_capacity(cw_bat);
> + if (ret)
> + return ret;
> +
> + ret = cw2217_get_temp(cw_bat);
> + if (ret)
> + return ret;
> +
> + ret = cw2217_get_current(cw_bat);
> + if (ret)
> + return ret;
> +
> + ret = cw2217_get_cycle_count(cw_bat);
> + if (ret)
> + return ret;
> +
> + return cw2217_get_soh(cw_bat);
> +}
> +
> +static int cw2217_parse_properties(struct cw2217_battery *cw_bat)
> +{
> + struct device *dev = cw_bat->dev;
> + int length;
> + int ret;
> +
> + ret = device_property_read_u32(dev, "cellwise,monitor-interval-ms",
> + &cw_bat->poll_interval_ms);
> + if (ret) {
> + dev_dbg(cw_bat->dev, "Using default poll interval\n");
> + cw_bat->poll_interval_ms = CW2217_DEFAULT_POLL_INTERVAL_MS;
> + }
> +
> + /*
> + * Datasheet only lists options of between 1M and 50M for resistor
> + * values, so assuming that is all and clamping if out of range.
> + */
> + ret = device_property_read_u32(dev,
> + "shunt-resistor-micro-ohms",
> + &cw_bat->user_rsense);
> + if (ret) {
> + cw_bat->user_rsense = CW2217_USER_RSENSE;
> + dev_warn(cw_bat->dev,
> + "Using default resistor value, reported mA may be wrong\n");
> + }
> +
> + /* Clamp, but also warn we're clamping due to out of range. */
> + if ((cw_bat->user_rsense < 1000000) || (cw_bat->user_rsense > 50000000)) {
> + cw_bat->user_rsense = clamp(cw_bat->user_rsense, 1000000, 50000000);
> + dev_warn(cw_bat->dev, "Shunt resistor value out of range, setting to %d",
> + cw_bat->user_rsense);
> + }
> +
> + length = device_property_count_u8(dev, "cellwise,battery-profile");
> + if (length < 0) {
> + dev_warn(cw_bat->dev,
> + "No battery-profile found, using current flash contents\n");
> + } else if (length != CW2217_PROFILE_SZ) {
> + dev_err(cw_bat->dev, "battery-profile must be %d bytes\n",
> + CW2217_PROFILE_SZ);
> + return -EINVAL;
> + } else {
> + cw_bat->bat_profile = devm_kzalloc(dev, length, GFP_KERNEL);
> + if (!cw_bat->bat_profile)
> + return -ENOMEM;
> +
> + ret = device_property_read_u8_array(dev,
> + "cellwise,battery-profile",
> + cw_bat->bat_profile,
> + length);
> + if (ret)
> + return ret;
> + }
> +
> + return 0;
> +}
> +
> +/* Load a new battery profile provided from the device tree. */
> +static int cw2217_update_config(struct cw2217_battery *cw_bat)
> +{
> + u32 reg;
> + int ret;
> +
> + /* Set the battery monitor state to shutdown. */
> + ret = cw2217_active(cw_bat, false);
> + if (ret)
> + return ret;
> +
> + /* update new battery info */
> + ret = regmap_raw_write(cw_bat->regmap, CW2217_BAT_PROFILE,
> + cw_bat->bat_profile, CW2217_PROFILE_SZ);
> + if (ret)
> + return ret;
> +
> + /* Set update flag for new profile and disable SOC alert. */
> + ret = regmap_write(cw_bat->regmap, CW2217_SOC_ALERT,
> + CW2217_SOC_UPDATE_FLG | CW2217_SOC_ALERT_OFF);
> + if (ret)
> + return ret;
> +
> + /* mask all interrupts */
> + ret = regmap_clear_bits(cw_bat->regmap, CW2217_INT_CONF,
> + CW2217_EN_SOC_INT | CW2217_EN_TMX_INT | CW2217_EN_TMN_INT);
> + if (ret)
> + return ret;
> +
> + ret = cw2217_active(cw_bat, true);
> + if (ret)
> + return ret;
> +
> + /*
> + * Check the undocumented register for bits 2 and 3 to be 1. Note that
> + * the BSP driver would sleep 100ms after every iteration and retry up
> + * to 50 times, this felt excessive so reducing to 10.
> + */
> + ret = regmap_read_poll_timeout(cw_bat->regmap, CW2217_IC_STATE, reg,
> + FIELD_GET(CW2217_IC_READY_MASK, reg) == CW2217_IC_READY,
> + 100 * USEC_PER_MSEC, 10 * USEC_PER_SEC);
> + if (ret) {
> + dev_err(cw_bat->dev,
> + "Gauge did not become ready after profile upload\n");
> + cw2217_active(cw_bat, false);
> + return ret;
> + }
> +
> + return 0;
> +}
> +
> +/* Determine whether the battery profile needs to be updated. */
> +static int cw2217_check_profile(struct cw2217_battery *cw_bat)
> +{
> + u8 bat_info[CW2217_PROFILE_SZ];
> + int ret;
> + u32 reg;
> +
> + cw_bat->update_profile = false;
> +
> + ret = regmap_read(cw_bat->regmap, CW2217_CONFIG, ®);
> + if (ret)
> + return ret;
> +
> + /* Active is 0x00, anything else is inactive */
> + if (FIELD_GET(CW2217_CONFIG_ACT_MSK, reg)) {
> + cw_bat->update_profile = true;
> + return 0;
> + }
> +
> + ret = regmap_read(cw_bat->regmap, CW2217_SOC_ALERT, ®);
> + if (ret)
> + return ret;
> +
> + if (FIELD_GET(CW2217_SOC_UPDATE_FLG, reg) == 0) {
> + cw_bat->update_profile = true;
> + return 0;
> + }
> +
> + ret = regmap_raw_read(cw_bat->regmap, CW2217_BAT_PROFILE, bat_info, CW2217_PROFILE_SZ);
> + if (ret) {
> + dev_err(cw_bat->dev, "Failed to read existing battery profile\n");
> + return ret;
> + }
> +
> + if (memcmp(bat_info, cw_bat->bat_profile, CW2217_PROFILE_SZ)) {
> + dev_warn(cw_bat->dev, "Replacing stored battery profile\n");
> + cw_bat->update_profile = true;
> + }
> +
> + return 0;
> +}
> +
> +static int cw2217_init(struct cw2217_battery *cw_bat)
> +{
> + int ret;
> +
> + ret = cw2217_get_chip_id(cw_bat);
> + if (ret)
> + return dev_err_probe(cw_bat->dev, ret, "I2C read write error");
> +
> + /*
> + * All of the known Cellwise 221x chips set the ID bits to 0xa0. If it
> + * doesn't match, we're trying to probe the wrong hardware.
> + */
> + if (cw_bat->chip_id != CW2217_CHIP_ID)
> + return dev_err_probe(cw_bat->dev, -ENODEV, "Not a cw221x\n");
> +
> + ret = cw2217_check_profile(cw_bat);
> + if (ret) {
> + dev_err(cw_bat->dev, "i2c read write error");
> + return ret;
> + }
> +
> + if (cw_bat->update_profile) {
> + ret = cw2217_update_config(cw_bat);
> + if (ret)
> + return ret;
> + }
> +
> + return 0;
> +}
> +
> +static void cw2217_bat_work(struct work_struct *work)
> +{
> + struct delayed_work *delay_work;
> + struct cw2217_battery *cw_bat;
> + int ret;
> +
> + delay_work = container_of(work,
> + struct delayed_work,
> + work);
> +
> + cw_bat = container_of(delay_work,
> + struct cw2217_battery,
> + battery_delay_work);
> +
> + ret = cw2217_update_data(cw_bat);
> + if (ret)
> + dev_err(cw_bat->dev, "i2c read error when update data");
> +
> + queue_delayed_work(cw_bat->battery_workqueue,
> + &cw_bat->battery_delay_work,
> + msecs_to_jiffies(cw_bat->poll_interval_ms));
> +}
> +
> +static int cw2217_battery_get_property(struct power_supply *psy,
> + enum power_supply_property psp,
> + union power_supply_propval *val)
> +{
> + struct cw2217_battery *cw_bat = power_supply_get_drvdata(psy);
> + int ret = 0;
> +
> + switch (psp) {
> + case POWER_SUPPLY_PROP_CYCLE_COUNT:
> + val->intval = cw_bat->cycle;
> + break;
> + case POWER_SUPPLY_PROP_CAPACITY:
> + val->intval = cw_bat->soc;
> + break;
> + case POWER_SUPPLY_PROP_STATUS:
> + /*
> + * Observed when battery was full that it would report
> + * approximately -12mv at between 97-99 SoC. Assume that if the
> + * battery current is above 0 it's charging, it's below
> + * 0 but with an SoC 95% or above it's not charging because
> + * it's full, otherwise it's simply not charging. If it's not
> + * supplied it is discharging.
> + */
> + if (power_supply_is_system_supplied()) {
> + if (cw_bat->cw_current > 0)
> + val->intval = POWER_SUPPLY_STATUS_CHARGING;
> + else if (cw_bat->soc >= 95)
> + val->intval = POWER_SUPPLY_STATUS_FULL;
> + else
> + val->intval = POWER_SUPPLY_STATUS_NOT_CHARGING;
> + } else
> + val->intval = POWER_SUPPLY_STATUS_DISCHARGING;
> + break;
> + case POWER_SUPPLY_PROP_PRESENT:
> + val->intval = (cw_bat->voltage <= 0) ? 0 : 1;
> + break;
> + case POWER_SUPPLY_PROP_VOLTAGE_NOW:
> + val->intval = cw_bat->voltage;
> + break;
> + case POWER_SUPPLY_PROP_CURRENT_NOW:
> + val->intval = cw_bat->cw_current;
> + break;
> + case POWER_SUPPLY_PROP_STATE_OF_HEALTH:
> + val->intval = cw_bat->soh;
> + break;
> + case POWER_SUPPLY_PROP_TEMP:
> + val->intval = cw_bat->temp;
> + break;
> + default:
> + ret = -EINVAL;
> + break;
> + }
> +
> + return ret;
> +}
> +
> +static enum power_supply_property cw2217_battery_properties[] = {
> + POWER_SUPPLY_PROP_CYCLE_COUNT,
> + POWER_SUPPLY_PROP_CAPACITY,
> + POWER_SUPPLY_PROP_PRESENT,
> + POWER_SUPPLY_PROP_VOLTAGE_NOW,
> + POWER_SUPPLY_PROP_CURRENT_NOW,
> + POWER_SUPPLY_PROP_STATUS,
> + POWER_SUPPLY_PROP_STATE_OF_HEALTH,
> + POWER_SUPPLY_PROP_TEMP,
> +};
> +
> +static const struct power_supply_desc cw2217_bat_desc = {
> + .name = "cw2217-battery",
> + .type = POWER_SUPPLY_TYPE_BATTERY,
> + .properties = cw2217_battery_properties,
> + .num_properties = ARRAY_SIZE(cw2217_battery_properties),
> + .get_property = cw2217_battery_get_property,
> +};
> +
> +static const struct regmap_range cw2217_ranges_rd_yes[] = {
> + regmap_reg_range(CW2217_VERSION, CW2217_VERSION),
> + regmap_reg_range(CW2217_VCELL_H, CW2217_TEMP),
> + regmap_reg_range(CW2217_CONFIG, CW2217_CONFIG),
> + regmap_reg_range(CW2217_INT_CONF, CW2217_CURRENT_L),
> + regmap_reg_range(CW2217_BAT_PROFILE,
> + CW2217_BAT_PROFILE + CW2217_PROFILE_SZ - 1),
> + regmap_reg_range(CW2217_T_HOST_H, CW2217_USER_CONF),
> + regmap_reg_range(CW2217_CYCLECNT_H, CW2217_STB_CUR_L),
> + regmap_reg_range(CW2217_FW_VERSION, CW2217_FW_VERSION),
> +};
> +
> +static const struct regmap_access_table cw2217_rd_table = {
> + .yes_ranges = cw2217_ranges_rd_yes,
> + .n_yes_ranges = ARRAY_SIZE(cw2217_ranges_rd_yes),
> +};
> +
> +static const struct regmap_range cw2217_ranges_wr_yes[] = {
> + regmap_reg_range(CW2217_TEMP, CW2217_TEMP),
> + regmap_reg_range(CW2217_CONFIG, CW2217_CONFIG),
> + regmap_reg_range(CW2217_INT_CONF, CW2217_CURRENT_L),
> + regmap_reg_range(CW2217_BAT_PROFILE,
> + CW2217_BAT_PROFILE + CW2217_PROFILE_SZ - 1),
> + regmap_reg_range(CW2217_T_HOST_H, CW2217_USER_CONF),
> +};
> +
> +static const struct regmap_access_table cw2217_wr_table = {
> + .yes_ranges = cw2217_ranges_wr_yes,
> + .n_yes_ranges = ARRAY_SIZE(cw2217_ranges_wr_yes),
> +};
> +
> +static const struct regmap_range cw2217_ranges_vol_yes[] = {
> + regmap_reg_range(CW2217_VCELL_H, CW2217_TEMP),
> + regmap_reg_range(CW2217_INT_CONF, CW2217_SOC_ALERT),
> + regmap_reg_range(CW2217_CURRENT_H, CW2217_CURRENT_L),
> + regmap_reg_range(CW2217_CYCLECNT_H, CW2217_STB_CUR_L),
> +};
> +
> +static const struct regmap_access_table cw2217_vol_table = {
> + .yes_ranges = cw2217_ranges_vol_yes,
> + .n_yes_ranges = ARRAY_SIZE(cw2217_ranges_vol_yes),
> +};
> +
> +static const struct regmap_config cw2217_regmap_config = {
> + .reg_bits = 8,
> + .val_bits = 8,
> + .rd_table = &cw2217_rd_table,
> + .wr_table = &cw2217_wr_table,
> + .volatile_table = &cw2217_vol_table,
> + .cache_type = REGCACHE_MAPLE,
> + .max_register = CW2217_FW_VERSION,
> +};
> +
> +static int cw2217_probe(struct i2c_client *client)
> +{
> + struct power_supply_config psy_cfg = { 0 };
> + struct cw2217_battery *cw_bat;
> + int ret;
> +
> + cw_bat = devm_kzalloc(&client->dev, sizeof(*cw_bat), GFP_KERNEL);
> + if (!cw_bat)
> + return -ENOMEM;
> +
> + i2c_set_clientdata(client, cw_bat);
> + cw_bat->dev = &client->dev;
> +
> + cw_bat->regmap = devm_regmap_init_i2c(client, &cw2217_regmap_config);
> + if (IS_ERR(cw_bat->regmap))
> + return dev_err_probe(cw_bat->dev, PTR_ERR(cw_bat->regmap),
> + "Failed to allocate regmap: %ld\n",
> + PTR_ERR(cw_bat->regmap));
> +
> + ret = cw2217_parse_properties(cw_bat);
> + if (ret)
> + return ret;
> +
> + ret = cw2217_init(cw_bat);
> + if (ret)
> + return dev_err_probe(cw_bat->dev, ret, "cw2217 init fail\n");
> +
> + ret = cw2217_get_fw_version(cw_bat);
> + if (ret)
> + return dev_err_probe(cw_bat->dev, ret,
> + "Unable to read fw version\n");
> +
> + ret = cw2217_update_data(cw_bat);
> + if (ret)
> + return dev_err_probe(cw_bat->dev, ret,
> + "Unable to read initial data\n");
> +
> + psy_cfg.drv_data = cw_bat;
> + cw_bat->cw_bat = devm_power_supply_register(&client->dev,
> + &cw2217_bat_desc, &psy_cfg);
> + if (IS_ERR(cw_bat->cw_bat))
> + return dev_err_probe(cw_bat->dev, PTR_ERR(cw_bat->cw_bat),
> + "Failed to register battery: %ld\n",
> + PTR_ERR(cw_bat->cw_bat));
> +
> + cw_bat->battery_workqueue = devm_alloc_ordered_workqueue(&client->dev,
> + "cw2217_gauge", 0);
> + if (!cw_bat->battery_workqueue)
> + return -ENOMEM;
> +
> + ret = devm_delayed_work_autocancel(&client->dev,
> + &cw_bat->battery_delay_work,
> + cw2217_bat_work);
> + if (ret)
> + return dev_err_probe(&client->dev, ret,
> + "Failed to register delayed work\n");
> +
> + queue_delayed_work(cw_bat->battery_workqueue,
> + &cw_bat->battery_delay_work,
> + msecs_to_jiffies(50));
> +
> + return 0;
> +}
> +
> +static int __maybe_unused cw2217_bat_suspend(struct device *dev)
> +{
> + struct i2c_client *client = to_i2c_client(dev);
> + struct cw2217_battery *cw_bat = i2c_get_clientdata(client);
> +
> + cancel_delayed_work(&cw_bat->battery_delay_work);
> + return 0;
> +}
> +
> +static int __maybe_unused cw2217_bat_resume(struct device *dev)
> +{
> + struct i2c_client *client = to_i2c_client(dev);
> + struct cw2217_battery *cw_bat = i2c_get_clientdata(client);
> +
> + queue_delayed_work(cw_bat->battery_workqueue,
> + &cw_bat->battery_delay_work,
> + msecs_to_jiffies(20));
> + return 0;
> +}
> +
> +static SIMPLE_DEV_PM_OPS(cw2217_bat_pm_ops, cw2217_bat_suspend,
> + cw2217_bat_resume);
> +
> +static const struct i2c_device_id cw2217_id_table[] = {
> + { .name = "cw2215" },
> + { .name = "cw2217" },
> + { .name = "cw2218" },
> + { }
> +};
> +MODULE_DEVICE_TABLE(i2c, cw2217_id_table);
> +
> +static const struct of_device_id cw2217_match_table[] = {
> + { .compatible = "cellwise,cw2215", },
> + { .compatible = "cellwise,cw2217", },
> + { .compatible = "cellwise,cw2218", },
> + { },
> +};
> +MODULE_DEVICE_TABLE(of, cw2217_match_table);
> +
> +static struct i2c_driver cw2217_driver = {
> + .driver = {
> + .name = "cw2217",
> + .pm = &cw2217_bat_pm_ops,
> + .of_match_table = of_match_ptr(cw2217_match_table),
> + },
> + .probe = cw2217_probe,
> + .id_table = cw2217_id_table,
> +};
> +
> +module_i2c_driver(cw2217_driver);
> +
> +MODULE_AUTHOR("Xu Shengfei <xsf@rock-chips.com>");
> +MODULE_AUTHOR("Chris Morgan <macromorgan@hotmail.com>");
> +MODULE_DESCRIPTION("cw2217 battery driver");
> +MODULE_LICENSE("GPL");
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH 2/3] power: supply: cw2217: Add support for Cellwise cw2217
2026-10-27 13:34 ` Shawn Lin
@ 2026-10-27 15:46 ` Chris Morgan
0 siblings, 0 replies; 7+ messages in thread
From: Chris Morgan @ 2026-10-27 15:46 UTC (permalink / raw)
To: Shawn Lin
Cc: Chris Morgan, linux-pm, devicetree, linux-rockchip, heiko,
conor+dt, krzk+dt, robh, sre, t.schramm, Xu Shengfei
On Tue, Oct 27, 2026 at 09:34:53PM +0800, Shawn Lin wrote:
> Cc: Shengfei
>
> 在 2026/10/27 星期二 21:17, Chris Morgan 写道:
> > From: Chris Morgan <macromorgan@hotmail.com>
> >
> > Add support for the CW2217 i2c battery gauge. The gauge driver was
> > adapted from the Rockchip BSP driver [1].
> >
>
> I don't look into it to evaluate how much it differ from Shengfei's
> driver. If not too much, he deserves a credit here. I CCed him to
> look more into your submission version.
It shouldn't differ a lot, and I left his name in the file itself. I'll
include it on the next revision in the Signed-off/Co-developed line if
that works? Sorry for the oversight.
Thank you.
>
> > [1] https://github.com/rockchip-linux/kernel/blob/develop-6.12/drivers/power/supply/cw221x_battery.c
> >
> > Signed-off-by: Chris Morgan <macromorgan@hotmail.com>
> > ---
> > drivers/power/supply/Kconfig | 11 +
> > drivers/power/supply/Makefile | 1 +
> > drivers/power/supply/cw2217_battery.c | 818 ++++++++++++++++++++++++++
> > 3 files changed, 830 insertions(+)
> > create mode 100644 drivers/power/supply/cw2217_battery.c
> >
> > diff --git a/drivers/power/supply/Kconfig b/drivers/power/supply/Kconfig
> > index b89ef40df7f8..6a5235ec16c1 100644
> > --- a/drivers/power/supply/Kconfig
> > +++ b/drivers/power/supply/Kconfig
> > @@ -141,6 +141,17 @@ config BATTERY_CW2015
> > This driver can also be built as a module. If so, the module will be
> > called cw2015_battery.
> > +config BATTERY_CW2217
> > + tristate "CW2217 Battery driver"
> > + depends on I2C
> > + select REGMAP_I2C
> > + help
> > + Say Y here to enable support for the cellwise cw2217
> > + battery fuel gauge (used in the Anbernic RG Vita Pro & others)
> > +
> > + This driver can also be built as a module. If so, the module will be
> > + called cw2217_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 f3c0d1006c1a..66e6303888c7 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_CW2217) += cw2217_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/cw2217_battery.c b/drivers/power/supply/cw2217_battery.c
> > new file mode 100644
> > index 000000000000..8b18db3d54db
> > --- /dev/null
> > +++ b/drivers/power/supply/cw2217_battery.c
> > @@ -0,0 +1,818 @@
> > +// SPDX-License-Identifier: GPL-2.0
> > +/*
> > + * Chrager driver for cw2217
> > + *
> > + * Copyright (c) 2026 Rockchip Electronics Co., Ltd.
> > + *
> > + * Author: Xu Shengfei <xsf@rock-chips.com>
> > + */
> > +#include <linux/bits.h>
> > +#include <linux/delay.h>
> > +#include <linux/devm-helpers.h>
> > +#include <linux/i2c.h>
> > +#include <linux/minmax.h>
> > +#include <linux/module.h>
> > +#include <linux/power_supply.h>
> > +#include <linux/regmap.h>
> > +#include <linux/workqueue.h>
> > +
> > +#include <asm/byteorder.h>
> > +
> > +#define CW2217_VERSION 0x00
> > +#define CW2217_CHIP_ID 0xA0
> > +
> > +#define CW2217_VCELL_H 0x02
> > +#define CW2217_VCELL_L 0x03
> > +
> > +#define CW2217_SOC_H 0x04
> > +#define CW2217_SOC_L 0x05
> > +
> > +#define CW2217_TEMP 0x06
> > +
> > +#define CW2217_CONFIG 0x08
> > +#define CW2217_CONFIG_ACT_MSK GENMASK(7, 4)
> > +#define CW2217_CONFIG_SLEEP (0x03 << 6)
> > +#define CW2217_CONFIG_RESTART (0x03 << 4)
> > +#define CW2217_CONFIG_ACTIVE (0x00)
> > +
> > +#define CW2217_INT_CONF 0x0A
> > +#define CW2217_EN_SOC_INT BIT(6)
> > +#define CW2217_EN_TMX_INT BIT(5)
> > +#define CW2217_EN_TMN_INT BIT(4)
> > +
> > +#define CW2217_SOC_ALERT 0x0B
> > +#define CW2217_SOC_UPDATE_FLG BIT(7)
> > +#define CW2217_SOC_ALERT_MSK GENMASK(6, 0)
> > +#define CW2217_SOC_ALERT_OFF 0x0
> > +
> > +#define CW2217_TEMP_MAX 0x0C
> > +
> > +#define CW2217_TEMP_MIN 0x0D
> > +
> > +#define CW2217_CURRENT_H 0x0E
> > +#define CW2217_CURRENT_L 0x0F
> > +
> > +#define CW2217_BAT_PROFILE 0x10
> > +#define CW2217_PROFILE_SZ 80
> > +
> > +#define CW2217_T_HOST_H 0xA0
> > +#define CW2217_T_HOST_L 0xA1
> > +
> > +#define CW2217_USER_CONF 0xA2
> > +
> > +#define CW2217_CYCLECNT_H 0xA4
> > +#define CW2217_CYCLECNT_L 0xA5
> > +
> > +#define CW2217_SOH 0xA6
> > +
> > +/* This register not documented in datasheet. */
> > +#define CW2217_IC_STATE 0xA7
> > +#define CW2217_IC_READY_MASK 0x0C
> > +#define CW2217_IC_READY 3
> > +
> > +#define CW2217_STB_CUR_H 0xA8
> > +#define CW2217_STB_CUR_L 0xA9
> > +
> > +#define CW2217_FW_VERSION 0xAB
> > +#define CW2217_ID_MASK GENMASK(7, 6)
> > +#define CW2215_ID 2
> > +#define CW2217_ID 1
> > +#define CW2218_ID 0
> > +
> > +/* Default values from BSP driver */
> > +#define CW2217_USER_RSENSE 1500
> > +#define CW2217_DEFAULT_POLL_INTERVAL_MS 5000
> > +
> > +struct cw2217_battery {
> > + struct device *dev;
> > + struct regmap *regmap;
> > + struct power_supply *cw_bat;
> > + u8 *bat_profile;
> > + struct workqueue_struct *battery_workqueue;
> > + struct delayed_work battery_delay_work;
> > + int user_rsense;
> > + int chip_id;
> > + int voltage;
> > + int soc;
> > + int temp;
> > + long cw_current;
> > + int cycle;
> > + int soh;
> > + int fw_version;
> > + u32 poll_interval_ms;
> > + bool update_profile;
> > +};
> > +
> > +/*
> > + * The CONFIG register is used for the host MCU to configure the fuel gauge IC.
> > + * The default value is 0xF0, SLEEP and RESTART bits are set. To power up the
> > + * IC, the host MCU needs to write 0x30 to exit shutdown mode, and then write
> > + * 0x00 to restart the gauge to enter active mode. To reset the IC, the host
> > + * MCU needs to write 0xF0, 0x30 and 0x00 in sequence to this register to
> > + * complete the restart procedure. The CW2217 will reload relevant parameters
> > + * and settings and restart SOC calculation. Note that the SOC may be a
> > + * different value after reset operation since it is a brand-new calculation
> > + * based on the latest battery status. The BSP driver slept 20ms after restart
> > + * and 10ms after setting active, though no delays are documented in the
> > + * datasheet.
> > + */
> > +static int cw2217_active(struct cw2217_battery *cw_bat, bool active)
> > +{
> > + int ret;
> > +
> > + ret = regmap_write(cw_bat->regmap, CW2217_CONFIG,
> > + CW2217_CONFIG_RESTART);
> > + if (ret)
> > + return ret;
> > +
> > + fsleep(20000);
> > +
> > + if (active) {
> > + ret = regmap_write(cw_bat->regmap, CW2217_CONFIG,
> > + CW2217_CONFIG_ACTIVE);
> > + if (ret)
> > + return ret;
> > + } else {
> > + ret = regmap_write(cw_bat->regmap, CW2217_CONFIG,
> > + CW2217_CONFIG_SLEEP | CW2217_CONFIG_RESTART);
> > + if (ret)
> > + return ret;
> > + }
> > +
> > + fsleep(10000);
> > +
> > + return 0;
> > +}
> > +
> > +/*
> > + * The 0x00 register is an UNSIGNED 8bit read-only register. Its value is
> > + * fixed to 0xA0 in shutdown mode and active mode.
> > + */
> > +static int cw2217_get_chip_id(struct cw2217_battery *cw_bat)
> > +{
> > + u32 reg;
> > + int ret;
> > +
> > + ret = regmap_read(cw_bat->regmap, CW2217_VERSION, ®);
> > + if (ret)
> > + return ret;
> > +
> > + cw_bat->chip_id = reg;
> > +
> > + return 0;
> > +}
> > +
> > +/*
> > + * The VCELL registers (0x02 0x03) are an UNSIGNED 14bit read-only register
> > + * that updates the battery voltage continuously. Battery voltage is measured
> > + * between the VCELL pin and VSS pin, which is the ground reference. A 14bit
> > + * sigma-delta A/D converter is used and the voltage resolution is 312.5uV
> > + * (0.3125mV is * 5/16).
> > + */
> > +static int cw2217_get_voltage(struct cw2217_battery *cw_bat)
> > +{
> > + __be16 reg;
> > + int ret;
> > +
> > + ret = regmap_bulk_read(cw_bat->regmap, CW2217_VCELL_H,
> > + ®, sizeof(reg));
> > + if (ret)
> > + return ret;
> > +
> > + cw_bat->voltage = ((be16_to_cpu(reg) * 5) / 16) * 1000;
> > +
> > + return 0;
> > +}
> > +
> > +/*
> > + * The SOC registers (0x04 0x05) are an UNSIGNED 16bit read-only register that
> > + * indicates the SOC of the battery. The SOC shows in % format, which means
> > + * how much percent of the battery's total available capacity is remaining in
> > + * the battery now. The SOC can intrinsically adjust itself to cater to the
> > + * change of battery status; including load, temperature, and aging.
> > + * The high byte(0x04) contains the SOC in 1% unit which can be directly used
> > + * if this resolution is good enough for the application. The low byte (0x05)
> > + * provides more accurate fractional part of the SOC and its LSB is (1/256) %.
> > + * For now, we are only interested in the high byte.
> > + */
> > +static int cw2217_get_capacity(struct cw2217_battery *cw_bat)
> > +{
> > + u32 reg;
> > + int ret;
> > +
> > + ret = regmap_read(cw_bat->regmap, CW2217_SOC_H, ®);
> > + if (ret)
> > + return ret;
> > +
> > + cw_bat->soc = clamp(reg, 0, 100);
> > +
> > + return 0;
> > +}
> > +
> > +/*
> > + * The TEMP register is an UNSIGNED 8bit read only register.
> > + * It reports the real-time battery temperature
> > + * measured at TS pin. The scope is from -40 to 87.5 degrees Celsius,
> > + * LSB is 0.5 degree Celsius. TEMP(C) = - 40 + Value(0x06 Reg) / 2
> > + */
> > +static int cw2217_get_temp(struct cw2217_battery *cw_bat)
> > +{
> > + u32 reg;
> > + int ret;
> > +
> > + ret = regmap_read(cw_bat->regmap, CW2217_TEMP, ®);
> > + if (ret)
> > + return ret;
> > +
> > + cw_bat->temp = ((int)reg / 2) - 40;
> > +
> > + return 0;
> > +}
> > +
> > +/*
> > + * Current is a signed 16bit register (0x0E 0x0F) that reports current A/D
> > + * converter result of the voltage across the current sense resistor,
> > + * 10mohm typical. The register value should be divided by the sense
> > + * resistance to convert to amperes. The value of the sense resistor
> > + * determines the resolution and the full-scale range of the current readings.
> > + * The LSB of 0x0F is (52.4/32768)uV for CW2215 and CW2217. The LSB of 0x0F is
> > + * (125/32768)uV for CW2218.
> > + */
> > +static int cw2217_get_current(struct cw2217_battery *cw_bat)
> > +{
> > + s32 cw_current;
> > + __be16 reg;
> > + u8 fw_ver;
> > + int ret;
> > +
> > + if (cw_bat->fw_version == 0) {
> > + cw_bat->cw_current = 0;
> > + dev_err(cw_bat->dev, "Error! CW2217 firmware read error!\n");
> > + return -EIO;
> > + }
> > +
> > + ret = regmap_bulk_read(cw_bat->regmap, CW2217_CURRENT_H,
> > + ®, sizeof(reg));
> > + if (ret)
> > + return ret;
> > +
> > + cw_current = (s16)be16_to_cpu(reg);
> > +
> > + fw_ver = FIELD_GET(CW2217_ID_MASK, cw_bat->fw_version);
> > + switch (fw_ver) {
> > + case CW2215_ID:
> > + case CW2217_ID:
> > + cw_current = (cw_current * 1600) / (cw_bat->user_rsense / 1000);
> > + cw_bat->cw_current = cw_current * 1000;
> > + break;
> > + case CW2218_ID:
> > + cw_current = (cw_current * 3815) / (cw_bat->user_rsense / 1000);
> > + cw_bat->cw_current = cw_current * 1000;
> > + break;
> > + default:
> > + cw_bat->cw_current = 0;
> > + dev_err(cw_bat->dev, "Error! CW2217 firmware read error!\n");
> > + return -EIO;
> > + }
> > +
> > + return 0;
> > +}
> > +
> > +/*
> > + * CYCLECNT is an UNSIGNED 16bit register (0xA4 0xA5) that counts cycle life
> > + * of the battery. The LSB of 0xA5 stands for 1/16 cycle. This register will
> > + * be cleared after entering shutdown mode.
> > + */
> > +static int cw2217_get_cycle_count(struct cw2217_battery *cw_bat)
> > +{
> > + __be16 reg;
> > + int ret;
> > +
> > + ret = regmap_bulk_read(cw_bat->regmap, CW2217_CYCLECNT_H,
> > + ®, sizeof(reg));
> > + if (ret)
> > + return ret;
> > +
> > + cw_bat->cycle = be16_to_cpu(reg) / 16;
> > +
> > + return 0;
> > +}
> > +
> > +/*
> > + * SOH (State of Health) is an UNSIGNED 8bit register(0xA6) that represents the level of
> > + * battery aging by tracking battery internal impedance increment. When the device enters
> > + * active mode, this register refresh to 0x64 by default. Its range is 0x00 to 0x64,
> > + * indicating 0 to 100%. This register will be clear after enters shutdown mode.
> > + */
> > +static int cw2217_get_soh(struct cw2217_battery *cw_bat)
> > +{
> > + u32 reg;
> > + int ret;
> > +
> > + ret = regmap_read(cw_bat->regmap, CW2217_SOH, ®);
> > + if (ret)
> > + return ret;
> > +
> > + cw_bat->soh = clamp(reg, 0, 100);
> > +
> > + return 0;
> > +}
> > +
> > +/*
> > + * FW_VERSION register reports the firmware (FW) running in the chip. It is
> > + * fixed to 0x00 when the chip is in shutdown mode. When in active mode,
> > + * Bit [7:6] = '01' stand for the CW2217, Bit [7:6] = '00' stand for the
> > + * CW2218 and Bit [7:6] = '10' stand for CW2215. Bit[5:0] stand for the FW
> > + * version running in the chip. Note that the FW version is subject to update.
> > + */
> > +static int cw2217_get_fw_version(struct cw2217_battery *cw_bat)
> > +{
> > + u32 reg;
> > + int ret;
> > +
> > + ret = regmap_read(cw_bat->regmap, CW2217_FW_VERSION, ®);
> > + if (ret)
> > + return ret;
> > +
> > + cw_bat->fw_version = reg;
> > +
> > + return 0;
> > +}
> > +
> > +static int cw2217_update_data(struct cw2217_battery *cw_bat)
> > +{
> > + int ret;
> > +
> > + ret = cw2217_get_voltage(cw_bat);
> > + if (ret)
> > + return ret;
> > +
> > + ret = cw2217_get_capacity(cw_bat);
> > + if (ret)
> > + return ret;
> > +
> > + ret = cw2217_get_temp(cw_bat);
> > + if (ret)
> > + return ret;
> > +
> > + ret = cw2217_get_current(cw_bat);
> > + if (ret)
> > + return ret;
> > +
> > + ret = cw2217_get_cycle_count(cw_bat);
> > + if (ret)
> > + return ret;
> > +
> > + return cw2217_get_soh(cw_bat);
> > +}
> > +
> > +static int cw2217_parse_properties(struct cw2217_battery *cw_bat)
> > +{
> > + struct device *dev = cw_bat->dev;
> > + int length;
> > + int ret;
> > +
> > + ret = device_property_read_u32(dev, "cellwise,monitor-interval-ms",
> > + &cw_bat->poll_interval_ms);
> > + if (ret) {
> > + dev_dbg(cw_bat->dev, "Using default poll interval\n");
> > + cw_bat->poll_interval_ms = CW2217_DEFAULT_POLL_INTERVAL_MS;
> > + }
> > +
> > + /*
> > + * Datasheet only lists options of between 1M and 50M for resistor
> > + * values, so assuming that is all and clamping if out of range.
> > + */
> > + ret = device_property_read_u32(dev,
> > + "shunt-resistor-micro-ohms",
> > + &cw_bat->user_rsense);
> > + if (ret) {
> > + cw_bat->user_rsense = CW2217_USER_RSENSE;
> > + dev_warn(cw_bat->dev,
> > + "Using default resistor value, reported mA may be wrong\n");
> > + }
> > +
> > + /* Clamp, but also warn we're clamping due to out of range. */
> > + if ((cw_bat->user_rsense < 1000000) || (cw_bat->user_rsense > 50000000)) {
> > + cw_bat->user_rsense = clamp(cw_bat->user_rsense, 1000000, 50000000);
> > + dev_warn(cw_bat->dev, "Shunt resistor value out of range, setting to %d",
> > + cw_bat->user_rsense);
> > + }
> > +
> > + length = device_property_count_u8(dev, "cellwise,battery-profile");
> > + if (length < 0) {
> > + dev_warn(cw_bat->dev,
> > + "No battery-profile found, using current flash contents\n");
> > + } else if (length != CW2217_PROFILE_SZ) {
> > + dev_err(cw_bat->dev, "battery-profile must be %d bytes\n",
> > + CW2217_PROFILE_SZ);
> > + return -EINVAL;
> > + } else {
> > + cw_bat->bat_profile = devm_kzalloc(dev, length, GFP_KERNEL);
> > + if (!cw_bat->bat_profile)
> > + return -ENOMEM;
> > +
> > + ret = device_property_read_u8_array(dev,
> > + "cellwise,battery-profile",
> > + cw_bat->bat_profile,
> > + length);
> > + if (ret)
> > + return ret;
> > + }
> > +
> > + return 0;
> > +}
> > +
> > +/* Load a new battery profile provided from the device tree. */
> > +static int cw2217_update_config(struct cw2217_battery *cw_bat)
> > +{
> > + u32 reg;
> > + int ret;
> > +
> > + /* Set the battery monitor state to shutdown. */
> > + ret = cw2217_active(cw_bat, false);
> > + if (ret)
> > + return ret;
> > +
> > + /* update new battery info */
> > + ret = regmap_raw_write(cw_bat->regmap, CW2217_BAT_PROFILE,
> > + cw_bat->bat_profile, CW2217_PROFILE_SZ);
> > + if (ret)
> > + return ret;
> > +
> > + /* Set update flag for new profile and disable SOC alert. */
> > + ret = regmap_write(cw_bat->regmap, CW2217_SOC_ALERT,
> > + CW2217_SOC_UPDATE_FLG | CW2217_SOC_ALERT_OFF);
> > + if (ret)
> > + return ret;
> > +
> > + /* mask all interrupts */
> > + ret = regmap_clear_bits(cw_bat->regmap, CW2217_INT_CONF,
> > + CW2217_EN_SOC_INT | CW2217_EN_TMX_INT | CW2217_EN_TMN_INT);
> > + if (ret)
> > + return ret;
> > +
> > + ret = cw2217_active(cw_bat, true);
> > + if (ret)
> > + return ret;
> > +
> > + /*
> > + * Check the undocumented register for bits 2 and 3 to be 1. Note that
> > + * the BSP driver would sleep 100ms after every iteration and retry up
> > + * to 50 times, this felt excessive so reducing to 10.
> > + */
> > + ret = regmap_read_poll_timeout(cw_bat->regmap, CW2217_IC_STATE, reg,
> > + FIELD_GET(CW2217_IC_READY_MASK, reg) == CW2217_IC_READY,
> > + 100 * USEC_PER_MSEC, 10 * USEC_PER_SEC);
> > + if (ret) {
> > + dev_err(cw_bat->dev,
> > + "Gauge did not become ready after profile upload\n");
> > + cw2217_active(cw_bat, false);
> > + return ret;
> > + }
> > +
> > + return 0;
> > +}
> > +
> > +/* Determine whether the battery profile needs to be updated. */
> > +static int cw2217_check_profile(struct cw2217_battery *cw_bat)
> > +{
> > + u8 bat_info[CW2217_PROFILE_SZ];
> > + int ret;
> > + u32 reg;
> > +
> > + cw_bat->update_profile = false;
> > +
> > + ret = regmap_read(cw_bat->regmap, CW2217_CONFIG, ®);
> > + if (ret)
> > + return ret;
> > +
> > + /* Active is 0x00, anything else is inactive */
> > + if (FIELD_GET(CW2217_CONFIG_ACT_MSK, reg)) {
> > + cw_bat->update_profile = true;
> > + return 0;
> > + }
> > +
> > + ret = regmap_read(cw_bat->regmap, CW2217_SOC_ALERT, ®);
> > + if (ret)
> > + return ret;
> > +
> > + if (FIELD_GET(CW2217_SOC_UPDATE_FLG, reg) == 0) {
> > + cw_bat->update_profile = true;
> > + return 0;
> > + }
> > +
> > + ret = regmap_raw_read(cw_bat->regmap, CW2217_BAT_PROFILE, bat_info, CW2217_PROFILE_SZ);
> > + if (ret) {
> > + dev_err(cw_bat->dev, "Failed to read existing battery profile\n");
> > + return ret;
> > + }
> > +
> > + if (memcmp(bat_info, cw_bat->bat_profile, CW2217_PROFILE_SZ)) {
> > + dev_warn(cw_bat->dev, "Replacing stored battery profile\n");
> > + cw_bat->update_profile = true;
> > + }
> > +
> > + return 0;
> > +}
> > +
> > +static int cw2217_init(struct cw2217_battery *cw_bat)
> > +{
> > + int ret;
> > +
> > + ret = cw2217_get_chip_id(cw_bat);
> > + if (ret)
> > + return dev_err_probe(cw_bat->dev, ret, "I2C read write error");
> > +
> > + /*
> > + * All of the known Cellwise 221x chips set the ID bits to 0xa0. If it
> > + * doesn't match, we're trying to probe the wrong hardware.
> > + */
> > + if (cw_bat->chip_id != CW2217_CHIP_ID)
> > + return dev_err_probe(cw_bat->dev, -ENODEV, "Not a cw221x\n");
> > +
> > + ret = cw2217_check_profile(cw_bat);
> > + if (ret) {
> > + dev_err(cw_bat->dev, "i2c read write error");
> > + return ret;
> > + }
> > +
> > + if (cw_bat->update_profile) {
> > + ret = cw2217_update_config(cw_bat);
> > + if (ret)
> > + return ret;
> > + }
> > +
> > + return 0;
> > +}
> > +
> > +static void cw2217_bat_work(struct work_struct *work)
> > +{
> > + struct delayed_work *delay_work;
> > + struct cw2217_battery *cw_bat;
> > + int ret;
> > +
> > + delay_work = container_of(work,
> > + struct delayed_work,
> > + work);
> > +
> > + cw_bat = container_of(delay_work,
> > + struct cw2217_battery,
> > + battery_delay_work);
> > +
> > + ret = cw2217_update_data(cw_bat);
> > + if (ret)
> > + dev_err(cw_bat->dev, "i2c read error when update data");
> > +
> > + queue_delayed_work(cw_bat->battery_workqueue,
> > + &cw_bat->battery_delay_work,
> > + msecs_to_jiffies(cw_bat->poll_interval_ms));
> > +}
> > +
> > +static int cw2217_battery_get_property(struct power_supply *psy,
> > + enum power_supply_property psp,
> > + union power_supply_propval *val)
> > +{
> > + struct cw2217_battery *cw_bat = power_supply_get_drvdata(psy);
> > + int ret = 0;
> > +
> > + switch (psp) {
> > + case POWER_SUPPLY_PROP_CYCLE_COUNT:
> > + val->intval = cw_bat->cycle;
> > + break;
> > + case POWER_SUPPLY_PROP_CAPACITY:
> > + val->intval = cw_bat->soc;
> > + break;
> > + case POWER_SUPPLY_PROP_STATUS:
> > + /*
> > + * Observed when battery was full that it would report
> > + * approximately -12mv at between 97-99 SoC. Assume that if the
> > + * battery current is above 0 it's charging, it's below
> > + * 0 but with an SoC 95% or above it's not charging because
> > + * it's full, otherwise it's simply not charging. If it's not
> > + * supplied it is discharging.
> > + */
> > + if (power_supply_is_system_supplied()) {
> > + if (cw_bat->cw_current > 0)
> > + val->intval = POWER_SUPPLY_STATUS_CHARGING;
> > + else if (cw_bat->soc >= 95)
> > + val->intval = POWER_SUPPLY_STATUS_FULL;
> > + else
> > + val->intval = POWER_SUPPLY_STATUS_NOT_CHARGING;
> > + } else
> > + val->intval = POWER_SUPPLY_STATUS_DISCHARGING;
> > + break;
> > + case POWER_SUPPLY_PROP_PRESENT:
> > + val->intval = (cw_bat->voltage <= 0) ? 0 : 1;
> > + break;
> > + case POWER_SUPPLY_PROP_VOLTAGE_NOW:
> > + val->intval = cw_bat->voltage;
> > + break;
> > + case POWER_SUPPLY_PROP_CURRENT_NOW:
> > + val->intval = cw_bat->cw_current;
> > + break;
> > + case POWER_SUPPLY_PROP_STATE_OF_HEALTH:
> > + val->intval = cw_bat->soh;
> > + break;
> > + case POWER_SUPPLY_PROP_TEMP:
> > + val->intval = cw_bat->temp;
> > + break;
> > + default:
> > + ret = -EINVAL;
> > + break;
> > + }
> > +
> > + return ret;
> > +}
> > +
> > +static enum power_supply_property cw2217_battery_properties[] = {
> > + POWER_SUPPLY_PROP_CYCLE_COUNT,
> > + POWER_SUPPLY_PROP_CAPACITY,
> > + POWER_SUPPLY_PROP_PRESENT,
> > + POWER_SUPPLY_PROP_VOLTAGE_NOW,
> > + POWER_SUPPLY_PROP_CURRENT_NOW,
> > + POWER_SUPPLY_PROP_STATUS,
> > + POWER_SUPPLY_PROP_STATE_OF_HEALTH,
> > + POWER_SUPPLY_PROP_TEMP,
> > +};
> > +
> > +static const struct power_supply_desc cw2217_bat_desc = {
> > + .name = "cw2217-battery",
> > + .type = POWER_SUPPLY_TYPE_BATTERY,
> > + .properties = cw2217_battery_properties,
> > + .num_properties = ARRAY_SIZE(cw2217_battery_properties),
> > + .get_property = cw2217_battery_get_property,
> > +};
> > +
> > +static const struct regmap_range cw2217_ranges_rd_yes[] = {
> > + regmap_reg_range(CW2217_VERSION, CW2217_VERSION),
> > + regmap_reg_range(CW2217_VCELL_H, CW2217_TEMP),
> > + regmap_reg_range(CW2217_CONFIG, CW2217_CONFIG),
> > + regmap_reg_range(CW2217_INT_CONF, CW2217_CURRENT_L),
> > + regmap_reg_range(CW2217_BAT_PROFILE,
> > + CW2217_BAT_PROFILE + CW2217_PROFILE_SZ - 1),
> > + regmap_reg_range(CW2217_T_HOST_H, CW2217_USER_CONF),
> > + regmap_reg_range(CW2217_CYCLECNT_H, CW2217_STB_CUR_L),
> > + regmap_reg_range(CW2217_FW_VERSION, CW2217_FW_VERSION),
> > +};
> > +
> > +static const struct regmap_access_table cw2217_rd_table = {
> > + .yes_ranges = cw2217_ranges_rd_yes,
> > + .n_yes_ranges = ARRAY_SIZE(cw2217_ranges_rd_yes),
> > +};
> > +
> > +static const struct regmap_range cw2217_ranges_wr_yes[] = {
> > + regmap_reg_range(CW2217_TEMP, CW2217_TEMP),
> > + regmap_reg_range(CW2217_CONFIG, CW2217_CONFIG),
> > + regmap_reg_range(CW2217_INT_CONF, CW2217_CURRENT_L),
> > + regmap_reg_range(CW2217_BAT_PROFILE,
> > + CW2217_BAT_PROFILE + CW2217_PROFILE_SZ - 1),
> > + regmap_reg_range(CW2217_T_HOST_H, CW2217_USER_CONF),
> > +};
> > +
> > +static const struct regmap_access_table cw2217_wr_table = {
> > + .yes_ranges = cw2217_ranges_wr_yes,
> > + .n_yes_ranges = ARRAY_SIZE(cw2217_ranges_wr_yes),
> > +};
> > +
> > +static const struct regmap_range cw2217_ranges_vol_yes[] = {
> > + regmap_reg_range(CW2217_VCELL_H, CW2217_TEMP),
> > + regmap_reg_range(CW2217_INT_CONF, CW2217_SOC_ALERT),
> > + regmap_reg_range(CW2217_CURRENT_H, CW2217_CURRENT_L),
> > + regmap_reg_range(CW2217_CYCLECNT_H, CW2217_STB_CUR_L),
> > +};
> > +
> > +static const struct regmap_access_table cw2217_vol_table = {
> > + .yes_ranges = cw2217_ranges_vol_yes,
> > + .n_yes_ranges = ARRAY_SIZE(cw2217_ranges_vol_yes),
> > +};
> > +
> > +static const struct regmap_config cw2217_regmap_config = {
> > + .reg_bits = 8,
> > + .val_bits = 8,
> > + .rd_table = &cw2217_rd_table,
> > + .wr_table = &cw2217_wr_table,
> > + .volatile_table = &cw2217_vol_table,
> > + .cache_type = REGCACHE_MAPLE,
> > + .max_register = CW2217_FW_VERSION,
> > +};
> > +
> > +static int cw2217_probe(struct i2c_client *client)
> > +{
> > + struct power_supply_config psy_cfg = { 0 };
> > + struct cw2217_battery *cw_bat;
> > + int ret;
> > +
> > + cw_bat = devm_kzalloc(&client->dev, sizeof(*cw_bat), GFP_KERNEL);
> > + if (!cw_bat)
> > + return -ENOMEM;
> > +
> > + i2c_set_clientdata(client, cw_bat);
> > + cw_bat->dev = &client->dev;
> > +
> > + cw_bat->regmap = devm_regmap_init_i2c(client, &cw2217_regmap_config);
> > + if (IS_ERR(cw_bat->regmap))
> > + return dev_err_probe(cw_bat->dev, PTR_ERR(cw_bat->regmap),
> > + "Failed to allocate regmap: %ld\n",
> > + PTR_ERR(cw_bat->regmap));
> > +
> > + ret = cw2217_parse_properties(cw_bat);
> > + if (ret)
> > + return ret;
> > +
> > + ret = cw2217_init(cw_bat);
> > + if (ret)
> > + return dev_err_probe(cw_bat->dev, ret, "cw2217 init fail\n");
> > +
> > + ret = cw2217_get_fw_version(cw_bat);
> > + if (ret)
> > + return dev_err_probe(cw_bat->dev, ret,
> > + "Unable to read fw version\n");
> > +
> > + ret = cw2217_update_data(cw_bat);
> > + if (ret)
> > + return dev_err_probe(cw_bat->dev, ret,
> > + "Unable to read initial data\n");
> > +
> > + psy_cfg.drv_data = cw_bat;
> > + cw_bat->cw_bat = devm_power_supply_register(&client->dev,
> > + &cw2217_bat_desc, &psy_cfg);
> > + if (IS_ERR(cw_bat->cw_bat))
> > + return dev_err_probe(cw_bat->dev, PTR_ERR(cw_bat->cw_bat),
> > + "Failed to register battery: %ld\n",
> > + PTR_ERR(cw_bat->cw_bat));
> > +
> > + cw_bat->battery_workqueue = devm_alloc_ordered_workqueue(&client->dev,
> > + "cw2217_gauge", 0);
> > + if (!cw_bat->battery_workqueue)
> > + return -ENOMEM;
> > +
> > + ret = devm_delayed_work_autocancel(&client->dev,
> > + &cw_bat->battery_delay_work,
> > + cw2217_bat_work);
> > + if (ret)
> > + return dev_err_probe(&client->dev, ret,
> > + "Failed to register delayed work\n");
> > +
> > + queue_delayed_work(cw_bat->battery_workqueue,
> > + &cw_bat->battery_delay_work,
> > + msecs_to_jiffies(50));
> > +
> > + return 0;
> > +}
> > +
> > +static int __maybe_unused cw2217_bat_suspend(struct device *dev)
> > +{
> > + struct i2c_client *client = to_i2c_client(dev);
> > + struct cw2217_battery *cw_bat = i2c_get_clientdata(client);
> > +
> > + cancel_delayed_work(&cw_bat->battery_delay_work);
> > + return 0;
> > +}
> > +
> > +static int __maybe_unused cw2217_bat_resume(struct device *dev)
> > +{
> > + struct i2c_client *client = to_i2c_client(dev);
> > + struct cw2217_battery *cw_bat = i2c_get_clientdata(client);
> > +
> > + queue_delayed_work(cw_bat->battery_workqueue,
> > + &cw_bat->battery_delay_work,
> > + msecs_to_jiffies(20));
> > + return 0;
> > +}
> > +
> > +static SIMPLE_DEV_PM_OPS(cw2217_bat_pm_ops, cw2217_bat_suspend,
> > + cw2217_bat_resume);
> > +
> > +static const struct i2c_device_id cw2217_id_table[] = {
> > + { .name = "cw2215" },
> > + { .name = "cw2217" },
> > + { .name = "cw2218" },
> > + { }
> > +};
> > +MODULE_DEVICE_TABLE(i2c, cw2217_id_table);
> > +
> > +static const struct of_device_id cw2217_match_table[] = {
> > + { .compatible = "cellwise,cw2215", },
> > + { .compatible = "cellwise,cw2217", },
> > + { .compatible = "cellwise,cw2218", },
> > + { },
> > +};
> > +MODULE_DEVICE_TABLE(of, cw2217_match_table);
> > +
> > +static struct i2c_driver cw2217_driver = {
> > + .driver = {
> > + .name = "cw2217",
> > + .pm = &cw2217_bat_pm_ops,
> > + .of_match_table = of_match_ptr(cw2217_match_table),
> > + },
> > + .probe = cw2217_probe,
> > + .id_table = cw2217_id_table,
> > +};
> > +
> > +module_i2c_driver(cw2217_driver);
> > +
> > +MODULE_AUTHOR("Xu Shengfei <xsf@rock-chips.com>");
> > +MODULE_AUTHOR("Chris Morgan <macromorgan@hotmail.com>");
> > +MODULE_DESCRIPTION("cw2217 battery driver");
> > +MODULE_LICENSE("GPL");
>
^ permalink raw reply [flat|nested] 7+ messages in thread
* Re: [PATCH 1/3] dt-bindings: power: cw2015: add cw2215, cw2217, and cw2218
2026-10-27 13:17 ` [PATCH 1/3] dt-bindings: power: cw2015: add cw2215, cw2217, and cw2218 Chris Morgan
@ 2026-10-27 15:57 ` Krzysztof Kozlowski
0 siblings, 0 replies; 7+ messages in thread
From: Krzysztof Kozlowski @ 2026-10-27 15:57 UTC (permalink / raw)
To: Chris Morgan, linux-pm
Cc: devicetree, linux-rockchip, heiko, conor+dt, krzk+dt, robh, sre,
t.schramm, Chris Morgan
On 27/10/2026 14:17, Chris Morgan wrote:
>
> -allOf:
> - - $ref: power-supply.yaml#
> -
> properties:
> compatible:
> - const: cellwise,cw2015
> + enum:
> + - cellwise,cw2015
> + - cellwise,cw2215
> + - cellwise,cw2217
> + - cellwise,cw2218
>
> reg:
> maxItems: 1
> @@ -31,8 +32,6 @@ properties:
> it is to mail two batteries to a test facility of CellWise and receive
> back a test report with the binary blob.
> $ref: /schemas/types.yaml#/definitions/uint8-array
> - minItems: 64
> - maxItems: 64
This needs min/maxItems constraints.
>
> cellwise,monitor-interval-ms:
> description:
> @@ -41,7 +40,14 @@ properties:
>
> power-supplies:
> minItems: 1
> - maxItems: 8 # Should be enough
> + maxItems: 8 # Should be enough
Why changing? yamllint complained?
> +
> + shunt-resistor-micro-ohms:
> + description: |
Do not need '|' unless you need to preserve formatting.
> + The value, in micro-ohms, of the shunt resistor used for sampling
> + the battery current.
> + minimum: 1000000
> + maximum: 50000000
>
> monitored-battery: true
>
> @@ -49,6 +55,28 @@ required:
> - compatible
> - reg
>
> +allOf:
> + - $ref: power-supply.yaml#
> + - if:
> + properties:
> + compatible:
> + contains:
> + enum:
> + - cellwise,cw2015
> + then:
> + properties:
> + shunt-resistor-micro-ohms: false
> +
> + cellwise,battery-profile:
> + minItems: 64
> + maxItems: 64
> +
> + else:
> + properties:
> + cellwise,battery-profile:
> + minItems: 80
> + maxItems: 80
default for shunt-resistor-micro-ohms? or is it required for new devices?
Best regards,
Krzysztof
^ permalink raw reply [flat|nested] 7+ messages in thread
end of thread, other threads:[~2026-10-27 15:57 UTC | newest]
Thread overview: 7+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2026-10-27 13:17 [PATCH 0/3] Add Cellwise CW2217 Chris Morgan
2026-10-27 13:17 ` [PATCH 1/3] dt-bindings: power: cw2015: add cw2215, cw2217, and cw2218 Chris Morgan
2026-10-27 15:57 ` Krzysztof Kozlowski
2026-10-27 13:17 ` [PATCH 2/3] power: supply: cw2217: Add support for Cellwise cw2217 Chris Morgan
2026-10-27 13:34 ` Shawn Lin
2026-10-27 15:46 ` Chris Morgan
2026-10-27 13:17 ` [PATCH 3/3] arm64: dts: rockchip: Add Battery Gauge to Anbernic Vita-Pro Chris Morgan
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox