From: Chris Morgan <macroalpha82@gmail.com>
To: linux-pm@vger.kernel.org
Cc: devicetree@vger.kernel.org, linux-rockchip@lists.infradead.org,
heiko@sntech.de, conor+dt@kernel.org, krzk+dt@kernel.org,
robh@kernel.org, sre@kernel.org, t.schramm@manjaro.org,
Chris Morgan <macromorgan@hotmail.com>
Subject: [PATCH 2/3] power: supply: cw2217: Add support for Cellwise cw2217
Date: Tue, 27 Oct 2026 08:17:50 -0500 [thread overview]
Message-ID: <20261027131751.1724448-3-macroalpha82@gmail.com> (raw)
In-Reply-To: <20261027131751.1724448-1-macroalpha82@gmail.com>
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
WARNING: multiple messages have this Message-ID (diff)
From: Chris Morgan <macroalpha82@gmail.com>
To: linux-pm@vger.kernel.org
Cc: devicetree@vger.kernel.org, linux-rockchip@lists.infradead.org,
heiko@sntech.de, conor+dt@kernel.org, krzk+dt@kernel.org,
robh@kernel.org, sre@kernel.org, t.schramm@manjaro.org,
Chris Morgan <macromorgan@hotmail.com>
Subject: [PATCH 2/3] power: supply: cw2217: Add support for Cellwise cw2217
Date: Tue, 27 Oct 2026 08:17:50 -0500 [thread overview]
Message-ID: <20261027131751.1724448-3-macroalpha82@gmail.com> (raw)
In-Reply-To: <20261027131751.1724448-1-macroalpha82@gmail.com>
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
_______________________________________________
Linux-rockchip mailing list
Linux-rockchip@lists.infradead.org
http://lists.infradead.org/mailman/listinfo/linux-rockchip
next prev parent reply other threads:[~2026-10-27 13:17 UTC|newest]
Thread overview: 19+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-27 13:17 [PATCH 0/3] Add Cellwise CW2217 Chris Morgan
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
2026-10-27 13:17 ` Chris Morgan
2026-10-27 13:22 ` sashiko-bot
2026-10-27 15:57 ` Krzysztof Kozlowski
2026-10-27 15:57 ` Krzysztof Kozlowski
2026-10-27 13:17 ` Chris Morgan [this message]
2026-10-27 13:17 ` [PATCH 2/3] power: supply: cw2217: Add support for Cellwise cw2217 Chris Morgan
2026-10-27 13:31 ` sashiko-bot
2026-10-27 13:34 ` Shawn Lin
2026-10-27 13:34 ` Shawn Lin
2026-10-27 15:46 ` Chris Morgan
2026-10-27 15:46 ` Chris Morgan
2026-10-29 6:24 ` kernel test robot
2026-10-29 6:24 ` kernel test robot
2026-10-27 13:17 ` [PATCH 3/3] arm64: dts: rockchip: Add Battery Gauge to Anbernic Vita-Pro Chris Morgan
2026-10-27 13:17 ` Chris Morgan
2026-10-27 13:32 ` sashiko-bot
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20261027131751.1724448-3-macroalpha82@gmail.com \
--to=macroalpha82@gmail.com \
--cc=conor+dt@kernel.org \
--cc=devicetree@vger.kernel.org \
--cc=heiko@sntech.de \
--cc=krzk+dt@kernel.org \
--cc=linux-pm@vger.kernel.org \
--cc=linux-rockchip@lists.infradead.org \
--cc=macromorgan@hotmail.com \
--cc=robh@kernel.org \
--cc=sre@kernel.org \
--cc=t.schramm@manjaro.org \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.