From mboxrd@z Thu Jan 1 00:00:00 1970 Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.remote-ip=209.85.160.54 ARC-Seal: i=1; d=subspace.kernel.org; s=arc20261015; a=rsa-sha256; cv=none; t=1793107077; b=RLApNiYZf1eqCGqhyrrAsxRMBGtt40/Y7Z71cIoWi3yxgJT7aCJIsk3N8cqiaAE/agn4 fdpihFJgowKyKu45hj9ZmZJbj38qUuvyt3C0nuSZnJbdan8bk6RpTHnUe0r+HApk1ObGg Lo0c5A1mZ3Dce+rvqxZ7mAYucCJBTPz5nfMtzuHwt7AGjoPuA7C/6GZA3sZrZRgLClYvH DnwSW4/1TF3tOIySet7WKpcQ8hBBKufoPnhzrLsNisSZ5ZfvdQ0shAv4hJVAzsbWFgQ/t YmXqPxI29sTVHzybcAWM9BM8g7e03enW5TzAR74o6Y1tGYni/OTeEEZnOtWOe+I0YWw== ARC-Message-Signature: i=1; d=subspace.kernel.org; s=arc20261015; a=rsa-sha256; c=relaxed/relaxed; t=1793107077; h=DMARC-Filter:Received:DKIM-Signature:X-Google-DKIM-Signature: X-Gm-Message-State:X-Gm-Gg:X-Received:Received:From:To:Cc:Subject: Date:Message-ID:X-Mailer:In-Reply-To:References:MIME-Version: Content-Transfer-Encoding; bh=e+o8GD5Tfs8+zxqN8UKhZzp0BXr7+HuUYJw/GgZn0QE=; b=IK7m+LbP4s0EBdJgkdgxAe7eRt6et5jY8tiet/JQlN0LPYWYVeMw2BnAswzz04V4c+L0 U39w9x4A3YZxcZVGhX7wDFNH70ugvPE/kWw1Y/HMDaUup8ArWJ8zCMr2fVnbg9SPQ7vgc RYY2HaeWpr9PJ5kfTWuyF6N5AcUakGqWy14apW22f5PnP2Ys+TGJMKo4G9M9YXk5oUpk6 EURK0qmQVQr9JeGrTwGkFCb/iqtFhm40oTh5V5OJyMIbifdyK6bye8Ta+1JDfDCyrL7NA cpvoLWYrE9/uv2oEaXlhBZim2JhX5GEftGtXixtp0ZDkLBNrQuk8K3goUNlT3fjU1gg== ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass header.from=gmail.com; spf=pass smtp.mailfrom=gmail.com; dkim=pass header.d=gmail.com header.i=@gmail.com header.a=rsa-sha256 header.s=20251104 header.b=j4hh1wrC; arc=none smtp.remote-ip=209.85.160.54 Received: from mail-oa1-f54.google.com (mail-oa1-f54.google.com [209.85.160.54]) (using TLSv1.3 with cipher TLS_AES_128_GCM_SHA256 (128/128 bits) key-exchange X25519 server-signature ECDSA (prime256v1) server-digest SHA256) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id B8543606A326 for ; Tue, 27 Oct 2026 13:17:56 +0000 (UTC) DMARC-Filter: OpenDMARC Filter v1.4.2 smtp.subspace.kernel.org B8543606A326 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=gmail.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=gmail.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key, unprotected) header.d=gmail.com header.i=@gmail.com header.a=rsa-sha256 header.s=20251104 header.b=j4hh1wrC Received: by mail-oa1-f54.google.com with SMTP id 586e51a60fabf-48ed0b68895so2124011fac.1 for ; Tue, 27 Oct 2026 06:17:56 -0700 (PDT) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20251104; t=1793107076; x=1793711876; darn=vger.kernel.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:from:to:cc:subject:date :message-id:reply-to:content-type; bh=e+o8GD5Tfs8+zxqN8UKhZzp0BXr7+HuUYJw/GgZn0QE=; b=j4hh1wrCC21HzBG3U99n0UAUSIaXS5fJ/YhkdadmYoMgJe6NuZFVRcxGS5+aBQQclE dIj/dp7BW0f76w/NK2VN+1Ip7XbZVG7lLwwSn5AdPCqCNc+IqJ9KFHuZVvZlRXoUx744 1kiNeXCXDB4GMpfGUCJ+0WJlLU07Dzdf4zLwarbfoizg6fOnE9AJtLrNZZjfAsSWSLfU 7LSxRjCZcI4MJJJQ25n5N0nji4VzpHMbHhuUUwe1pOrnn05xXEzwjaRD2mB1vniCSGn2 kHQAyhz3k2VTx9a1CUInfMB6+99o/iplDi7NCNsJOWdV0lOFvSoPvyWgcBhfpHWnTqxy 3ZvA== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20260707; t=1793107076; x=1793711876; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:cc:to:from:x-gm-gg:x-gm-message-state:from :to:cc:subject:date:message-id:reply-to:content-type; bh=e+o8GD5Tfs8+zxqN8UKhZzp0BXr7+HuUYJw/GgZn0QE=; b=WR637NXnVwSVCSxHODRCK8sV76L7pM0Ph2A4GgNGReNQW49s3hNRhFyb15k/vJ79rT G6mXOcvJG0cc9oZDT+nsIZMm8akXFDm8A94xvmAJvEJTxpnAY5KKaJ34hj0sxegFzqGM frg21BR2we0Ry/87dbdHkj55VwylLDZNVYSkMzgC3WWvdpKdq1tQUxgX8s3wTM/6wn3C ji3ww54cRH2QoAEa0jF2utfCTGckaKjKvU4LJs/3bTFnJDFzcFfj6yZhyl0hkisjp7NE Arz6PelZVQsIXdq7ZJgq2FtbQkWw1NDFgryVqD/B3DfZWE+x6PdAxLPvGWWXxwYQmg8p OFLQ== X-Gm-Message-State: AFq9FYJ/CvcwIho2svBssRiiGCK2rLr+mEsTFxm8x54mdUhlU66JCHAU IqG7LjJ7eafMruAqdptgt6uUjmXhyrgEszqaXvZgGOIJfl7OinsWzQjB X-Gm-Gg: AYBFou2TLe94KyVAC/rMprB/xMhw94xSJIZy3+ijSPEPjjS20wMxS1men1stLmWOLdG kYT77nOznnTj4TJ7l5or8563HEVG2wH0uTh8lRY0nNrxBF6pTc6fectFh5DtpyMY2uL1Uk41PcH b4eJm9JuWN1cf6+On234R5sOhLtMHymaDladVxt9GGKgXmvvmjbO2gw4MxaTVF3OXI3Vo4kaNeY IHo1O4KcQtv5jZgn9JPt2lnj4mgdCp+RMkd680cTDpwWGVFPAp4BDN4xSLvdv7LC7HYnyjn59t0 42aNT+XvZ/DX+VwID8kEbP3GGCLg+e+lQySWupo5/ZseqOoxGCD3kSepazTghvU10jDkjixr9BF 1TZR0LAG3vM7x3wJ+4LQZpOwGnoshOwJjYHBPV9/nv/j2I/RKPev7GUIc22kVxYLSPS3yx+mPPy QDQXLevJP0L7qOguhVuyW2D2QMK/hNmpJdamH2yFAAiwd/ZbcNCSbX3J+G6n1o3JZvP1RPUgbE6 EQEt/ndFa3Np5LNAs5p4HU7LqxmgeC/I7VrYV6o/5HOcgWPzND0QEQjXQ== X-Received: by 2002:a05:6870:c22f:b0:483:bdb3:a524 with SMTP id 586e51a60fabf-4a83f1cb6eamr5837285fac.4.1793107075599; Tue, 27 Oct 2026 06:17:55 -0700 (PDT) Received: from neuromancer ([75.28.21.198]) by smtp.gmail.com with ESMTPSA id 586e51a60fabf-4a8b74c0a7fsm23412fac.2.2026.10.27.06.17.55 (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Tue, 27 Oct 2026 06:17:55 -0700 (PDT) From: Chris Morgan 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 Subject: [PATCH 2/3] power: supply: cw2217: Add support for Cellwise cw2217 Date: Tue, 27 Oct 2026 08:17:50 -0500 Message-ID: <20261027131751.1724448-3-macroalpha82@gmail.com> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20261027131751.1724448-1-macroalpha82@gmail.com> References: <20261027131751.1724448-1-macroalpha82@gmail.com> X-Mailing-List: devicetree@vger.kernel.org List-Id: List-Post: List-Help: List-Subscribe: List-Unsubscribe: List-Owner: Precedence: list MIME-Version: 1.0 Content-Transfer-Encoding: 8bit From: Chris Morgan 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 --- 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 + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#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 "); +MODULE_AUTHOR("Chris Morgan "); +MODULE_DESCRIPTION("cw2217 battery driver"); +MODULE_LICENSE("GPL"); -- 2.53.0