[PATCH v2 3/3] power: supply: Add a driver for Injoinic power bank ICs

From: Samuel Holland
Date: Sun Feb 13 2022 - 23:23:19 EST


This driver supports several chip variants which all share the same I2C
register interface. Since the chip will turn off and become inaccessible
under conditions outside of software control (e.g. upon button press or
input voltage removal), some special handling is needed to delay the
initialization of the IC until it is accessible.

Signed-off-by: Samuel Holland <samuel@xxxxxxxxxxxx>
---

Changes in v2:
- Add check for allocation failure
- Rename "charger" to "battery" for accuracy
- Add separate supply for USB 5V boost output stage
- Move POWER_SUPPLY_PROP_ONLINE to the boost supply
- Add POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN to the boost supply

MAINTAINERS | 5 +
drivers/power/supply/Kconfig | 8 +
drivers/power/supply/Makefile | 1 +
drivers/power/supply/ip5xxx_power.c | 638 ++++++++++++++++++++++++++++
4 files changed, 652 insertions(+)
create mode 100644 drivers/power/supply/ip5xxx_power.c

diff --git a/MAINTAINERS b/MAINTAINERS
index fca970a46e77..5c8ef361a69b 100644
--- a/MAINTAINERS
+++ b/MAINTAINERS
@@ -9475,6 +9475,11 @@ F: include/linux/mfd/ingenic-tcu.h
F: sound/soc/codecs/jz47*
F: sound/soc/jz4740/

+INJOINIC IP5xxx POWER BANK IC DRIVER
+M: Samuel Holland <samuel@xxxxxxxxxxxx>
+S: Maintained
+F: drivers/power/supply/ip5xxx_power.c
+
INOTIFY
M: Jan Kara <jack@xxxxxxx>
R: Amir Goldstein <amir73il@xxxxxxxxx>
diff --git a/drivers/power/supply/Kconfig b/drivers/power/supply/Kconfig
index b366e2fd8e97..539f66018ec4 100644
--- a/drivers/power/supply/Kconfig
+++ b/drivers/power/supply/Kconfig
@@ -51,6 +51,14 @@ config GENERIC_ADC_BATTERY
Say Y here to enable support for the generic battery driver
which uses IIO framework to read adc.

+config IP5XXX_POWER
+ tristate "Injoinic IP5xxx power bank IC driver"
+ depends on I2C
+ select REGMAP_I2C
+ help
+ Say Y to include support for Injoinic IP5xxx power bank ICs,
+ which include a battery charger and a boost converter.
+
config MAX8925_POWER
tristate "MAX8925 battery charger support"
depends on MFD_MAX8925
diff --git a/drivers/power/supply/Makefile b/drivers/power/supply/Makefile
index 2c1b264b2046..7a70c740324e 100644
--- a/drivers/power/supply/Makefile
+++ b/drivers/power/supply/Makefile
@@ -12,6 +12,7 @@ obj-$(CONFIG_GENERIC_ADC_BATTERY) += generic-adc-battery.o
obj-$(CONFIG_PDA_POWER) += pda_power.o
obj-$(CONFIG_APM_POWER) += apm_power.o
obj-$(CONFIG_AXP20X_POWER) += axp20x_usb_power.o
+obj-$(CONFIG_IP5XXX_POWER) += ip5xxx_power.o
obj-$(CONFIG_MAX8925_POWER) += max8925_power.o
obj-$(CONFIG_WM831X_BACKUP) += wm831x_backup.o
obj-$(CONFIG_WM831X_POWER) += wm831x_power.o
diff --git a/drivers/power/supply/ip5xxx_power.c b/drivers/power/supply/ip5xxx_power.c
new file mode 100644
index 000000000000..218e8e689a3f
--- /dev/null
+++ b/drivers/power/supply/ip5xxx_power.c
@@ -0,0 +1,638 @@
+// SPDX-License-Identifier: GPL-2.0
+//
+// Copyright (C) 2021 Samuel Holland <samuel@xxxxxxxxxxxx>
+
+#include <linux/i2c.h>
+#include <linux/module.h>
+#include <linux/power_supply.h>
+#include <linux/regmap.h>
+
+#define IP5XXX_SYS_CTL0 0x01
+#define IP5XXX_SYS_CTL0_WLED_DET_EN BIT(4)
+#define IP5XXX_SYS_CTL0_WLED_EN BIT(3)
+#define IP5XXX_SYS_CTL0_BOOST_EN BIT(2)
+#define IP5XXX_SYS_CTL0_CHARGER_EN BIT(1)
+#define IP5XXX_SYS_CTL1 0x02
+#define IP5XXX_SYS_CTL1_LIGHT_SHDN_EN BIT(1)
+#define IP5XXX_SYS_CTL1_LOAD_PWRUP_EN BIT(0)
+#define IP5XXX_SYS_CTL2 0x0c
+#define IP5XXX_SYS_CTL2_LIGHT_SHDN_TH GENMASK(7, 3)
+#define IP5XXX_SYS_CTL3 0x03
+#define IP5XXX_SYS_CTL3_LONG_PRESS_TIME_SEL GENMASK(7, 6)
+#define IP5XXX_SYS_CTL3_BTN_SHDN_EN BIT(5)
+#define IP5XXX_SYS_CTL4 0x04
+#define IP5XXX_SYS_CTL4_SHDN_TIME_SEL GENMASK(7, 6)
+#define IP5XXX_SYS_CTL4_VIN_PULLOUT_BOOST_EN BIT(5)
+#define IP5XXX_SYS_CTL5 0x07
+#define IP5XXX_SYS_CTL5_NTC_DIS BIT(6)
+#define IP5XXX_SYS_CTL5_WLED_MODE_SEL BIT(1)
+#define IP5XXX_SYS_CTL5_BTN_SHDN_SEL BIT(0)
+#define IP5XXX_CHG_CTL1 0x22
+#define IP5XXX_CHG_CTL1_BOOST_UVP_SEL GENMASK(3, 2)
+#define IP5XXX_CHG_CTL2 0x24
+#define IP5XXX_CHG_CTL2_BAT_TYPE_SEL GENMASK(6, 5)
+#define IP5XXX_CHG_CTL2_BAT_TYPE_SEL_4_2V (0x0 << 5)
+#define IP5XXX_CHG_CTL2_BAT_TYPE_SEL_4_3V (0x1 << 5)
+#define IP5XXX_CHG_CTL2_BAT_TYPE_SEL_4_35V (0x2 << 5)
+#define IP5XXX_CHG_CTL2_CONST_VOLT_SEL GENMASK(2, 1)
+#define IP5XXX_CHG_CTL4 0x26
+#define IP5XXX_CHG_CTL4_BAT_TYPE_SEL_EN BIT(6)
+#define IP5XXX_CHG_CTL4A 0x25
+#define IP5XXX_CHG_CTL4A_CONST_CUR_SEL GENMASK(4, 0)
+#define IP5XXX_MFP_CTL0 0x51
+#define IP5XXX_MFP_CTL1 0x52
+#define IP5XXX_GPIO_CTL2 0x53
+#define IP5XXX_GPIO_CTL2A 0x54
+#define IP5XXX_GPIO_CTL3 0x55
+#define IP5XXX_READ0 0x71
+#define IP5XXX_READ0_CHG_STAT GENMASK(7, 5)
+#define IP5XXX_READ0_CHG_STAT_IDLE (0x0 << 5)
+#define IP5XXX_READ0_CHG_STAT_TRICKLE (0x1 << 5)
+#define IP5XXX_READ0_CHG_STAT_CONST_VOLT (0x2 << 5)
+#define IP5XXX_READ0_CHG_STAT_CONST_CUR (0x3 << 5)
+#define IP5XXX_READ0_CHG_STAT_CONST_VOLT_STOP (0x4 << 5)
+#define IP5XXX_READ0_CHG_STAT_FULL (0x5 << 5)
+#define IP5XXX_READ0_CHG_STAT_TIMEOUT (0x6 << 5)
+#define IP5XXX_READ0_CHG_OP BIT(4)
+#define IP5XXX_READ0_CHG_END BIT(3)
+#define IP5XXX_READ0_CONST_VOLT_TIMEOUT BIT(2)
+#define IP5XXX_READ0_CHG_TIMEOUT BIT(1)
+#define IP5XXX_READ0_TRICKLE_TIMEOUT BIT(0)
+#define IP5XXX_READ0_TIMEOUT GENMASK(2, 0)
+#define IP5XXX_READ1 0x72
+#define IP5XXX_READ1_WLED_PRESENT BIT(7)
+#define IP5XXX_READ1_LIGHT_LOAD BIT(6)
+#define IP5XXX_READ1_VIN_OVERVOLT BIT(5)
+#define IP5XXX_READ2 0x77
+#define IP5XXX_READ2_BTN_PRESS BIT(3)
+#define IP5XXX_READ2_BTN_LONG_PRESS BIT(1)
+#define IP5XXX_READ2_BTN_SHORT_PRESS BIT(0)
+#define IP5XXX_BATVADC_DAT0 0xa2
+#define IP5XXX_BATVADC_DAT1 0xa3
+#define IP5XXX_BATIADC_DAT0 0xa4
+#define IP5XXX_BATIADC_DAT1 0xa5
+#define IP5XXX_BATOCV_DAT0 0xa8
+#define IP5XXX_BATOCV_DAT1 0xa9
+
+struct ip5xxx {
+ struct regmap *regmap;
+ bool initialized;
+};
+
+/*
+ * The IP5xxx charger only responds on I2C when it is "awake". The charger is
+ * generally only awake when VIN is powered or when its boost converter is
+ * enabled. Going into shutdown resets all register values. To handle this:
+ * 1) When any bus error occurs, assume the charger has gone into shutdown.
+ * 2) Attempt the initialization sequence on each subsequent register access
+ * until it succeeds.
+ */
+static int ip5xxx_read(struct ip5xxx *ip5xxx, unsigned int reg,
+ unsigned int *val)
+{
+ int ret;
+
+ ret = regmap_read(ip5xxx->regmap, reg, val);
+ if (ret)
+ ip5xxx->initialized = false;
+
+ return ret;
+}
+
+static int ip5xxx_update_bits(struct ip5xxx *ip5xxx, unsigned int reg,
+ unsigned int mask, unsigned int val)
+{
+ int ret;
+
+ ret = regmap_update_bits(ip5xxx->regmap, reg, mask, val);
+ if (ret)
+ ip5xxx->initialized = false;
+
+ return ret;
+}
+
+static int ip5xxx_initialize(struct power_supply *psy)
+{
+ struct ip5xxx *ip5xxx = power_supply_get_drvdata(psy);
+ int ret;
+
+ if (ip5xxx->initialized)
+ return 0;
+
+ /*
+ * Disable shutdown under light load.
+ * Enable power on when under load.
+ */
+ ret = ip5xxx_update_bits(ip5xxx, IP5XXX_SYS_CTL1,
+ IP5XXX_SYS_CTL1_LIGHT_SHDN_EN |
+ IP5XXX_SYS_CTL1_LOAD_PWRUP_EN,
+ IP5XXX_SYS_CTL1_LOAD_PWRUP_EN);
+ if (ret)
+ return ret;
+
+ /*
+ * Enable shutdown after a long button press (as configured below).
+ */
+ ret = ip5xxx_update_bits(ip5xxx, IP5XXX_SYS_CTL3,
+ IP5XXX_SYS_CTL3_BTN_SHDN_EN,
+ IP5XXX_SYS_CTL3_BTN_SHDN_EN);
+ if (ret)
+ return ret;
+
+ /*
+ * Power on automatically when VIN is removed.
+ */
+ ret = ip5xxx_update_bits(ip5xxx, IP5XXX_SYS_CTL4,
+ IP5XXX_SYS_CTL4_VIN_PULLOUT_BOOST_EN,
+ IP5XXX_SYS_CTL4_VIN_PULLOUT_BOOST_EN);
+ if (ret)
+ return ret;
+
+ /*
+ * Enable the NTC.
+ * Configure the button for two presses => LED, long press => shutdown.
+ */
+ ret = ip5xxx_update_bits(ip5xxx, IP5XXX_SYS_CTL5,
+ IP5XXX_SYS_CTL5_NTC_DIS |
+ IP5XXX_SYS_CTL5_WLED_MODE_SEL |
+ IP5XXX_SYS_CTL5_BTN_SHDN_SEL,
+ IP5XXX_SYS_CTL5_WLED_MODE_SEL |
+ IP5XXX_SYS_CTL5_BTN_SHDN_SEL);
+ if (ret)
+ return ret;
+
+ ip5xxx->initialized = true;
+ dev_dbg(psy->dev.parent, "Initialized after power on\n");
+
+ return 0;
+}
+
+static const enum power_supply_property ip5xxx_battery_properties[] = {
+ POWER_SUPPLY_PROP_STATUS,
+ POWER_SUPPLY_PROP_CHARGE_TYPE,
+ POWER_SUPPLY_PROP_HEALTH,
+ POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN,
+ POWER_SUPPLY_PROP_VOLTAGE_NOW,
+ POWER_SUPPLY_PROP_VOLTAGE_OCV,
+ POWER_SUPPLY_PROP_CURRENT_NOW,
+ POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT,
+ POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX,
+ POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE,
+ POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE_MAX,
+};
+
+static int ip5xxx_battery_get_status(struct ip5xxx *ip5xxx, int *val)
+{
+ unsigned int rval;
+ int ret;
+
+ ret = ip5xxx_read(ip5xxx, IP5XXX_READ0, &rval);
+ if (ret)
+ return ret;
+
+ switch (rval & IP5XXX_READ0_CHG_STAT) {
+ case IP5XXX_READ0_CHG_STAT_IDLE:
+ *val = POWER_SUPPLY_STATUS_DISCHARGING;
+ break;
+ case IP5XXX_READ0_CHG_STAT_TRICKLE:
+ case IP5XXX_READ0_CHG_STAT_CONST_CUR:
+ case IP5XXX_READ0_CHG_STAT_CONST_VOLT:
+ *val = POWER_SUPPLY_STATUS_CHARGING;
+ break;
+ case IP5XXX_READ0_CHG_STAT_CONST_VOLT_STOP:
+ case IP5XXX_READ0_CHG_STAT_FULL:
+ *val = POWER_SUPPLY_STATUS_FULL;
+ break;
+ case IP5XXX_READ0_CHG_STAT_TIMEOUT:
+ *val = POWER_SUPPLY_STATUS_NOT_CHARGING;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static int ip5xxx_battery_get_charge_type(struct ip5xxx *ip5xxx, int *val)
+{
+ unsigned int rval;
+ int ret;
+
+ ret = ip5xxx_read(ip5xxx, IP5XXX_READ0, &rval);
+ if (ret)
+ return ret;
+
+ switch (rval & IP5XXX_READ0_CHG_STAT) {
+ case IP5XXX_READ0_CHG_STAT_IDLE:
+ case IP5XXX_READ0_CHG_STAT_CONST_VOLT_STOP:
+ case IP5XXX_READ0_CHG_STAT_FULL:
+ case IP5XXX_READ0_CHG_STAT_TIMEOUT:
+ *val = POWER_SUPPLY_CHARGE_TYPE_NONE;
+ break;
+ case IP5XXX_READ0_CHG_STAT_TRICKLE:
+ *val = POWER_SUPPLY_CHARGE_TYPE_TRICKLE;
+ break;
+ case IP5XXX_READ0_CHG_STAT_CONST_CUR:
+ case IP5XXX_READ0_CHG_STAT_CONST_VOLT:
+ *val = POWER_SUPPLY_CHARGE_TYPE_STANDARD;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static int ip5xxx_battery_get_health(struct ip5xxx *ip5xxx, int *val)
+{
+ unsigned int rval;
+ int ret;
+
+ ret = ip5xxx_read(ip5xxx, IP5XXX_READ0, &rval);
+ if (ret)
+ return ret;
+
+ if (rval & IP5XXX_READ0_TIMEOUT)
+ *val = POWER_SUPPLY_HEALTH_SAFETY_TIMER_EXPIRE;
+ else
+ *val = POWER_SUPPLY_HEALTH_GOOD;
+
+ return 0;
+}
+
+static int ip5xxx_battery_get_voltage_max(struct ip5xxx *ip5xxx, int *val)
+{
+ unsigned int rval;
+ int ret;
+
+ ret = ip5xxx_read(ip5xxx, IP5XXX_CHG_CTL2, &rval);
+ if (ret)
+ return ret;
+
+ /*
+ * It is not clear what this will return if
+ * IP5XXX_CHG_CTL4_BAT_TYPE_SEL_EN is not set...
+ */
+ switch (rval & IP5XXX_CHG_CTL2_BAT_TYPE_SEL) {
+ case IP5XXX_CHG_CTL2_BAT_TYPE_SEL_4_2V:
+ *val = 4200000;
+ break;
+ case IP5XXX_CHG_CTL2_BAT_TYPE_SEL_4_3V:
+ *val = 4300000;
+ break;
+ case IP5XXX_CHG_CTL2_BAT_TYPE_SEL_4_35V:
+ *val = 4350000;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ return 0;
+}
+
+static int ip5xxx_battery_read_adc(struct ip5xxx *ip5xxx,
+ u8 lo_reg, u8 hi_reg, int *val)
+{
+ unsigned int hi, lo;
+ int ret;
+
+ ret = ip5xxx_read(ip5xxx, lo_reg, &lo);
+ if (ret)
+ return ret;
+
+ ret = ip5xxx_read(ip5xxx, hi_reg, &hi);
+ if (ret)
+ return ret;
+
+ *val = sign_extend32(hi << 8 | lo, 13);
+
+ return 0;
+}
+
+static int ip5xxx_battery_get_property(struct power_supply *psy,
+ enum power_supply_property psp,
+ union power_supply_propval *val)
+{
+ struct ip5xxx *ip5xxx = power_supply_get_drvdata(psy);
+ int raw, ret, vmax;
+ unsigned int rval;
+
+ ret = ip5xxx_initialize(psy);
+ if (ret)
+ return ret;
+
+ switch (psp) {
+ case POWER_SUPPLY_PROP_STATUS:
+ return ip5xxx_battery_get_status(ip5xxx, &val->intval);
+
+ case POWER_SUPPLY_PROP_CHARGE_TYPE:
+ return ip5xxx_battery_get_charge_type(ip5xxx, &val->intval);
+
+ case POWER_SUPPLY_PROP_HEALTH:
+ return ip5xxx_battery_get_health(ip5xxx, &val->intval);
+
+ case POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN:
+ return ip5xxx_battery_get_voltage_max(ip5xxx, &val->intval);
+
+ case POWER_SUPPLY_PROP_VOLTAGE_NOW:
+ ret = ip5xxx_battery_read_adc(ip5xxx, IP5XXX_BATVADC_DAT0,
+ IP5XXX_BATVADC_DAT1, &raw);
+
+ val->intval = 2600000 + DIV_ROUND_CLOSEST(raw * 26855, 100);
+ return 0;
+
+ case POWER_SUPPLY_PROP_VOLTAGE_OCV:
+ ret = ip5xxx_battery_read_adc(ip5xxx, IP5XXX_BATOCV_DAT0,
+ IP5XXX_BATOCV_DAT1, &raw);
+
+ val->intval = 2600000 + DIV_ROUND_CLOSEST(raw * 26855, 100);
+ return 0;
+
+ case POWER_SUPPLY_PROP_CURRENT_NOW:
+ ret = ip5xxx_battery_read_adc(ip5xxx, IP5XXX_BATIADC_DAT0,
+ IP5XXX_BATIADC_DAT1, &raw);
+
+ val->intval = DIV_ROUND_CLOSEST(raw * 745985, 1000);
+ return 0;
+
+ case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT:
+ ret = ip5xxx_read(ip5xxx, IP5XXX_CHG_CTL4A, &rval);
+ if (ret)
+ return ret;
+
+ rval &= IP5XXX_CHG_CTL4A_CONST_CUR_SEL;
+ val->intval = 100000 * rval;
+ return 0;
+
+ case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT_MAX:
+ val->intval = 100000 * 0x1f;
+ return 0;
+
+ case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE:
+ ret = ip5xxx_battery_get_voltage_max(ip5xxx, &vmax);
+ if (ret)
+ return ret;
+
+ ret = ip5xxx_read(ip5xxx, IP5XXX_CHG_CTL2, &rval);
+ if (ret)
+ return ret;
+
+ rval &= IP5XXX_CHG_CTL2_CONST_VOLT_SEL;
+ val->intval = vmax + 14000 * (rval >> 1);
+ return 0;
+
+ case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE_MAX:
+ ret = ip5xxx_battery_get_voltage_max(ip5xxx, &vmax);
+ if (ret)
+ return ret;
+
+ val->intval = vmax + 14000 * 3;
+ return 0;
+
+ default:
+ return -EINVAL;
+ }
+}
+
+static int ip5xxx_battery_set_voltage_max(struct ip5xxx *ip5xxx, int val)
+{
+ unsigned int rval;
+ int ret;
+
+ switch (val) {
+ case 4200000:
+ rval = IP5XXX_CHG_CTL2_BAT_TYPE_SEL_4_2V;
+ break;
+ case 4300000:
+ rval = IP5XXX_CHG_CTL2_BAT_TYPE_SEL_4_3V;
+ break;
+ case 4350000:
+ rval = IP5XXX_CHG_CTL2_BAT_TYPE_SEL_4_35V;
+ break;
+ default:
+ return -EINVAL;
+ }
+
+ ret = ip5xxx_update_bits(ip5xxx, IP5XXX_CHG_CTL2,
+ IP5XXX_CHG_CTL2_BAT_TYPE_SEL, rval);
+ if (ret)
+ return ret;
+
+ ret = ip5xxx_update_bits(ip5xxx, IP5XXX_CHG_CTL4,
+ IP5XXX_CHG_CTL4_BAT_TYPE_SEL_EN,
+ IP5XXX_CHG_CTL4_BAT_TYPE_SEL_EN);
+ if (ret)
+ return ret;
+
+ return 0;
+}
+
+static int ip5xxx_battery_set_property(struct power_supply *psy,
+ enum power_supply_property psp,
+ const union power_supply_propval *val)
+{
+ struct ip5xxx *ip5xxx = power_supply_get_drvdata(psy);
+ unsigned int rval;
+ int ret, vmax;
+
+ ret = ip5xxx_initialize(psy);
+ if (ret)
+ return ret;
+
+ switch (psp) {
+ case POWER_SUPPLY_PROP_STATUS:
+ switch (val->intval) {
+ case POWER_SUPPLY_STATUS_CHARGING:
+ rval = IP5XXX_SYS_CTL0_CHARGER_EN;
+ break;
+ case POWER_SUPPLY_STATUS_DISCHARGING:
+ case POWER_SUPPLY_STATUS_NOT_CHARGING:
+ rval = 0;
+ break;
+ default:
+ return -EINVAL;
+ }
+ return ip5xxx_update_bits(ip5xxx, IP5XXX_SYS_CTL0,
+ IP5XXX_SYS_CTL0_CHARGER_EN, rval);
+
+ case POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN:
+ return ip5xxx_battery_set_voltage_max(ip5xxx, val->intval);
+
+ case POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT:
+ rval = val->intval / 100000;
+ return ip5xxx_update_bits(ip5xxx, IP5XXX_CHG_CTL4A,
+ IP5XXX_CHG_CTL4A_CONST_CUR_SEL, rval);
+
+ case POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE:
+ ret = ip5xxx_battery_get_voltage_max(ip5xxx, &vmax);
+ if (ret)
+ return ret;
+
+ rval = ((val->intval - vmax) / 14000) << 1;
+ return ip5xxx_update_bits(ip5xxx, IP5XXX_CHG_CTL2,
+ IP5XXX_CHG_CTL2_CONST_VOLT_SEL, rval);
+
+ default:
+ return -EINVAL;
+ }
+}
+
+static int ip5xxx_battery_property_is_writeable(struct power_supply *psy,
+ enum power_supply_property psp)
+{
+ return psp == POWER_SUPPLY_PROP_STATUS ||
+ psp == POWER_SUPPLY_PROP_VOLTAGE_MAX_DESIGN ||
+ psp == POWER_SUPPLY_PROP_CONSTANT_CHARGE_CURRENT ||
+ psp == POWER_SUPPLY_PROP_CONSTANT_CHARGE_VOLTAGE;
+}
+
+static const struct power_supply_desc ip5xxx_battery_desc = {
+ .name = "ip5xxx-battery",
+ .type = POWER_SUPPLY_TYPE_BATTERY,
+ .properties = ip5xxx_battery_properties,
+ .num_properties = ARRAY_SIZE(ip5xxx_battery_properties),
+ .get_property = ip5xxx_battery_get_property,
+ .set_property = ip5xxx_battery_set_property,
+ .property_is_writeable = ip5xxx_battery_property_is_writeable,
+};
+
+static const enum power_supply_property ip5xxx_boost_properties[] = {
+ POWER_SUPPLY_PROP_ONLINE,
+ POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN,
+};
+
+static int ip5xxx_boost_get_property(struct power_supply *psy,
+ enum power_supply_property psp,
+ union power_supply_propval *val)
+{
+ struct ip5xxx *ip5xxx = power_supply_get_drvdata(psy);
+ unsigned int rval;
+ int ret;
+
+ ret = ip5xxx_initialize(psy);
+ if (ret)
+ return ret;
+
+ switch (psp) {
+ case POWER_SUPPLY_PROP_ONLINE:
+ ret = ip5xxx_read(ip5xxx, IP5XXX_SYS_CTL0, &rval);
+ if (ret)
+ return ret;
+
+ val->intval = !!(rval & IP5XXX_SYS_CTL0_BOOST_EN);
+ return 0;
+
+ case POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN:
+ ret = ip5xxx_read(ip5xxx, IP5XXX_CHG_CTL1, &rval);
+ if (ret)
+ return ret;
+
+ rval &= IP5XXX_CHG_CTL1_BOOST_UVP_SEL;
+ val->intval = 4530000 + 100000 * (rval >> 2);
+ return 0;
+
+ default:
+ return -EINVAL;
+ }
+}
+
+static int ip5xxx_boost_set_property(struct power_supply *psy,
+ enum power_supply_property psp,
+ const union power_supply_propval *val)
+{
+ struct ip5xxx *ip5xxx = power_supply_get_drvdata(psy);
+ unsigned int rval;
+ int ret;
+
+ ret = ip5xxx_initialize(psy);
+ if (ret)
+ return ret;
+
+ switch (psp) {
+ case POWER_SUPPLY_PROP_ONLINE:
+ rval = val->intval ? IP5XXX_SYS_CTL0_BOOST_EN : 0;
+ return ip5xxx_update_bits(ip5xxx, IP5XXX_SYS_CTL0,
+ IP5XXX_SYS_CTL0_BOOST_EN, rval);
+
+ case POWER_SUPPLY_PROP_VOLTAGE_MIN_DESIGN:
+ rval = ((val->intval - 4530000) / 100000) << 2;
+ return ip5xxx_update_bits(ip5xxx, IP5XXX_CHG_CTL1,
+ IP5XXX_CHG_CTL1_BOOST_UVP_SEL, rval);
+
+ default:
+ return -EINVAL;
+ }
+}
+
+static int ip5xxx_boost_property_is_writeable(struct power_supply *psy,
+ enum power_supply_property psp)
+{
+ return true;
+}
+
+static const struct power_supply_desc ip5xxx_boost_desc = {
+ .name = "ip5xxx-boost",
+ .type = POWER_SUPPLY_TYPE_USB,
+ .properties = ip5xxx_boost_properties,
+ .num_properties = ARRAY_SIZE(ip5xxx_boost_properties),
+ .get_property = ip5xxx_boost_get_property,
+ .set_property = ip5xxx_boost_set_property,
+ .property_is_writeable = ip5xxx_boost_property_is_writeable,
+};
+
+static const struct regmap_config ip5xxx_regmap_config = {
+ .reg_bits = 8,
+ .val_bits = 8,
+ .max_register = IP5XXX_BATOCV_DAT1,
+};
+
+static int ip5xxx_power_probe(struct i2c_client *client)
+{
+ struct power_supply_config psy_cfg = {};
+ struct device *dev = &client->dev;
+ struct power_supply *psy;
+ struct ip5xxx *ip5xxx;
+
+ ip5xxx = devm_kzalloc(dev, sizeof(*ip5xxx), GFP_KERNEL);
+ if (!ip5xxx)
+ return -ENOMEM;
+
+ ip5xxx->regmap = devm_regmap_init_i2c(client, &ip5xxx_regmap_config);
+ if (IS_ERR(ip5xxx->regmap))
+ return PTR_ERR(ip5xxx->regmap);
+
+ psy_cfg.of_node = dev->of_node;
+ psy_cfg.drv_data = ip5xxx;
+
+ psy = devm_power_supply_register(dev, &ip5xxx_battery_desc, &psy_cfg);
+ if (IS_ERR(psy))
+ return PTR_ERR(psy);
+
+ psy = devm_power_supply_register(dev, &ip5xxx_boost_desc, &psy_cfg);
+ if (IS_ERR(psy))
+ return PTR_ERR(psy);
+
+ return 0;
+}
+
+static const struct of_device_id ip5xxx_power_of_match[] = {
+ { .compatible = "injoinic,ip5108" },
+ { .compatible = "injoinic,ip5109" },
+ { .compatible = "injoinic,ip5207" },
+ { .compatible = "injoinic,ip5209" },
+ { }
+};
+MODULE_DEVICE_TABLE(of, ip5xxx_power_of_match);
+
+static struct i2c_driver ip5xxx_power_driver = {
+ .probe_new = ip5xxx_power_probe,
+ .driver = {
+ .name = "ip5xxx-power",
+ .of_match_table = ip5xxx_power_of_match,
+ }
+};
+module_i2c_driver(ip5xxx_power_driver);
+
+MODULE_AUTHOR("Samuel Holland <samuel@xxxxxxxxxxxx>");
+MODULE_DESCRIPTION("Injoinic IP5xxx power bank IC driver");
+MODULE_LICENSE("GPL");
--
2.33.1